Android-自定義氣泡View,讓我們告別.9圖

實踐背景

在即時通訊類應用里胧后,很常見各種氣泡布局包裹消息芋浮,通常我們采用.9圖實現(xiàn)。但是使用氣泡圖片面臨著間距不可控壳快,如果是圖片消息纸巷,此方法就無法實現(xiàn)氣泡。本文將介紹如何更加用優(yōu)雅的方式去實現(xiàn)自定義氣泡布局眶痰。

PS前置知識: 如何自定義view瘤旨、XFermode混合圖層、path概念以及貝賽爾曲線竖伯。
自定義View知識 可以在這里找一些文章補充學習存哲。

慣例,我們先看下最終要實現(xiàn)的效果圖七婴,如下圖祟偷,總共有4種類型,基本滿足日常需要打厘,可以根據(jù)需要再進行擴展修肠。

image.png

自定義氣泡View思路分析

1.圖形基本分析

以上四種常見氣泡,從外形上看是圓角帶犄角户盯,文字內(nèi)容在氣泡的矩形內(nèi)嵌施,圖片被裁剪部分。
圖片類型的氣泡上先舷,犄角部分是帶有圖片的一部分艰管,而且在圖片的左右下角有一個提示類型的圖片(特殊UI需要),上圖中未體現(xiàn)該效果蒋川。文字類型氣泡特殊一些牲芋,在單個字的時候,文字是居中的捺球,然后左右內(nèi)間距和多文字下的間距不一樣(UI要求)缸浦,但是從整體上也符合氣泡的通用裁剪規(guī)則。容器類型氣泡氮兵,內(nèi)部子view可隨意布置裂逐,但是最終顯示區(qū)域只有氣泡部分,這樣可擴展度搞泣栈。

2.實現(xiàn)思路分析

那么我們?nèi)绾稳バ纬蛇@種View呢卜高?最開始我接觸到的代碼是用drawable加載.9圖的方式弥姻,但是新UI效果圖片類型氣泡就無法下手了〔籼危可供采用的方案有2種庭敦,canvas的clipPath和XFermode圖層混合。

PS:目前任何布局類型都是四邊形的薪缆,而那些各種形狀的布局秧廉,其實只是四邊形布局只顯示其中部分區(qū)域而已。

  • clipPath: 通過對canvas的裁剪形成氣泡布局拣帽,先用描繪出一個氣泡模樣的path疼电,然后按照這個path把畫布裁剪,然后再這個畫布上繪制內(nèi)容减拭。具體操作下面會介紹蔽豺。

  • XFermode圖層混合:XFermode可以通過多個圖層進行疊加按照一定規(guī)則保留部分圖形區(qū)域。利用這個特性峡谊,我們先繪制原始圖形茫虽,然后在這個執(zhí)行之后,依然先描繪出氣泡path既们,我們可以給paint畫筆設置PorterDuffXfermode濒析,然后用畫筆帶上DST_IN模式進行圖層疊加。

  • 無論哪種方式啥纸,都是需要path的号杏,可以先看看自定義View知識

具體代碼實踐分析

1.簡單介紹下本文所需的自定義View知識

首先我們要知道自定義View需要做哪些代碼準備,一般來說斯棒,onDraw是必然需要的盾致。根據(jù)需要onMeasure,onSizeChange荣暮,onLayout庭惜,dispatchDraw等方法有時候也需要。本文是實現(xiàn)一個氣泡view穗酥,有單一顯示視圖的自定義view以及容器類型的氣泡护赊。所以大家需要了解onDraw、onSizeChange砾跃、dispatchDraw等需要重寫的方法骏啰。此外,了解invalidate和postInvalidate等刷新View視圖方法抽高。

onDraw

這個方法是核心判耕,主要是用來描繪出你展示的視圖界面。比如你想畫一個花翘骂,那么這個地方進行最終的描繪工作壁熄。在部分情況下帚豪,這里會在繼承某個View情況下,增加一些繪制请毛,此時會繼續(xù)調用super.onDraw(canvas); 志鞍,這樣保留父view的圖形。

