效果图
今天给大家带来的是双层波纹气泡效果,有请图片:
实现思路
1.首先计算自定义view的真实宽高和气泡的直径等size
2.画气泡的带透明度背景图
3.新建一个图层画里层的气泡波纹效果,使用xfermode混合模式SRC_IN画一个圆与一个贝塞尔曲线path从而生成波纹效果
4.再新建一个图层画外层的气泡波纹效果
5.最后通过改变画波纹的起始位置及其高度来让波纹动起来
开始绘制
1.自定义view计算宽高及其初始化一些属性
init {
//关闭渲染
mPaint.isAntiAlias = true
mDrawPaint.isAntiAlias = true
mBubbleTextPaint.isAntiAlias = true
mBubbleTextPaint.color = Color.WHITE
mBubbleTextPaint.style = Paint.Style.FILL
mBubbleTextPaint.textSize = DensityUtils.dp2px(context, 11f).toFloat()
mBgBitmap = BitmapFactory.decodeResource(context.resources, R.drawable.bubble_bg)
//关闭硬件加速,否则部分xfermode混合效果会失效
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
when (MeasureSpec.getMode(widthMeasureSpec)) {
MeasureSpec.EXACTLY -> {
mRealWidth = MeasureSpec.getSize(widthMeasureSpec)
}
MeasureSpec.AT_MOST -> {
mRealWidth = mDefaultWidth
}
MeasureSpec.UNSPECIFIED -> {
mRealWidth = mDefaultWidth
}
}
when (MeasureSpec.getMode(heightMeasureSpec)) {
MeasureSpec.EXACTLY -> {
mRealHeight = MeasureSpec.getSize(heightMeasureSpec)
}
MeasureSpec.AT_MOST -> {
mRealHeight = mDefaultHeight
}
MeasureSpec.UNSPECIFIED -> {
mRealHeight = mDefaultHeight
}
}
initAndCountSize()
setMeasuredDimension(mRealWidth, mRealHeight)
}
/**
* 初始化计算一些参数
*/
private fun initAndCountSize() {
mSquareSize = Math.min(mRealWidth, mRealHeight).toFloat()
mSquareSize -= 2 * mPadding
mCenterX = mRealWidth / 2f
mCenterY = mRealHeight / 2f
mWaveCount = Math.ceil((mSquareSize / mWaveWidth).toDouble()).toInt()
mControlValue = mWaveWidth / 5f * 2f
//渐变效果
mRadialGradient = RadialGradient(
mCenterX, mCenterY, mSquareSize / 3f * 2f, mBubbleLaterColor, mBubbleFrontColor, Shader.TileMode.CLAMP
)
//将背景图缩放成控件的大小
mMatrix.reset()
mMatrix.setScale(
(mSquareSize + mPadding * 2) / mBgBitmap.width.toFloat(),
(mSquareSize + mPadding * 2) / mBgBitmap.height.toFloat()
)
mRectF.set(0f, 0f, mRealWidth.toFloat(), mRealHeight.toFloat())
//画混合需要的背景圆
mSrcBitmap = Bitmap.createBitmap(mRealWidth, mRealHeight, Bitmap.Config.ARGB_8888)
mSrcCanvas.setBitmap(mSrcBitmap)
mPaint.color = Color.WHITE
mSrcCanvas.drawCircle(mCenterX, mCenterY, mSquareSize / 2, mPaint)
mDstBitmap = Bitmap.createBitmap(mRealWidth, mRealHeight, Bitmap.Config.ARGB_8888)
mDstCanvas.setBitmap(mDstBitmap)
mDst2Bitmap = Bitmap.createBitmap(mRealWidth, mRealHeight, Bitmap.Config.ARGB_8888)
mDst2Canvas.setBitmap(mDst2Bitmap)
}
2.画气泡的带透明度背景图
canvas.drawBitmap(mBgBitmap, mMatrix, null)
3.新建一个图层画里层的气泡波纹效果,使用xfermode混合模式SRC_IN画一个圆与一个贝塞尔曲线path从而生成波纹效果
//新建一个图层
mLayerId = canvas.saveLayer(mRectF, mDrawPaint, Canvas.ALL_SAVE_FLAG)
//先画个圆颜色可随意但是不要透明,因为透明度会影响混合效果
canvas.drawBitmap(mSrcBitmap, 0f, 0f, mDrawPaint)
//设置SRC_IN混合模式
mDrawPaint.xfermode = mPorterDuffXfermode
drawDstBitmap()
canvas.drawBitmap(mDstBitmap, 0f, 0f, mDrawPaint)
mDrawPaint.xfermode = null
canvas.restoreToCount(mLayerId)
/**
* 画里层的贝塞尔曲线Bitmap
*/
private fun drawDstBitmap() {
//清除掉图像 不然图像会重叠
mDstBitmap?.eraseColor(Color.TRANSPARENT)
mPaint.color = mBubbleFrontColor
mPath.reset()
mPath.moveTo(mStartWidth, mCurrentHeight)
//使用三阶贝塞尔曲线绘制path
for (i in 0 until mWaveCount * 2) {
mPath.cubicTo(
mStartWidth + mPadding.toFloat() + mWaveWidth * i + mControlValue, mCurrentHeight - mWaveHeight,
mStartWidth + mPadding.toFloat() + mWaveWidth * (i + 1) - mControlValue, mCurrentHeight + mWaveHeight,
mStartWidth + mPadding.toFloat() + mWaveWidth * (i + 1), mCurrentHeight
)
}
mPath.lineTo(mRealWidth.toFloat(), mRealHeight.toFloat())
mPath.lineTo(0f, mRealHeight.toFloat())
mPath.close()
mDstCanvas.drawPath(mPath, mPaint)
}
至于为什么都使用drawBitmap绘制,是因为xfermode的原因,容我慢慢道来
4.画外层的波纹效果
//新建一个图层
mLayerId = canvas.saveLayer(mRectF, mDrawPaint, Canvas.ALL_SAVE_FLAG)
canvas.drawBitmap(mSrcBitmap, 0f, 0f, mDrawPaint)
mDrawPaint.xfermode = mPorterDuffXfermode
drawDst2Bitmap()
canvas.drawBitmap(mDst2Bitmap, 0f, 0f, mDrawPaint)
mDrawPaint.xfermode = null
canvas.restoreToCount(mLayerId)
/**
* 画外层的贝塞尔曲线Bitmap
*/
private fun drawDst2Bitmap() {
//清除掉图像 不然图像会重叠
mDst2Bitmap?.eraseColor(Color.TRANSPARENT)
mPaint.color = mBubbleLaterColor
//阴影效果
// mPaint.setShadowLayer(10f, 5f, 5f, mBubbleShaderColor)
//设置径向渐变效果
mPaint.shader = mRadialGradient
mPath.reset()
mPath.moveTo(mStartWidth, mCurrentHeight)
//使用三阶贝塞尔曲线绘制path
for (i in 0 until mWaveCount * 2) {
mPath.cubicTo(
mStartWidth + mPadding.toFloat() + mWaveWidth * i + mControlValue, mCurrentHeight + mWaveHeight,
mStartWidth + mPadding.toFloat() + mWaveWidth * (i + 1) - mControlValue, mCurrentHeight - mWaveHeight,
mStartWidth + mPadding.toFloat() + mWaveWidth * (i + 1), mCurrentHeight
)
}
mPath.lineTo(mRealWidth.toFloat(), mRealHeight.toFloat())
mPath.lineTo(0f, mRealHeight.toFloat())
mPath.close()
mDst2Canvas.drawPath(mPath, mPaint)
// mPaint.clearShadowLayer()
mPaint.shader = null
}
5.让波纹动起来通过控制贝塞尔曲线开始绘制的起点不断平移来实现,上升下降效果类似
/**
* 开启动画
*/
fun startAnimator() {
while (mIsOpen) {
Thread.sleep(10)
startGo()
startUpDown()
postInvalidate()
}
}
/**
* 加入大波浪效果
*/
private fun startGo() {
if (mStartWidth >= 0) {
mStartWidth = -mWaveCount * mWaveWidth
}
mStartWidth += mSpeedGo
}
/**
* 加入上升下降效果
*/
private fun startUpDown() {
if (mIsUp) {
if (mPercent >= 100) {
mIsUp = false
mPercent -= mSpeedUp
} else {
mPercent += mSpeedUp
}
} else {
if (mPercent <= 0f) {
mIsUp = true
mPercent += mSpeedUp
} else {
mPercent -= mSpeedUp
}
}
}
private var mRunnable = Runnable {
startAnimator()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
mIsOpen = true
ThreadPoolManage.getInstance().execute(mRunnable)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mIsOpen = false
ThreadPoolManage.getInstance().remove(mRunnable)
}
大概的绘制步骤差不多完成了,绘制的时候大家可以会有些疑问,为什么要新建这个多个图层,还有为什么要使用drawBitmap绘制?
因为我们使用xfermode混合模式的时候,它是会受图层上的内容的透明度影响,从而使整体的透明度也发生变化,为了不影响波纹的色彩透明,所以新建了图层实现。
关于为什么要用drawBitmap绘制,那得说说xfermode里面的坑了
相信了解过xfermode的人应该都看过谷歌官方的这张图但是当你自己去使用的时候会发现结果却是不同的,让我们来看看官方的代码
static Bitmap makeDst(int w, int h) {
Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(0xFFFFCC44);
c.drawOval(new RectF(0, 0, w*3/4, h*3/4), p);
return bm;
}
// create a bitmap with a rect, used for the "src" image
static Bitmap makeSrc(int w, int h) {
Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(0xFF66AAFF);
c.drawRect(w/3, h/3, w*19/20, h*19/20, p);
return bm;
}
可以看到谷歌是通过在一个新的画布上画bitmap,尽可能让绘制不受其他图像的影响,因为xfermode是会受其他图像透明度影响的
来看看谷歌的文档
这里有18种模式,右边是每种模式对应的计算公式
数组中前一个代表alpha,后一个代表color
sa:源图像的alpha
sc:源图像的color
da:目标图像的alpha
dc:目标图像的color
可以从中看到生成的图片是会受源图像目标图像及其他们的透明度所影响
至于为什么要关闭硬件加速,是因为谷歌说了xfermode部分模式不支持
总结
画双层波纹气泡主要是通过贝塞尔曲线控制波纹的幅度,使用xfermode来实现混合效果
使用xfermode来实现如谷歌官方图上的预测效果,使用建议:
1.关闭硬件加速
2.混合的图层尽可能的纯净,可以用canvas.saveLayer()新建图层,防止受其他不需要混合的图像所影响
3.需要使用canvas.drawBitmap()去绘制图像(不使用的话部分模式和预测效果有差异)
4.透明度会影响xfermode的生成的图像透明色值
注意上述的一些细节,合成的图片效果更好的达到预测的效果。
源码链接见于:https://github.com/RainCCC/CustomViewUtil
Thanks!