自定義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!

相關文章
相關標籤/搜索