onSizeChange

在進行ondraw之前會有若干次調用onSizeChange方仿,這里可以用來提前獲取當前view的最新高寬,比如下面代碼统翩。

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mHeight = h;
        mWidth = w;

    }
dispatchDraw

這個方法在本次自定義view中主要是給容器布局使用仙蚜,調用順序在ondraw之后,重寫這個用來在分發(fā)繪制內(nèi)部子view時厂汗,繪制需要的背景色以及進行圖層裁剪形成氣泡委粉。

2.ClipPath方式實現(xiàn)策略

以文本氣泡View為例,我們先思考氣泡的圖形path如何形成娶桦。首先我們先繼承TextView贾节,因為我們要在這個基礎上實現(xiàn)文字氣泡。圖形大致上拆分為一個圓角矩形衷畦,然后再左側或者右側畫一個犄角栗涂。犄角是帶有一定弧度的,這個和UI溝通過祈争,我當初是通過px一點點調整最終給到UI滿意的弧度斤程。圓角矩形的繪制就不多說了,看API菩混。犄角忿墅,是通過2條二階貝塞爾曲線形成的,上面一條沮峡,下面一條(關于path的介紹很多疚脐,看上面的預學習鏈接,或者自行搜索)邢疙。

    protected void onDraw(Canvas canvas) {
        canvas.setDrawFilter(mPaintFlagsDrawFilter);
//        LogUtil.i(TAG, getText() + "  getPaddingLeft" + getPaddingLeft() + "  getPaddingRight" + getPaddingRight());
        mSrcPath.reset();
        if (mIsRightPop) {
            mRoundRect.set(0, 0, mWidth - mWidthDiff, mHeight);
            mSrcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW);
            //給path增加右側的犄角棍弄,形成氣泡效果
            mSrcPath.moveTo(mWidth - mWidthDiff, mRoundRadius);
            mSrcPath.quadTo(mTopControl.x, mTopControl.y, mWidth, mRoundRadius - mDefaultCornerPadding);
            mSrcPath.quadTo(mBottomControl.x, mBottomControl.y, mWidth - mWidthDiff,
                    mRoundRadius + mWidthDiff);
        } else {
            mRoundRect.set(mWidthDiff, 0, mWidth, mHeight);
            mSrcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW);
            //給path增加右側的犄角,形成氣泡效果
            mSrcPath.moveTo(mWidthDiff, mRoundRadius);
            mSrcPath.quadTo(mTopControl.x, mTopControl.y, 0, mRoundRadius - mDefaultCornerPadding);
            mSrcPath.quadTo(mBottomControl.x, mBottomControl.y, mWidthDiff, mRoundRadius + mWidthDiff);
        }
        canvas.clipPath(mSrcPath);
        if (mLoadingBackColor != 0) {
            canvas.drawColor(mLoadingBackColor);
        }
        super.onDraw(canvas);

    }

代碼分析:addRoundRect是增加一個圓角矩形秘症,然后大小比整個view小一個mWidthDiff(這個是犄角的寬度照卦,預留位置給犄角繪制)。然后把path的繪制起點移動到 mSrcPath.moveTo(mWidthDiff, mRoundRadius);(左右恰好相反乡摹,這里是左側犄角的寫法)下圖中的綠色點役耕。Y坐標是mRoundRadius是因為圓角矩形的圓角值是這個,所以在圓角的結束位置開始繪制犄角聪廉。


這里開始繪制上面那條犄角的弧線瞬痘,TopControl控制點我是專門寫了一個界面故慈,不斷調整xy坐標px值進行計算。目前數(shù)值如下:

 mSrcPath.quadTo(mTopControl.x, mTopControl.y, 0, mRoundRadius - mDefaultCornerPadding);
            mSrcPath.quadTo(mBottomControl.x, mBottomControl.y, mWidthDiff, mRoundRadius + mWidthDiff);
 private void initValues() {
        if (mIsRightPop) {
            //設置犄角的控制橫坐標xy
            mTopControl.x = mWidth - DensityUtil.dip2px(getContext(), 2);
            mTopControl.y = mRoundRadius;
            mBottomControl.x = mWidth - DensityUtil.dip2px(getContext(), 1);
            mBottomControl.y = mRoundRadius + DensityUtil.dip2px(getContext(), 6);
        } else {
            //設置犄角的控制橫坐標xy
            mTopControl.x = DensityUtil.dip2px(getContext(), 2);
            mTopControl.y = mRoundRadius;
            mBottomControl.x = DensityUtil.dip2px(getContext(), 1);
            mBottomControl.y = mRoundRadius + DensityUtil.dip2px(getContext(), 6);
        }

    }

