PorterDuffXferMode實(shí)戰(zhàn)之WaveProgressBar 圓形水波紋進(jìn)度

一 寫在前面的話

前幾天寫了一篇Android 繪圖之PorterDuffXferMode實(shí)例講解與源碼解析,沒看過的可以先進(jìn)去看看。今天我們就來使用PorterDuffXferMode的DST_IN模式,本來想寫個(gè)圓形頭像來看看效果就行了,但是恰巧群里有朋友問到水波紋的進(jìn)度效果。于是乎去看了下實(shí)現(xiàn)方式正好也可以用到PorterDuffXferMode,立馬決定還是做個(gè)個(gè)效果更炫。先看看下面效果圖,就這么一張動(dòng)圖,懶癌犯了懶得做中間效果動(dòng)圖。

效果圖.gif

二 效果分析

看了上面效果圖,閑來分析下怎么做出這個(gè)效果。

  1. 靜態(tài)波浪效果(通過Canvas的drawPath方法畫二階貝塞爾曲線實(shí)現(xiàn))
  2. 動(dòng)態(tài)波浪效果(改變波浪高度和X方向繪制位置后定時(shí)invalidate)
  3. 圓形效果(通過PorterDuffXferMode的DST_IN和動(dòng)態(tài)波浪效果組合)
  4. 進(jìn)度文字(最終效果上繪制出文字)

三 波浪效果

  • 靜態(tài)波浪效果實(shí)現(xiàn)
    首先我們通過Canvas的drawPath方法畫二階貝塞爾曲線的方法,來繪制靜態(tài)波浪效果。我們先看效果,然后看代碼。代碼一般都注視得比較詳細(xì)。避免單獨(dú)再解釋代碼。來上靜態(tài)波浪效果圖:

    靜態(tài)波浪效果1.png

    先上一段xml,然后是代碼。原則上不改xml,就不再上了:

    <com.joker.widget.test.WaveProgressBar
        android:id="@+id/wave"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:layout_gravity="center_horizontal"
        android:padding="10dp"/>
//靜態(tài)波浪效果1
public class WaveProgressBar extends View {
    private Paint mWavePaint = new Paint(); //水波畫筆
    private Path mPath = new Path();  //路徑
    private int mWidth;  //控件寬
    private int mHeight; //控件高
    private int mWaveWidth = 200; //水波寬
    private int mWaveHeight = 100; //水波高
    int currentY = 200; //當(dāng)前Y值

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

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

    public WaveProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mWavePaint.setAntiAlias(true);
        mWavePaint.setColor(Color.GREEN);
    }

    //獲取控件的寬和高
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w > 0) {
            mWidth = w;
        }
        if (h > 0) {
            mHeight = h;
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mWidth <= 0 || mHeight <= 0) {
            return;
        }
        //繪制一個(gè)灰色背景 方便看效果
        canvas.drawColor(Color.GRAY);
        mPath.reset();
        //路徑起點(diǎn)
        mPath.moveTo(0, 200);
        //i+2: 一上一下2個(gè)半波為一組 count+2: 保證波浪占滿控件
        for (int i = 0, count = mWidth / mWaveWidth + 2; i < count; i += 2) {
            //上半波
            mPath.quadTo(mWaveWidth * (i + 0.5f), currentY - mWaveHeight, mWaveWidth * (i + 1), currentY);
            //下班波
            mPath.quadTo(mWaveWidth * (i + 1.5f), currentY + mWaveHeight, mWaveWidth * (i + 2), currentY);
        }
        //繪制路徑
        canvas.drawPath(mPath, mWavePaint);
    }
}

接下來我們讓波浪封閉起來,并且讓它水位升高,有總水杯里的水的感覺。還是和前面一樣,看圖看代碼:

封閉起來的靜態(tài)波浪效果.png
水位升高的的靜態(tài)波浪效果.png
//封閉起來的靜態(tài)波浪效果
public class WaveProgressBar extends View {
    private Paint mWavePaint = new Paint();
    private Path mPath = new Path();
    private int mWidth;
    private int mHeight;
    private int mWaveWidth = 200; //水波寬
    private int mWaveHeight = 100; //水波高
    int currentY = 200; //當(dāng)前Y值

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

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

