自定義View之仿慕課學院水波紋進度框

GertrudeAvi 8年前發布 | 13K 次閱讀 Bitmap Android開發 移動開發

場景

最近重新學了下自定義View打算仿造一下慕課學院的下拉刷新的水波紋進度框。先上效果圖:

change.gif

實現思路

1.加入圖片,并根據控件大小處理圖片大小

2.在臨時畫布上繪畫圖片圖層和水波紋圖層,并合并成圖片。

3.在畫布上繪制合成的圖片并調用invalidate();方法去重新計算繪制水波紋圖層;

首先讓我們的控件去繼承View,定義一些常量和自定義View的初始化:

/**
 * Y方向上的每次增長值
 */
private int increateHeight;
/**
 * X方向上的每次增長值
 */
private final int INCREATE_WIDTH = 0x00000005;
/**
 * 畫筆
 */
private Paint mPaint;
/**
 * 臨時畫布
 */
private Canvas mTempCanvas;
/**
 * 貝塞爾曲線路徑
 */
private Path mBezierPath;

/**
 * 當前波紋的y值
 */
private float mWaveY;
/**
 * 貝塞爾曲線控制點距離原點x的增量
 */
private float mBezierDiffX;
/**
 * 水波紋的X左邊是否在增長
 */
private boolean mIsXDiffIncrease = true;
/**
 * 水波紋最低控制點y
 */
private float mWaveLowestY;
/**
 * 來源圖片
 */
private Bitmap mOriginalBitmap;
/**
 * 來源圖片的寬度
 */
private int mOriginalBitmapWidth;
/**
 * 來源圖片的高度
 */
private int mOriginalBitmapHeight;
/**
 * 臨時圖片
 */
private Bitmap mTempBitmap;
/**
 * 組合圖形
 */
private Bitmap mCombinedBitmap;

/**
 * 是否測量過
 */
private boolean mIsMeasured = false;
/**
 * 停止重繪
 */
private boolean mStopInvalidate = false;

關于圖片的大小,這里我希望在MeasureSpec.AT_MOST的時候讓控件保持和圖片大小一致,在MeasureSpec.EXACTLY模式下讓圖片大小跟隨控件大小而改變,兩種模式下都需考慮padding情況。

先寫一個處理圖片縮放的方法:

/**
 * 按比例縮放圖片
 *
 * @param origin      原圖
 * @param widthRatio  width縮放比例
 * @param heightRatio heigt縮放比例
 * @return 新的bitmap
 */
private Bitmap scaleBitmap(Bitmap origin, float widthRatio, float heightRatio) {
    int width = origin.getWidth();
    int height = origin.getHeight();
    Matrix matrix = new Matrix();
    matrix.preScale(widthRatio, heightRatio);
    Bitmap newBitmap = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
    if (newBitmap.equals(origin)) {
        return newBitmap;
    }
    origin.recycle();
    origin = null;
    return newBitmap;
}

在View的onMeasure()方法中,根據測量模式的不同分別處理圖片,而處理圖片的步驟只需要執行一次,為避免onMeasure()方法多次調用而造成資源浪費,引入一個flag變量mIsMeasured來規避這個問題。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (null == mTempBitmap) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        return;
    }
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    float widthRatio = 1f, heightRatio = 1f;
    if (MeasureSpec.AT_MOST == widthMode) {
        widthSize = mTempBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
    }
    if (MeasureSpec.AT_MOST == heightMode) {
        heightSize = mTempBitmap.getHeight() + getPaddingLeft() + getPaddingRight();
    }
    //只在首次繪制的時候進行onDraw()操作前的初始化
    if (!mIsMeasured) {
        if (MeasureSpec.EXACTLY == widthMode) {
            widthRatio = (float) (widthSize - getPaddingLeft() - getPaddingRight()) / mTempBitmap.getWidth();
        }
        if (MeasureSpec.EXACTLY == widthMode) {
            heightRatio = (float) (heightSize - getPaddingTop() - getPaddingBottom()) / mTempBitmap.getHeight();
        }
        //初始化onDrawa()需要的參數,后續會介紹
        initDraw(mTempBitmap, widthRatio, heightRatio);
    }
    setMeasuredDimension(widthSize, heightSize);
}