在上面ondraw代碼執(zhí)行后框全,開始進行裁剪畫布察绷。

canvas.clipPath(mSrcPath);

此時,畫布就只有氣泡模樣的區(qū)域了津辩。接下來就簡單了拆撼,直接進行原圖形繪制以及內(nèi)部填充色的繪制。

//繪制背景色
  if (mLoadingBackColor != 0) {
       canvas.drawColor(mLoadingBackColor);
   }
  //下面是繪制textview自身具備的圖形喘沿,保留原有的所有特性闸度。
   super.onDraw(canvas);

值得注意的是文字類型氣泡有很多特殊點,比如文件要顯示在氣泡內(nèi)部蚜印,或者UI要求單個文字時莺禁,文字距離氣泡邊緣的間距不一樣等等。所以接下來介紹一些特殊之處窄赋,不過先貼出所有代碼哟冬。

package com.tc.bubblelayout;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;

/**
 * author:   tc
 * date:      2018/3/14 & 10:04
 * version    1.0
 * description 透明氣泡view
 * modify by
 */
public class BubbleTextView extends AppCompatTextView {
    private static final String TAG = "BubbleTextView";
    private Path mSrcPath;
    private int mHeight;
    private int mWidth;
    private RectF mRoundRect;
    /**
     * 上弧線控制點和下弧線控制點
     */
    private PointF mTopControl, mBottomControl;

    /**
     * 氣泡圖形右側留空區(qū)域寬度
     */
    private int mWidthDiff;
    /**
     * 右上角圓角的半徑
     */
    private int mRoundRadius;
    /**
     * 是否是右側氣泡
     */
    private boolean mIsRightPop;
    private int mLeftTextPadding;
    private int mRightTextPadding;

    /**
     * 加載時背景色
     */
    private int mLoadingBackColor;
    private int mDefaultPadding;
    private int mDefaultCornerPadding;
    private PaintFlagsDrawFilter mPaintFlagsDrawFilter;

    public BubbleTextView(Context context) {
        this(context, null);
    }