    public WaveProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mWavePaint.setAntiAlias(true);
        mWavePaint.setColor(Color.GREEN);
    }

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

    @Override
    protected void onDraw(Canvas canvas) {
        if (mWidth <= 0 || mHeight <= 0) {
            return;
        }
        canvas.drawColor(Color.GRAY);
        mPath.reset();
        //路徑起點(diǎn)
        mPath.moveTo(0, 200);
        //i+2: 一上一下2個(gè)半波為一組 count+2: 保證波浪占滿控件
        for (int i = 0, count = mWidth / mWaveWidth + 2; i < count; i += 2) {
            //上半波
            mPath.quadTo(mWaveWidth * (i + 0.5f), currentY - mWaveHeight, mWaveWidth * (i + 1), currentY);
            //下班波
            mPath.quadTo(mWaveWidth * (i + 1.5f), currentY + mWaveHeight, mWaveWidth * (i + 2), currentY);
        }
        //一下兩句代碼就完成了水波的封閉效果
        mPath.lineTo(mWidth, mHeight);
        mPath.lineTo(0, mHeight);
        mPath.close();
        canvas.drawPath(mPath, mWavePaint);
    }
}

還是不得不啰嗦一下,我們這里添加了2行代碼實(shí)現(xiàn)了水波的封閉:

        mPath.lineTo(mWidth, mHeight);
        mPath.lineTo(0, mHeight);

接下來就是水位的上升了,但是這個(gè)地方就上代碼就有點(diǎn)故意拉長篇幅的嫌疑了。其實(shí)很簡(jiǎn)單我們只需要改變currentY的值,就可以實(shí)現(xiàn)水位的上升了。
這里關(guān)于Canvas路徑繪制,或者是坐標(biāo)系搞不明白的可以看我的:Android 繪圖之Canvas相關(guān)API使用一文,在里面有比較詳細(xì)的說明。

  1. 動(dòng)態(tài)波浪效果實(shí)現(xiàn)
    上面我們已經(jīng)實(shí)現(xiàn)了靜態(tài)的波浪效果了,代碼還是比較簡(jiǎn)單的,接下來我們讓波浪動(dòng)次打次動(dòng)次打次動(dòng)起來。


    動(dòng)態(tài)波浪效果.gif
  //動(dòng)態(tài)波浪效果
  public class WaveProgressBar extends View {
    private Paint mWavePaint = new Paint();
    private Path mPath = new Path();
    private int mWidth;
    private int mHeight;
    private int mWaveWidth = 200; //水波寬
    private int mWaveHeight = 100; //水波高
    int currentY = 200; //當(dāng)前Y值
    private double mRate = 0.1; //流速
    private int distance;   //距離
    private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            distance += mWaveWidth * mRate; //計(jì)算流動(dòng)總距離
            distance = distance % (mWaveWidth << 1);  //根據(jù)總距離算出當(dāng)前移動(dòng)距離
            invalidate(); //刷新重繪
            mHandler.sendEmptyMessageDelayed(0, 20); //20ms發(fā)送一個(gè)消息
            return false;
        }
    });

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

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

    public WaveProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mWavePaint.setAntiAlias(true);
        mWavePaint.setColor(Color.GREEN);
        //20ms發(fā)送一個(gè)消息
        mHandler.sendEmptyMessageDelayed(0, 20);
    }

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

    @Override
    protected void onDraw(Canvas canvas) {
        if (mWidth <= 0 || mHeight <= 0) {
            return;
        }
        canvas.drawColor(Color.GRAY);
        mPath.reset();
        //路徑起點(diǎn)
        mPath.moveTo(0, 200);
        //i+2: 一上一下2個(gè)半波為一組 count+2: 保證波浪占滿控件
        for (int i = 0, count = mWidth / mWaveWidth + 2; i < count; i += 2) {
            //上半波  減去distance 造成左移效果 
            mPath.quadTo(mWaveWidth * (i + 0.5f) - distance, currentY - mWaveHeight, mWaveWidth * (i + 1) - distance, currentY);
            //下班波
            mPath.quadTo(mWaveWidth * (i + 1.5f) - distance, currentY + mWaveHeight, mWaveWidth * (i + 2) - distance, currentY);
        }
        mPath.lineTo(mWidth, mHeight);
        mPath.lineTo(0, mHeight);
        mPath.close();
        canvas.drawPath(mPath, mWavePaint);
    }
}

看效果圖的水流還是很急的,減小速率就可以讓它流慢點(diǎn),我們先不關(guān)心這個(gè),說說實(shí)現(xiàn)思路。主要是利用Handler定時(shí)發(fā)送消息通知重繪的方式,每20ms計(jì)算出移動(dòng)距離并刷新重繪一次。在繪制的時(shí)候減去移動(dòng)距離就有了左移效果,其他的說明在代碼中注釋很詳盡了。

