作者:Rain_Shieh

效果图

今天给大家带来的是双层波纹气泡效果,有请图片:



Android 自己画气泡背景_贝塞尔曲线

image

bubble_good.gif

实现思路

1.首先计算自定义view的真实宽高和气泡的直径等size
2.画气泡的带透明度背景图
3.新建一个图层画里层的气泡波纹效果,使用xfermode混合模式SRC_IN画一个圆与一个贝塞尔曲线path从而生成波纹效果
4.再新建一个图层画外层的气泡波纹效果
5.最后通过改变画波纹的起始位置及其高度来让波纹动起来

开始绘制

1.自定义view计算宽高及其初始化一些属性

1init {
 2        //关闭渲染
 3        mPaint.isAntiAlias = true
 4        mDrawPaint.isAntiAlias = true
 5        mBubbleTextPaint.isAntiAlias = true
 6        mBubbleTextPaint.color = Color.WHITE
 7        mBubbleTextPaint.style = Paint.Style.FILL
 8        mBubbleTextPaint.textSize = DensityUtils.dp2px(context, 11f).toFloat()
 9        mBgBitmap = BitmapFactory.decodeResource(context.resources, R.drawable.bubble_bg)
10        //关闭硬件加速,否则部分xfermode混合效果会失效
11        setLayerType(View.LAYER_TYPE_SOFTWARE, null)
12    }
13
14 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
15        when (MeasureSpec.getMode(widthMeasureSpec)) {
16            MeasureSpec.EXACTLY -> {
17                mRealWidth = MeasureSpec.getSize(widthMeasureSpec)
18            }
19            MeasureSpec.AT_MOST -> {
20                mRealWidth = mDefaultWidth
21            }
22            MeasureSpec.UNSPECIFIED -> {
23                mRealWidth = mDefaultWidth
24            }
25        }
26        when (MeasureSpec.getMode(heightMeasureSpec)) {
27            MeasureSpec.EXACTLY -> {
28                mRealHeight = MeasureSpec.getSize(heightMeasureSpec)
29            }
30            MeasureSpec.AT_MOST -> {
31                mRealHeight = mDefaultHeight
32            }
33            MeasureSpec.UNSPECIFIED -> {
34                mRealHeight = mDefaultHeight
35            }
36        }
37        initAndCountSize()
38        setMeasuredDimension(mRealWidth, mRealHeight)
39    }
40
41/**42* 初始化计算一些参数43*/    
44private fun initAndCountSize() {
45        mSquareSize = Math.min(mRealWidth, mRealHeight).toFloat()
46        mSquareSize -= 2 * mPadding
47        mCenterX = mRealWidth / 2f
48        mCenterY = mRealHeight / 2f
49        mWaveCount = Math.ceil((mSquareSize / mWaveWidth).toDouble()).toInt()
50        mControlValue = mWaveWidth / 5f * 2f
51        //渐变效果
52        mRadialGradient = RadialGradient(
53            mCenterX, mCenterY, mSquareSize / 3f * 2f, mBubbleLaterColor, mBubbleFrontColor, Shader.TileMode.CLAMP
54        )
55        //将背景图缩放成控件的大小
56        mMatrix.reset()
57        mMatrix.setScale(
58            (mSquareSize + mPadding * 2) / mBgBitmap.width.toFloat(),
59            (mSquareSize + mPadding * 2) / mBgBitmap.height.toFloat()
60        )
61        mRectF.set(0f, 0f, mRealWidth.toFloat(), mRealHeight.toFloat())
62        //画混合需要的背景圆
63        mSrcBitmap = Bitmap.createBitmap(mRealWidth, mRealHeight, Bitmap.Config.ARGB_8888)
64        mSrcCanvas.setBitmap(mSrcBitmap)
65        mPaint.color = Color.WHITE
66        mSrcCanvas.drawCircle(mCenterX, mCenterY, mSquareSize / 2, mPaint)
67        mDstBitmap = Bitmap.createBitmap(mRealWidth, mRealHeight, Bitmap.Config.ARGB_8888)
68        mDstCanvas.setBitmap(mDstBitmap)
69        mDst2Bitmap = Bitmap.createBitmap(mRealWidth, mRealHeight, Bitmap.Config.ARGB_8888)
70        mDst2Canvas.setBitmap(mDst2Bitmap)
71    }