    public BubbleTextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BubbleTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.BubbleView);
        mLoadingBackColor = attr.getColor(R.styleable.BubbleView_BubbleView_backgroundColor, 0);
        mIsRightPop = attr.getBoolean(R.styleable.BubbleView_BubbleView_rightPop, true);
        //左側或右側留出的空余區(qū)域
        mWidthDiff = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_blank_space_width,
                DensityUtil.dip2px(getContext(), 7));
        //圓角的半徑
        mRoundRadius = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_roundRadius,
                DensityUtil.dip2px(context, 8));
        mLeftTextPadding = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_leftTextPadding,
                DensityUtil.dip2px(context, 0));
        mRightTextPadding = attr.getDimensionPixelOffset(R.styleable.BubbleView_BubbleView_rightTextPadding,
                DensityUtil.dip2px(context, 0));
        attr.recycle();
        mSrcPath = new Path();
        mTopControl = new PointF(0, 0);
        mBottomControl = new PointF(0, 0);
        mRoundRect = new RectF();
        //默認一個字的時候的間隔
        mDefaultPadding = DensityUtil.dip2px(getContext(), 16);
        mDefaultCornerPadding = DensityUtil.dip2px(getContext(), 3);
        mPaintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint
                .FILTER_BITMAP_FLAG);
        setTextPadding(mRightTextPadding, mLeftTextPadding);
    }


    private void initValues() {
        if (mIsRightPop) {
            //設置犄角的控制橫坐標xy
            mTopControl.x = mWidth - DensityUtil.dip2px(getContext(), 2);
            mTopControl.y = mRoundRadius;
            mBottomControl.x = mWidth - DensityUtil.dip2px(getContext(), 1);
            mBottomControl.y = mRoundRadius + DensityUtil.dip2px(getContext(), 6);
        } else {
            //設置犄角的控制橫坐標xy
            mTopControl.x = DensityUtil.dip2px(getContext(), 2);
            mTopControl.y = mRoundRadius;
            mBottomControl.x = DensityUtil.dip2px(getContext(), 1);
            mBottomControl.y = mRoundRadius + DensityUtil.dip2px(getContext(), 6);
        }

    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mHeight = h;
        mWidth = w;
        initValues();
    }

    public void judgePadding() {
        int length = getText().length();
        if (length == 1) {
            setTextPadding(mDefaultPadding, mDefaultPadding);
        } else {
            setTextPadding(mRightTextPadding, mLeftTextPadding);
        }
    }


    @Override
    protected void onDraw(Canvas canvas) {
        canvas.setDrawFilter(mPaintFlagsDrawFilter);
//        LogUtil.i(TAG, getText() + "  getPaddingLeft" + getPaddingLeft() + "  getPaddingRight" + getPaddingRight());
        mSrcPath.reset();
        if (mIsRightPop) {
            mRoundRect.set(0, 0, mWidth - mWidthDiff, mHeight);
            mSrcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW);
            //給path增加右側的犄角,形成氣泡效果
            mSrcPath.moveTo(mWidth - mWidthDiff, mRoundRadius);
            mSrcPath.quadTo(mTopControl.x, mTopControl.y, mWidth, mRoundRadius - mDefaultCornerPadding);
            mSrcPath.quadTo(mBottomControl.x, mBottomControl.y, mWidth - mWidthDiff,
                    mRoundRadius + mWidthDiff);
        } else {
            mRoundRect.set(mWidthDiff, 0, mWidth, mHeight);
            mSrcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW);
            //給path增加右側的犄角忆绰,形成氣泡效果
            mSrcPath.moveTo(mWidthDiff, mRoundRadius);
            mSrcPath.quadTo(mTopControl.x, mTopControl.y, 0, mRoundRadius - mDefaultCornerPadding);
            mSrcPath.quadTo(mBottomControl.x, mBottomControl.y, mWidthDiff, mRoundRadius + mWidthDiff);
        }
        canvas.clipPath(mSrcPath);
        if (mLoadingBackColor != 0) {
            canvas.drawColor(mLoadingBackColor);
        }
        super.onDraw(canvas);

    }

    private void setTextPadding(int rightTextPadding, int leftTextPadding) {
        if (mIsRightPop) {
            setPadding(leftTextPadding, getPaddingTop(), rightTextPadding + mWidthDiff, getPaddingBottom());
        } else {
            setPadding(leftTextPadding + mWidthDiff, getPaddingTop(), rightTextPadding, getPaddingBottom());
        }
    }

    public void setLoadingBackColor(int loadingBackColor) {
        if (loadingBackColor <= 0) {
            mLoadingBackColor = 0;
            return;
        }
        mLoadingBackColor = getResources().getColor(loadingBackColor);
    }

    public void setLeftTextPadding(int leftTextPadding) {
        mLeftTextPadding = DensityUtil.dip2px(getContext(), leftTextPadding);
    }

    public void setRightTextPadding(int rightTextPadding) {
        mRightTextPadding = DensityUtil.dip2px(getContext(), rightTextPadding);
    }

    public void updateView() {
        judgePadding();
        invalidate();
    }

    /**
     * 設置圓角的半徑
     *
     * @param roundRadius
     */
    public void setRoundRadius(int roundRadius) {
        mRoundRadius = DensityUtil.dip2px(getContext(), roundRadius);
    }


    /**
     * 是否是右側氣泡
     *
     * @param rightPop 是否是右側氣泡 false則為左側氣泡
     */
    public void setRightPop(boolean rightPop) {
        mIsRightPop = rightPop;
    }
}