上述代碼中在2個測量模式下都對padding參數進行了計算,而initDraw()方法主要是對繪畫的參數做初始化。

/**
 * 初始化Draw所需數據
 *
 * @param tempBitmap
 * @param widthRatio
 * @param heightRatio
 */
private void initDraw(Bitmap tempBitmap, float widthRatio, float heightRatio) {
    mOriginalBitmap = scaleBitmap(tempBitmap, widthRatio, heightRatio);
    initData();
    if (null == mPaint)
        initPaint();
    initCanvas();
    mIsMeasured = true;
}

/**
 * 初始化繪畫曲線和左邊所需的一些變量值
 */
private void initData() {
    mOriginalBitmapWidth = mOriginalBitmap.getWidth();
    mOriginalBitmapHeight = mOriginalBitmap.getHeight();
    mWaveY = mOriginalBitmapHeight;
    mBezierDiffX = INCREATE_WIDTH;
    mWaveLowestY = 1.4f * mOriginalBitmapHeight;
    increateHeight = mOriginalBitmapHeight / 100;
}

/**
 * 初始化畫筆
 */
private void initPaint() {
    mPaint = new Paint();
    mBezierPath = new Path();
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Paint.Style.FILL);
}
/**
 * 初始化畫布講2個圖層繪畫至mCombinedBitmap
 */
private void initCanvas() {
    mTempCanvas = new Canvas();
    //根據原圖縮放處理結果創建一個等大的臨時畫布
    mCombinedBitmap = Bitmap.createBitmap(mOriginalBitmapWidth + getPaddingLeft() + getPaddingRight(),
            mOriginalBitmapHeight + getPaddingTop() + getPaddingBottom(), Bitmap.Config.ARGB_8888);
    //將臨時畫布上的繪畫畫在mCombinedBitmap上
    mTempCanvas.setBitmap(mCombinedBitmap);
}

這個初始化的操作分為3部分分別對應initPaint()、initData()、initCanvas()三個函數。

initData()主要是用于后續繪制水波紋圖層時候的坐標點計算。

initPaint()就是對畫筆的初始化,這個比較容易理解。

initCanvas()中根據處理后的圖片大小創建一個等大的臨時畫布,并繪畫集到mCombinedBitmap(合成的最終Bitmap)中。

接下來需要繪畫縮放后的原圖和繪畫水波紋圖層。

/**
 * 合成bitmap
 */
private void combinedBitMap() {
    mCombinedBitmap.eraseColor(Color.parseColor("#00ffffff"));
    mTempCanvas.drawBitmap(mOriginalBitmap, 0, 0, mPaint);
    //取兩層交集顯示在上層
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    //繪制水波紋圖層
    drawWaveBitmap();
}

上述的代碼繪制了圖片圖層,在繪制水波紋的圖層時設置了

mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

該模式只取2個圖層的交集,所以水波紋圖層只會顯示在圖片圖層的非空白處,就會做出水波紋在圖片內部的視覺感覺。

src_in.png

下一步就是繪制水波紋圖層。把水波紋圖層分為如下圖所示的“水紋區域”和“靜水區域”2部分。如下圖;

水面圖層分布.png

繪制水波紋圖層主要在于繪制曲線。可以結合下面的圖片便于理解。

曲線繪制過程.png

/**
 * 計算path,繪畫水波紋圖層
 */
private void drawWaveBitmap() {
    mBezierPath.reset();
    if (mIsXDiffIncrease) {
        mBezierDiffX += INCREATE_WIDTH;
    } else {
        mBezierDiffX -= INCREATE_WIDTH;
    }
    checkIncrease(mBezierDiffX);
    if (mWaveY >= 0) {
        mWaveY -= increateHeight;
        mWaveLowestY -= increateHeight;
    } else {
        //還原坐標
        mWaveY = mOriginalBitmapHeight;
        mWaveLowestY = 1.2f * mOriginalBitmapHeight;
    }
    //曲線路徑
    mBezierPath.moveTo(0, mWaveY);
    mBezierPath.cubicTo(
            mBezierDiffX, mWaveY - (mWaveLowestY - mWaveY),
            mBezierDiffX + mOriginalBitmapWidth / 2, mWaveLowestY,
            mOriginalBitmapWidth, mWaveY);
    //豎直線
    mBezierPath.lineTo(mOriginalBitmapWidth, mOriginalBitmapHeight);
    //橫直線
    mBezierPath.lineTo(0, mOriginalBitmapHeight);
    mBezierPath.close();
    mTempCanvas.drawPath(mBezierPath, mPaint);
    mPaint.setXfermode(null);
}

