自定义View之双层波纹气泡(xFermode)

效果图

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


bubble_good.gif

实现思路

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

开始绘制

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

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从而生成波纹效果bash

//新建一个图层
            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的缘由,容我慢慢道来ide

4.画外层的波纹效果post

//新建一个图层
 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混合模式的时候,它是会受图层上的内容的透明度影响,从而使总体的透明度也发生变化,为了避免影响波纹的色彩透明,因此新建了图层实现。ui

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

图片.png

相信了解过xfermode的人应该都看过谷歌官方的这张图可是当你本身去使用的时候会发现结果倒是不一样的,让咱们来看看官方的代码spa

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是会受其余图像透明度影响的
来看看谷歌的文档


图片.png

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


图片.png

总结

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

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

相关文章
相关标签/搜索