四 圓形效果

水波效果我們就做到這里,接下來就是利用PorterDuffXferMode的DST_IN模式來實(shí)現(xiàn)圓形效果,我們先繪制的水波后繪制的圓,所以我們需要使用DST_IN。其實(shí)繪制圓形頭像效果原理也是一樣的,至于用什么模式就跟你繪制的先后順序,還有需求有關(guān)。不了解PorterDuffXferMode可以去看看我之前的文章,本文后我會(huì)給出相關(guān)鏈接地址。


圓形效果.gif
  //圓形波浪效果
  public class WaveProgressBar extends View {
    private Paint mWavePaint = new Paint();
    private Path mPath = new Path();
    private int mWidth;
    private int mHeight;
    private int mWaveWidth = 200; //水波寬
    private int mWaveHeight = 100; //水波高
    int currentY = 200; //當(dāng)前Y值
    private double mRate = 0.01; //流速
    private int distance;   //距離
    private PorterDuffXfermode mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
    private Bitmap mCircleBitmap;   //圓形bitmap
    private RectF mBorderRectF; //邊框矩形
    private int mBorderRadius; //邊框半徑
    private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            distance += mWaveWidth * mRate; //計(jì)算流動(dòng)總距離
            distance = distance % (mWaveWidth << 1);
            invalidate();
            mHandler.sendEmptyMessageDelayed(0, 20);
            return false;
        }
    });

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

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

    public WaveProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mWavePaint.setAntiAlias(true);
        mWavePaint.setColor(Color.GREEN);
        mHandler.sendEmptyMessageDelayed(0, 20);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w > 0) {
            mWidth = w;
        }
        if (h > 0) {
            mHeight = h;
        }
        mWidth = mHeight = Math.min(mWidth, mHeight);
        //創(chuàng)建邊框矩形
        mBorderRectF = new RectF(0, 0, mWidth, mHeight);
        //邊框半徑
        mBorderRadius = (mWidth >> 1);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mWidth <= 0 || mHeight <= 0) {
            return;
        }
        canvas.drawBitmap(createWaveBitmap(), 0, 0, mWavePaint);
    }

    //創(chuàng)建水波bitmap
    private Bitmap createWaveBitmap() {
        Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        canvas.drawColor(Color.GRAY);
        mPath.reset();
        //路徑起點(diǎn)
        mPath.moveTo(0, 200);
        //i+2: 一上一下2個(gè)半波為一組 count+2: 保證波浪占滿控件
        for (int i = 0, count = mWidth / mWaveWidth + 2; i < count; i += 2) {
            //上半波  減去distance 造成左移效果
            mPath.quadTo(mWaveWidth * (i + 0.5f) - distance, currentY - mWaveHeight, mWaveWidth * (i + 1) - distance, currentY);
            //下班波
            mPath.quadTo(mWaveWidth * (i + 1.5f) - distance, currentY + mWaveHeight, mWaveWidth * (i + 2) - distance, currentY);
        }
        mPath.lineTo(mWidth, mHeight);
        mPath.lineTo(0, mHeight);
        mPath.close();
        canvas.drawPath(mPath, mWavePaint);
        if (mCircleBitmap == null) {
            //創(chuàng)建圓形的bitmap
            mCircleBitmap = createShapeBitmap();
        }
        //設(shè)置mXfermode模式
        mWavePaint.setXfermode(mXfermode);
        canvas.drawBitmap(mCircleBitmap, 0, 0, mWavePaint);
        mWavePaint.setXfermode(null);
        return bitmap;
    }

    //創(chuàng)建形狀的bitmap
    private Bitmap createShapeBitmap() {
        Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        canvas.drawCircle(mBorderRectF.centerX(), mBorderRectF.centerY(), mBorderRadius, mWavePaint);
        return bitmap;
    }
}

這里把onDraw里面的代碼抽取出來了,把onDraw的背景繪制放在了createWaveBitmap中便于觀看效果,利用PorterDuffXferMode的DST_IN模式來實(shí)現(xiàn)圓形效果。

五 繪制文字

我們已經(jīng)完成了圓形的水波紋效果,接下來就是將文字繪制在圓形的水波里面。繪制文字就比較簡(jiǎn)單了,調(diào)用Canvas的drawText方法在圓形水波紋上將文字繪制出來就O啦。