2.画气泡的带透明度背景图

1canvas.drawBitmap(mBgBitmap, mMatrix, null)

3.新建一个图层画里层的气泡波纹效果,使用xfermode混合模式SRC_IN画一个圆与一个贝塞尔曲线path从而生成波纹效果

1            //新建一个图层
 2            mLayerId = canvas.saveLayer(mRectF, mDrawPaint, Canvas.ALL_SAVE_FLAG) 
 3            //先画个圆颜色可随意但是不要透明,因为透明度会影响混合效果
 4            canvas.drawBitmap(mSrcBitmap, 0f, 0f, mDrawPaint)
 5            //设置SRC_IN混合模式
 6            mDrawPaint.xfermode = mPorterDuffXfermode
 7            drawDstBitmap()
 8            canvas.drawBitmap(mDstBitmap, 0f, 0f, mDrawPaint)
 9            mDrawPaint.xfermode = null
10            canvas.restoreToCount(mLayerId)
11
12/**13     * 画里层的贝塞尔曲线Bitmap14     */
15    private fun drawDstBitmap() {
16        //清除掉图像 不然图像会重叠
17        mDstBitmap?.eraseColor(Color.TRANSPARENT)
18        mPaint.color = mBubbleFrontColor
19        mPath.reset()
20        mPath.moveTo(mStartWidth, mCurrentHeight)
21        //使用三阶贝塞尔曲线绘制path
22        for (i in 0 until mWaveCount * 2) {
23            mPath.cubicTo(
24                mStartWidth + mPadding.toFloat() + mWaveWidth * i + mControlValue, mCurrentHeight - mWaveHeight,
25                mStartWidth + mPadding.toFloat() + mWaveWidth * (i + 1) - mControlValue, mCurrentHeight + mWaveHeight,
26                mStartWidth + mPadding.toFloat() + mWaveWidth * (i + 1), mCurrentHeight
27            )
28        }
29        mPath.lineTo(mRealWidth.toFloat(), mRealHeight.toFloat())
30        mPath.lineTo(0f, mRealHeight.toFloat())
31        mPath.close()
32        mDstCanvas.drawPath(mPath, mPaint)
33    }

至于为什么都使用drawBitmap绘制,是因为xfermode的原因,容我慢慢道来

4.画外层的波纹效果

1 //新建一个图层
 2 mLayerId = canvas.saveLayer(mRectF, mDrawPaint, Canvas.ALL_SAVE_FLAG)
 3            canvas.drawBitmap(mSrcBitmap, 0f, 0f, mDrawPaint)
 4            mDrawPaint.xfermode = mPorterDuffXfermode
 5            drawDst2Bitmap()
 6            canvas.drawBitmap(mDst2Bitmap, 0f, 0f, mDrawPaint)
 7            mDrawPaint.xfermode = null
 8            canvas.restoreToCount(mLayerId)
 9
10 /**11     * 画外层的贝塞尔曲线Bitmap12     */
13    private fun drawDst2Bitmap() {
14        //清除掉图像 不然图像会重叠
15        mDst2Bitmap?.eraseColor(Color.TRANSPARENT)
16        mPaint.color = mBubbleLaterColor
17//阴影效果
18//        mPaint.setShadowLayer(10f, 5f, 5f, mBubbleShaderColor)
19//设置径向渐变效果
20        mPaint.shader = mRadialGradient
21        mPath.reset()
22        mPath.moveTo(mStartWidth, mCurrentHeight)
23//使用三阶贝塞尔曲线绘制path
24        for (i in 0 until mWaveCount * 2) {
25            mPath.cubicTo(
26                mStartWidth + mPadding.toFloat() + mWaveWidth * i + mControlValue, mCurrentHeight + mWaveHeight,
27                mStartWidth + mPadding.toFloat() + mWaveWidth * (i + 1) - mControlValue, mCurrentHeight - mWaveHeight,
28                mStartWidth + mPadding.toFloat() + mWaveWidth * (i + 1), mCurrentHeight
29            )
30        }
31        mPath.lineTo(mRealWidth.toFloat(), mRealHeight.toFloat())
32        mPath.lineTo(0f, mRealHeight.toFloat())
33        mPath.close()
34        mDst2Canvas.drawPath(mPath, mPaint)
35//        mPaint.clearShadowLayer()
36        mPaint.shader = null
37    }