代碼中的judgePadding方法主要是為了設置不同的兩側留白間距浩峡,在文本長度為1的時候,UI需要采取不同的間距较木。這里大家可以根據(jù)需要自行決定是否保留這種邏輯红符。此外這個方法調用的setTextPadding方法是用來控制文本內(nèi)容顯示不會被氣泡部分裁剪掉,所以當左右側氣泡時有默認的間距mWidthDiff伐债,在繪制文本時预侯,通過設置padding,增加這個左或右間距值峰锁。下圖中萎馅,leftPadding和rightPadding屬于黃色框部分留白,紅色是mWidthDiff虹蒋。


image.png

3.XFermode方式實現(xiàn)策略

本方法需要先了解XFermode是什么糜芳,如果上面文章還沒看過,我這里先簡單介紹下魄衅。XFermode可以通俗的理解成為數(shù)學上合并集運算峭竣,X∪Y或者X∩Y等等。這里XFermode和它類似晃虫,只不過是對圖形的相交或者不相交區(qū)域按照一定規(guī)則取想要保留的圖形區(qū)域皆撩。比如下圖
畫了一個黃色圓形和一個藍色方形,然后根據(jù)16種XFermode模式進行取想要保留的圖形區(qū)域。

image.png

PS:圖片轉自http://www.reibang.com/p/78c36742d50f 扛吞,這篇文章分析為啥官方的圖層混合和我們實際效果不一致的問題呻惕,大家可以看看。

關于path氣泡部分的計算滥比,和上面clippath是一致的亚脆,這個保持不變。有點不同的是在之前的XFermode實現(xiàn)方案里盲泛,我直接在同一個canvas上先繪制背景色和調用 super.onDraw(canvas);繪制原始圖形濒持,然后再用這個canvas繪制氣泡path(調用 canvas.drawPath(srcPath, mPaint);)。大致代碼如下,mPaint已經(jīng)在初始化時設置了setXfermode的具體模式為DST_IN寺滚。

 @Override
    protected void onDraw(Canvas canvas) {
        int saveCount = canvas.saveLayerAlpha(0, 0, getWidth(), getHeight(), 255,
                Canvas.ALL_SAVE_FLAG);
        if (mLoadingBackColor != 0) {
            canvas.drawColor(getResources().getColor(mLoadingBackColor));
        }
        super.onDraw(canvas);
        if (mShowButtonBitmap != null) {
            int bitmapHeight = mShowButtonBitmap.getHeight();
            int bitmapWidth = mShowButtonBitmap.getWidth();
            int top = mHeight - bitmapHeight - DensityUtil.dip2px(getContext(), 5);
            if (mIsRightPop) {
                canvas.drawBitmap(mShowButtonBitmap, DensityUtil.dip2px
                        (getContext(), 5), top, mPaint);
            } else {
                canvas.drawBitmap(mShowButtonBitmap, mWidth - bitmapWidth - DensityUtil.dip2px
                        (getContext(), 5), top, mPaint);
            }
        }

        mPaint.setXfermode(mPorterDuffXfermode);
        srcPath.reset();
        if (mIsRightPop) {
            mRoundRect.set(0, 0, mWidth - mWidthDiff, mHeight);
        } else {
            mRoundRect.set(mWidthDiff, 0, mWidth, mHeight);
        }

        srcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW);

        if (mIsRightPop) {
            //給path增加右側的犄角弥喉,形成氣泡效果
            srcPath.moveTo(mWidth - mWidthDiff, mRoundRadius);
            srcPath.quadTo(topControl.x, topControl.y, mWidth, mRoundRadius - DensityUtil.dip2px(getContext(), 3));
            srcPath.quadTo(bottomControl.x, bottomControl.y, mWidth - mWidthDiff,
                    mRoundRadius + mWidthDiff);
        } else {
            //給path增加右側的犄角,形成氣泡效果
            srcPath.moveTo(mWidthDiff, mRoundRadius);
            srcPath.quadTo(topControl.x, topControl.y, 0, mRoundRadius - DensityUtil.dip2px(getContext(), 3));
            srcPath.quadTo(bottomControl.x, bottomControl.y, mWidthDiff, mRoundRadius + mWidthDiff);
        }


        //繪制path所形成的圖形玛迄,清除形成透明效果,露出這一區(qū)域
        canvas.drawPath(srcPath, mPaint);


        mPaint.setXfermode(null);
        canvas.restoreToCount(saveCount);

    }