添加文字后的水波紋效果.gif
public class WaveProgressBar extends View {
    private Paint mWavePaint = new Paint();
    private Paint mTextPaint = new Paint();
    private Path mPath = new Path();
    private int mWidth;
    private int mHeight;
    private int mWaveWidth = 200; //水波寬
    private int mWaveHeight = 100; //水波高
    int currentY = 200; //當(dāng)前Y值
    private double mRate = 0.1; //流速
    private int distance;   //距離
    private PorterDuffXfermode mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
    private Bitmap mCircleBitmap;   //圓形bitmap
    private RectF mBorderRectF; //邊框矩形
    private int mBorderRadius; //邊框半徑
    private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            distance += mWaveWidth * mRate; //計(jì)算流動(dòng)總距離
            distance = distance % (mWaveWidth << 1);
            invalidate();
            mHandler.sendEmptyMessageDelayed(0, 20);
            return false;
        }
    });

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

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

    public WaveProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mWavePaint.setAntiAlias(true);
        mWavePaint.setColor(Color.GREEN);

        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setTextSize(32);
        mHandler.sendEmptyMessageDelayed(0, 20);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w > 0) {
            mWidth = w;
        }
        if (h > 0) {
            mHeight = h;
        }
        mWidth = mHeight = Math.min(mWidth, mHeight);
        //創(chuàng)建邊框矩形
        mBorderRectF = new RectF(0, 0, mWidth, mHeight);
        //邊框半徑
        mBorderRadius = (mWidth >> 1);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mWidth <= 0 || mHeight <= 0) {
            return;
        }
        canvas.drawBitmap(createWaveBitmap(), 0, 0, mWavePaint);
        canvas.drawText("80%", mBorderRectF.centerX(), mBorderRectF.centerY(), mTextPaint);
    }

    //創(chuàng)建水波bitmap
    private Bitmap createWaveBitmap() {
        Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        canvas.drawColor(Color.GRAY);
        mPath.reset();
        //路徑起點(diǎn)
        mPath.moveTo(0, 200);
        //i+2: 一上一下2個(gè)半波為一組 count+2: 保證波浪占滿控件
        for (int i = 0, count = mWidth / mWaveWidth + 2; i < count; i += 2) {
            //上半波  減去distance 造成左移效果
            mPath.quadTo(mWaveWidth * (i + 0.5f) - distance, currentY - mWaveHeight, mWaveWidth * (i + 1) - distance, currentY);
            //下班波
            mPath.quadTo(mWaveWidth * (i + 1.5f) - distance, currentY + mWaveHeight, mWaveWidth * (i + 2) - distance, currentY);
        }
        mPath.lineTo(mWidth, mHeight);
        mPath.lineTo(0, mHeight);
        mPath.close();
        canvas.drawPath(mPath, mWavePaint);
        if (mCircleBitmap == null) {
            //創(chuàng)建圓形的bitmap
            mCircleBitmap = createShapeBitmap();
        }
        //設(shè)置mXfermode模式
        mWavePaint.setXfermode(mXfermode);
        canvas.drawBitmap(mCircleBitmap, 0, 0, mWavePaint);
        mWavePaint.setXfermode(null);
        return bitmap;
    }

    //創(chuàng)建形狀的bitmap
    private Bitmap createShapeBitmap() {
        Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        canvas.drawCircle(mBorderRectF.centerX(), mBorderRectF.centerY(), mBorderRadius, mWavePaint);
        return bitmap;
    }
}

這一步?jīng)]什么可說的,新創(chuàng)建了mTextPaint 畫筆,設(shè)置了字體顏色大小等屬性,最后在原本畫好的效果上再繪制上文字就成了。可我們文章開頭的效果圖上,文字是隨著水位而上升的,其實(shí)這個(gè)很簡(jiǎn)單,改變文本繪制的Y軸位置就能達(dá)到上升效果,我們的水波紋效果到這里就完成了它的雛形。

上面的演示代碼還是比較雜亂,創(chuàng)建Bitmap都還是在onDraw中,沒有考慮控件的測(cè)量,也不支持自定義屬性等。大家可以根據(jù)上面的步驟,自己整理實(shí)現(xiàn)定義成自己想要的效果。

最后我將源碼整理了下,優(yōu)化后的源碼后續(xù)會(huì)補(bǔ)上地址,就不在文中給出了。

附:
Android 繪圖之Canvas相關(guān)API使用
Android 繪圖之PorterDuffXferMode實(shí)例講解與源碼解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容