在曲線繪制過程.png中,取A、B、C、D四點作為曲線的繪制參考點。A、D兩點坐標比較好確認。A點X坐標恒等于0,B點的X坐標值就為圖片的寬度mOriginalBitmapWidth,兩點的Y坐標的值都是靜水區域的上邊緣線的Y值mWaveY。所以A、B坐標分別(0, mWaveY)和(mOriginalBitmapWidth, mWaveY)。

B、C兩點的坐標沒有固定的計算方法,這里介紹下我的計算方法:

定義C點的Y值為mWaveLowestY,mWaveLowestY和mWaveY按照相同的增長數值變化,這樣就讓C點距離AD線段的距離就不變,為了計算方便也讓B點到AD線段的距離等于這個數值。至于X坐標值這里假定讓B、C兩點分別在(10,1/2 AD),(10+1/2 AD,AD)區間內變化。

private void checkIncrease(float mBezierDiffX) {
    if (mIsXDiffIncrease) {
        mIsXDiffIncrease = mBezierDiffX > 0.5 * mOriginalBitmapWidth ? !mIsXDiffIncrease : mIsXDiffIncrease;
    } else {
        mIsXDiffIncrease = mBezierDiffX < 10 ? !mIsXDiffIncrease : mIsXDiffIncrease;
    }
}



 if (mIsXDiffIncrease) {
        //INCREATE_WIDTH是每次增漲的固定值
        mBezierDiffX += INCREATE_WIDTH;
    } else {
        mBezierDiffX -= INCREATE_WIDTH;
    }

每次重新draw的時候,mWaveY的值會變化,這樣曲線就可以隨著mWaveY而上下浮動,而曲線上的B、C兩點的X坐標發生變化,就能實現自身的水紋波動。畫完曲線后在D點沿豎直方向畫一條直線到最底部,再畫一條橫直線到最左部,設置path.close()便能形成一個閉環。填充效果就如上圖曲線繪制過程.png中的填充圖所示。這樣水波紋圖層就完成了。單獨效果圖如下:

wave.gif

最后畫在Canvas上并設置invalidate();就OK了。View之后會重新draw。

@Override
protected void onDraw(Canvas canvas) {
    if (mCombinedBitmap == null) {
        return;
    }
    combinedBitMap();
    //從左上角開始繪圖(需要計算padding值)
    canvas.drawBitmap(mCombinedBitmap, getPaddingLeft(), getPaddingTop(), null);
    if (!mStopInvalidate)
        //重繪
        invalidate();
}

mStopInvalidate是停止重繪的flag,后續設置自定義屬性會用到。

設置自定義屬性

自定義屬性這里實現了設置來源圖片,設置水波紋顏色以及停止水波紋的方法。

/**
 * 設置原始圖片資源
 *
 * @param resId
 */
public void setOriginalImage(@DrawableRes int resId) {
    mTempBitmap = BitmapFactory.decodeResource(getResources(), resId);
    mIsMeasured = false;
    requestLayout();
}

/**
 * 設置最終生成圖片的填充顏色資源
 *
 * @param color
 */
public void setWaveColor(@ColorInt int color) {
    if (null == mPaint)
        initPaint();
    mPaint.setColor(color);
}

/**
 * 停止/開啟 重繪
 *
 * @param mStopInvalidate
 */
public void setmStopInvalidate(boolean mStopInvalidate) {
    this.mStopInvalidate = mStopInvalidate;
    if (!mStopInvalidate)
        invalidate();
}

最終效果

演示.gif

 

 

來自:http://www.jianshu.com/p/0f88c34cce8f

 

 本文由用戶 GertrudeAvi 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!