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

效果图

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


bubble_good.gif

实现思路

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里面的坑了

图片.png

相信了解过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是会受其他图像透明度影响的
来看看谷歌的文档


图片.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的生成的图像透明色值

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容