自定義View之仿慕課學院水波紋進度框
場景
最近重新學了下自定義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