canvas.saveLayerAlpha方法是新建立一個layer層棚亩,之后所有的繪制都在這個圖層上蓖议,這樣xfermode才能操作合并圖形。上面代碼大概邏輯就是新建layer層讥蟆,繪制背景色勒虾,繪制原圖形super.draw(因為很多時候都是繼承某個控件,要保留原有的圖形)瘸彤,繪制UI要求的提示圖形修然,然后用paint設置xfermode去繪制氣泡path,最終完成圖形混合保留氣泡部分圖形质况。

在一開始這種邏輯很好實現(xiàn)了氣泡布局愕宋,后續(xù)我把clippath的方式都替換成這個方式了。但是最近Android9.0系統(tǒng)出現(xiàn)后结榄,我更新了我的小米手機到9.0系統(tǒng)中贝,我發(fā)現(xiàn)這種方式不起作用了,繪制出來的view沒有被裁剪為氣泡臼朗。最后試了不同的混合模式邻寿,然后調整path和super.draw之間的順序,然后始終會有一些問題视哑。最后采用氣泡先繪制到另外一個canvas绣否,然后合成為bitmap,最后再把bitmap和現(xiàn)在canvas圖形進行混合挡毅。

改進的策略完美解決了9.0上代碼不起作用的問題蒜撮。代碼如下:

 @Override
    protected void onDraw(Canvas canvas) {
        canvas.setDrawFilter(mPaintFlagsDrawFilter);
        int saveCount = canvas.saveLayerAlpha(0, 0, mWidth, mHeight, 255,
                Canvas.ALL_SAVE_FLAG);
        drawBackColor(canvas);
        super.onDraw(canvas);
        drawTipIcon(canvas);

        mPaint.setXfermode(mPorterDuffXfermode);
        //繪制氣泡部分,和 super.onDraw(canvas);繪制的畫面利用xfermode做疊加計算
        canvas.drawBitmap(mBubbleBitmap, 0, 0, mPaint);

        mPaint.setXfermode(null);
        canvas.restoreToCount(saveCount);
    }

全部代碼就不貼了慷嗜,文末給出鏈接淀弹,可以自己下載看丹壕,其它部分代碼和clippath方式差別不大。

4.容器氣泡布局實現(xiàn)的特殊處

在容器氣泡布局實現(xiàn)時薇溃,子view的ondraw是不需要重寫以及干涉的菌赖。我們只要限制自view的繪制范圍或者在最終圖形上裁剪。所以基于這種思路沐序,我們ondraw的重寫放到dispatchDraw(上面第一小節(jié)介紹過了)琉用,這個方法在ondraw調用之后,所以這里可以做一些背景色和裁剪布局圖形的工作策幼。

@Override
    protected void dispatchDraw(Canvas canvas) {
        canvas.setDrawFilter(mPaintFlagsDrawFilter);
        int saveCount = canvas.saveLayerAlpha(0, 0, mWidth, mHeight, 255,
                Canvas.ALL_SAVE_FLAG);
        drawBackColor(canvas);
        super.dispatchDraw(canvas);

        mPaint.setXfermode(mPorterDuffXfermode);
        //繪制氣泡部分邑时,和 super.onDraw(canvas);繪制的畫面利用xfermode做疊加計算
        canvas.drawBitmap(mBubbleBitmap, 0, 0, mPaint);
        if (mIsShowBorder && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && mBorderColor != 0) {
            //繪制氣泡的四周邊框
            canvas.drawPath(mSrcPath, mBorderPaint);
        }
        mPaint.setXfermode(null);
        canvas.restoreToCount(saveCount);
    }