5.让波纹动起来通过控制贝塞尔曲线开始绘制的起点不断平移来实现,上升下降效果类似

1 /** 2     * 开启动画 3     */
 4    fun startAnimator() {
 5        while (mIsOpen) {
 6            Thread.sleep(10)
 7            startGo()
 8            startUpDown()
 9            postInvalidate()
10        }
11    }
12
13/**14     * 加入大波浪效果15     */
16    private fun startGo() {
17        if (mStartWidth >= 0) {
18            mStartWidth = -mWaveCount * mWaveWidth
19        }
20        mStartWidth += mSpeedGo
21    }
22
23    /**24     * 加入上升下降效果25     */
26    private fun startUpDown() {
27        if (mIsUp) {
28            if (mPercent >= 100) {
29                mIsUp = false
30                mPercent -= mSpeedUp
31            } else {
32                mPercent += mSpeedUp
33            }
34        } else {
35            if (mPercent <= 0f) {
36                mIsUp = true
37                mPercent += mSpeedUp
38            } else {
39                mPercent -= mSpeedUp
40            }
41        }
42    }
43
44private var mRunnable = Runnable {
45        startAnimator()
46    }
47
48override fun onAttachedToWindow() {
49        super.onAttachedToWindow()
50        mIsOpen = true
51        ThreadPoolManage.getInstance().execute(mRunnable)
52    }
53
54    override fun onDetachedFromWindow() {
55        super.onDetachedFromWindow()
56        mIsOpen = false
57        ThreadPoolManage.getInstance().remove(mRunnable)
58    }

大概的绘制步骤差不多完成了,绘制的时候大家可以会有些疑问,为什么要新建这个多个图层,还有为什么要使用drawBitmap绘制?
因为我们使用xfermode混合模式的时候,它是会受图层上的内容的透明度影响,从而使整体的透明度也发生变化,为了不影响波纹的色彩透明,所以新建了图层实现。

关于为什么要用drawBitmap绘制,那得说说xfermode里面的坑了



Android 自己画气泡背景_图层_02

image

图片.png

相信了解过xfermode的人应该都看过谷歌官方的这张图但是当你自己去使用的时候会发现结果却是不同的,让我们来看看官方的代码

1static Bitmap makeDst(int w, int h) {
 2Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
 3Canvas c = new Canvas(bm);
 4Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
 5
 6p.setColor(0xFFFFCC44);
 7c.drawOval(new RectF(0, 0, w*3/4, h*3/4), p);
 8return bm;
 9}
10
11// create a bitmap with a rect, used for the "src" image
12static Bitmap makeSrc(int w, int h) {
13Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
14Canvas c = new Canvas(bm);
15Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
16
17p.setColor(0xFF66AAFF);
18c.drawRect(w/3, h/3, w*19/20, h*19/20, p);
19return bm;
20}

可以看到谷歌是通过在一个新的画布上画bitmap,尽可能让绘制不受其他图像的影响,因为xfermode是会受其他图像透明度影响的
来看看谷歌的文档



Android 自己画气泡背景_混合模式_03

image

图片.png

这里有18种模式,右边是每种模式对应的计算公式
数组中前一个代表alpha,后一个代表color
sa:源图像的alpha
sc:源图像的color
da:目标图像的alpha
dc:目标图像的color
可以从中看到生成的图片是会受源图像目标图像及其他们的透明度所影响
至于为什么要关闭硬件加速,是因为谷歌说了xfermode部分模式不支持



Android 自己画气泡背景_图层_04

image

图片.png

总结

画双层波纹气泡主要是通过贝塞尔曲线控制波纹的幅度,使用xfermode来实现混合效果
使用xfermode来实现如谷歌官方图上的预测效果,使用建议:
1.关闭硬件加速
2.混合的图层尽可能的纯净,可以用canvas.saveLayer()新建图层,防止受其他不需要混合的图像所影响
3.需要使用canvas.drawBitmap()去绘制图像(不使用的话部分模式和预测效果有差异)
4.透明度会影响xfermode的生成的图像透明色值

注意上述的一些细节,合成的图片效果更好的达到预测的效果。
源码链接见于:github.com/RainCCC/Cus…
Thanks!