核心代碼里和上面代碼都一致,只是移動到dispatchDraw里特姐,這里 canvas.drawPath(mSrcPath, mBorderPaint)晶丘,是做一些布局邊框的特殊UI需求,布局四周的氣泡形狀的邊框唐含。


image.png

3.兼容性問題

  • 9.0手機直接在原有canvas上用XFermode混合圖層繪制氣泡路徑path浅浮,DST_IN沒起作用,導致氣泡布局不生效捷枯,上面已經(jīng)分析過具體的解決方式了滚秩。
  • 文本類型氣泡,當文本長度超出一個屏幕時淮捆,列表滑動過程中會閃動郁油,而且在滑動停止到部分臨界滑動位置,文本內(nèi)容會消失攀痊。這種情況下桐腌,如果需要顯示長文本氣泡,只能采用clippath方式做文本氣泡效果蚕苇,
  • clipPath有個很大的缺陷就是裁剪布局會在邊緣處有鋸齒感哩掺,無法解決。即使
    canvas.setDrawFilter(mPaintFlagsDrawFilter);設置也無效涩笤,paint設置抗鋸齒也沒用嚼吞。唯一的解決方案就是XFermode實現(xiàn)氣泡。

附錄

關注下面幾個view即可


image.png

源碼下載地址

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蹬碧,一起剝皮案震驚了整個濱河市舱禽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌恩沽,老刑警劉巖誊稚,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡里伯,警方通過查閱死者的電腦和手機城瞎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疾瓮,“玉大人脖镀,你說我怎么就攤上這事±堑纾” “怎么了蜒灰?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長肩碟。 經(jīng)常有香客問我强窖,道長,這世上最難降的妖魔是什么削祈? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任翅溺,我火速辦了婚禮,結果婚禮上髓抑,老公的妹妹穿的比我還像新娘未巫。我一直安慰自己,他們只是感情好启昧,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著劈伴,像睡著了一般密末。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上跛璧,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天严里,我揣著相機與錄音,去河邊找鬼追城。 笑死刹碾,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的座柱。 我是一名探鬼主播迷帜,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼色洞!你這毒婦竟也來了戏锹?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤火诸,失蹤者是張志新(化名)和其女友劉穎锦针,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡奈搜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年悉盆,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片馋吗。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡焕盟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出耗美,到底是詐尸還是另有隱情京髓,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布商架,位于F島的核電站堰怨,受9級特大地震影響,放射性物質發(fā)生泄漏蛇摸。R本人自食惡果不足惜备图,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赶袄。 院中可真熱鬧揽涮,春花似錦、人聲如沸饿肺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽敬辣。三九已至雪标,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間溉跃,已是汗流浹背村刨。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留撰茎,地道東北人嵌牺。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像龄糊,于是被迫代替她去往敵國和親逆粹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 一款view圓角自定義炫惩,氣泡自定義 直接圓角四個角分別設置隱藏和顯示枯饿,氣泡箭頭支持各種方向顯示 git鏈接:htt...
    gleeeli閱讀 418評論 1 0
  • 【Android 自定義View之繪圖】 基礎圖形的繪制 一、Paint與Canvas 繪圖需要兩個工具诡必,筆和紙奢方。...
    Rtia閱讀 11,669評論 5 34
  • 男生,那鹊奖,不叫喜歡苛聘。 高二到現(xiàn)在,2009-2017年忠聚,從懵懵懂懂的高中生到現(xiàn)在在職場上的一只小魚蝦设哗。生活只剩下上...
    脫兔醬閱讀 444評論 0 0
  • 《六一三行情詩》 痛苦瞬間過后 力量源泉涌現(xiàn) 為了未來為了現(xiàn)在
    向昕閱讀 253評論 0 0
  • 我從來不在QQ上發(fā)表說說,也從來不在微信里曬幸噶襟埃或者苦難网梢,我一直認為冷暖自知就好,我一直以為我可以享受孤獨赂毯。今天終...
    翠敏AA閱讀 574評論 0 0