Android自定義View實現(xiàn)水面上漲效果
實現(xiàn)效果如下:

實現(xiàn)思路:
1、如何實現(xiàn)圓中水面上漲效果:利用Paint的setXfermode屬性為PorterDuff.Mode.SRC_IN畫出進度所在的矩形與圓的交集實現(xiàn)
2、如何水波紋效果:利用貝塞爾曲線,動態(tài)改變波峰值,實現(xiàn)“隨著進度的增加,水波紋逐漸變小的效果”
話不多說,看代碼。
首先是自定義屬性值,有哪些可自定義屬性值呢?
圓的背景顏色:circle_color,進度的顏色:progress_color,進度顯示文字的顏色:text_color,進度文字的大小:text_size,還有最后一個:波紋最大高度:ripple_topheight
<declare-styleable name="WaterProgressView"> <attr name="circle_color" format="color"/><!--圓的顏色--> <attr name="progress_color" format="color"/><!--進度的顏色--> <attr name="text_color" format="color"/><!--文字的顏色--> <attr name="text_size" format="dimension"/><!--文字大小--> <attr name="ripple_topheight" format="dimension"/><!--水頁漣漪最大高度--> </declare-styleable>
下面是自定義View:WaterProgressView的部份代碼:
成員變量
public class WaterProgressView extends ProgressBar {
//默認(rèn)圓的背景色
public static final int DEFAULT_CIRCLE_COLOR = 0xff00cccc;
//默認(rèn)進度的顏色
public static final int DEFAULT_PROGRESS_COLOR = 0xff00CC66;
//默認(rèn)文字的顏色
public static final int DEFAULT_TEXT_COLOR = 0xffffffff;
//默認(rèn)文字的大小
public static final int DEFAULT_TEXT_SIZE = 18;
//默認(rèn)的波峰最高點
public static final int DEFAULT_RIPPLE_TOPHEIGHT = 10;
private Context mContext;
private Canvas mPaintCanvas;
private Bitmap mBitmap;
//畫圓的畫筆
private Paint mCirclePaint;
//畫圓的畫筆的顏色
private int mCircleColor;
//畫進度的畫筆
private Paint mProgressPaint;
//畫進度的畫筆的顏色
private int mProgressColor ;
//畫進度的path
private Path mProgressPath;
//貝塞爾曲線波峰最大值
private int mRippleTop = 10;
//進度文字的畫筆
private Paint mTextPaint;
//進度文字的顏色
private int mTextColor;
private int mTextSize = 18;
//目標(biāo)進度,也就是雙擊時處理任務(wù)的進度,會影響曲線的振幅
private int mTargetProgress = 50;
//監(jiān)聽雙擊和單擊事件
private GestureDetector mGestureDetector;
}
獲取自定義屬性值:
private void getAttrValue(AttributeSet attrs) {
TypedArray ta = mContext.obtainStyledAttributes(attrs, R.styleable.WaterProgressView);
mCircleColor = ta.getColor(R.styleable.WaterProgressView_circle_color,DEFAULT_CIRCLE_COLOR);
mProgressColor = ta.getColor(R.styleable.WaterProgressView_progress_color,DEFAULT_PROGRESS_COLOR);
mTextColor = ta.getColor(R.styleable.WaterProgressView_text_color,DEFAULT_TEXT_COLOR);
mTextSize = (int) ta.getDimension(R.styleable.WaterProgressView_text_size, DesityUtils.sp2px(mContext,DEFAULT_TEXT_SIZE));
mRippleTop = (int)ta.getDimension(R.styleable.WaterProgressView_ripple_topheight,DesityUtils.dp2px(mContext,DEFAULT_RIPPLE_TOPHEIGHT));
ta.recycle();
}
定義構(gòu)造函數(shù),注意 mProgressPaint.setXfermode
//當(dāng)new該類時調(diào)用此構(gòu)造函數(shù)
public WaterProgressView(Context context) {
this(context,null);
}
//當(dāng)xml文件中定義該自定義View時調(diào)用此構(gòu)造函數(shù)
public WaterProgressView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public WaterProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
getAttrValue(attrs);
//初始化畫筆的相關(guān)屬性
initPaint();
mProgressPath = new Path();
}
private void initPaint() {
//初始化畫圓的paint
mCirclePaint = new Paint();
mCirclePaint.setColor(mCircleColor);
mCirclePaint.setStyle(Paint.Style.FILL);
mCirclePaint.setAntiAlias(true);
mCirclePaint.setDither(true);
//初始化畫進度的paint
mProgressPaint = new Paint();
mProgressPaint.setColor(mProgressColor);
mProgressPaint.setAntiAlias(true);
mProgressPaint.setDither(true);
mProgressPaint.setStyle(Paint.Style.FILL);
//其實mProgressPaint畫的也是矩形,當(dāng)設(shè)置xfermode為PorterDuff.Mode.SRC_IN后則顯示的為圓與進度矩形的交集,則為半圓
mProgressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
//初始化畫進度文字的畫筆
mTextPaint = new Paint();
mTextPaint.setColor(mTextColor);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setAntiAlias(true);
mTextPaint.setDither(true);
mTextPaint.setTextSize(mTextSize);
}
onMeasure()方法代碼:
@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//使用時,需要明確定義該View的尺寸,即用測量模式為MeasureSpec.EXACTLY
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width,height);
//初始化Bitmap,讓所有的drawCircle,drawPath,drawText都draw在該bitmap所在的canvas上,然后再將該bitmap 畫在onDraw方法的canvas上,
//所以此bitmap的width,height需要減去left,top,right,bottom的padding
mBitmap = Bitmap.createBitmap(width-getPaddingLeft()-getPaddingRight(),height- getPaddingTop()-getPaddingBottom(), Bitmap.Config.ARGB_8888);
mPaintCanvas = new Canvas(mBitmap);
}
接下來是核心部份,onDraw中的代碼。我們先將Circle,進度條,進度文字draw到自定義canvas的bitmap上,再將此bitmap draw到onDraw方法中的canvas上。drawCircle與drawText應(yīng)該沒什么難度,關(guān)鍵點就在于畫進度條,怎么畫呢?既然有水波紋效果,有曲線,就用drawPath了。
drawPath的流程如下:

其中ratio的代碼如下,即ratio為當(dāng)前進度占總進度的百分比
float ratio = getProgress()*1.0f/getMax();
因為坐標(biāo)是從B點向下和向右正向延伸的,則A點的坐標(biāo)為(width,(1-ratio)*height),其中width為bitmap的寬,height為bitmap的高。我們先將mProgressPath.moveTo到A點,然后從A點順時針方向確定path的各個關(guān)鍵點,如圖,則代碼如下:
int rightTop = (int) ((1-ratio)*height); mProgressPath.moveTo(width,rightTop); mProgressPath.lineTo(width,height); mProgressPath.lineTo(0,height); mProgressPath.lineTo(0,rightTop);
如此mProgressPath已經(jīng)lineTo到了C點,需要在A點與C點之間形成水波紋效果,則需要在A點與C點間畫貝塞爾曲線。

我們設(shè)定波峰最高點為10,則一段波長為40,需要畫width*1.0f/40段這樣的曲線,則畫曲線的代碼如下:
int count = (int) Math.ceil(width*1.0f/(10 *4));
for(int i=0; i<count; i++) {
mProgressPath.rQuadTo(10,10,2* 10,0);
mProgressPath.rQuadTo(10,-10,2* 10,0);
}
mProgressPath.close();
mPaintCanvas.drawPath(mProgressPath,mProgressPaint);
這樣就能畫出水面上漲且有波紋效果的進度條了。但我們還要實現(xiàn)隨著水面上漲,越接近目標(biāo)進度,水面波紋應(yīng)該越來越小,則應(yīng)該把10抽出為變量定義為mRippleTop等初始時波峰最大值,然后定義top為隨著進度不斷接近目標(biāo)進度時曲線的實時波峰值 ,其中mTargetProgress為目標(biāo)progress,因為有一個目標(biāo)進度才能實現(xiàn)當(dāng)前進度不斷接近目標(biāo)進度的過程中,水面漸趨于平面的效果:
float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;
所以drawPath的代碼更新如下:
float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;
for(int i=0; i<count; i++) {
mProgressPath.rQuadTo(mRippleTop,top,2* mRippleTop,0);
mProgressPath.rQuadTo(mRippleTop,-top,2* mRippleTop,0);
}
如此就能真正實現(xiàn)水面上漲的進度條了。
但如何實現(xiàn)圖中雙擊時水面從0%上漲到目標(biāo)進度,單擊時水面在目標(biāo)進度不斷涌動的效果呢?
先說說雙擊效果的實現(xiàn):這個簡單,定義一個Handler,,當(dāng)雙擊時,handler.postDelayed(runnable,time) ,每隔一段時間progress+1,在runnable中invalidate()不斷更新進度,直到當(dāng)前progress到達mTargetProgress。
代碼如下
/**
* 實現(xiàn)雙擊動畫
*/
private void startDoubleTapAnimation() {
setProgress(0);
doubleTapHandler.postDelayed(doubleTapRunnable,60);
}
private Handler doubleTapHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
//雙擊處理線程,隔60ms發(fā)送一次數(shù)據(jù)
private Runnable doubleTapRunnable = new Runnable() {
@Override
public void run() {
if(getProgress() < mTargetProgress) {
invalidate();
setProgress(getProgress()+1);
doubleTapHandler.postDelayed(doubleTapRunnable,60);
} else {
doubleTapHandler.removeCallbacks(doubleTapRunnable);
}
}
};
雙擊效果實現(xiàn)了,那如何實現(xiàn)單擊效果呢?單擊時要求水面不斷涌動一段時間,水面波紋逐漸變小,然后水面變平。我們可以定義一個mSingleTapAnimationCount變量為水面涌動的次數(shù),然后像雙擊時的處理一樣,定義一個Handler隔一段時間發(fā)送一次更新界面的message,mSingleTapAnimationCount-- ,然后我們交替地讓初始時的波峰一次為正一次為負,則能實現(xiàn)水面涌動的效果。
核心代碼如下:
private void startSingleTapAnimation() {
isSingleTapAnimation = true;
singleTapHandler.postDelayed(singleTapRunnable,200);
}
private Handler singleTapHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
//單擊處理線程,隔200ms發(fā)送一次數(shù)據(jù)
private Runnable singleTapRunnable = new Runnable() {
@Override
public void run() {
if(mSingleTapAnimationCount > 0) {
invalidate();
mSingleTapAnimationCount--;
singleTapHandler.postDelayed(singleTapRunnable,200);
} else {
singleTapHandler.removeCallbacks(singleTapRunnable);
//是否正在進行單擊動畫
isSingleTapAnimation = false;
//重置單擊動畫運行次數(shù)為50次
mSingleTapAnimationCount = 50;
}
}
};
onDraw中的代碼作相應(yīng)的更改,因單擊與雙擊時drawPath中曲線部分的繪制邏輯不一樣,則我們定義一個變量isSingleTapAnimation 區(qū)別是正在進行單擊動畫還是在進行雙擊動畫。
更改后的代碼如下:
//畫進度
mProgressPath.reset();
//從右上邊開始draw path
int rightTop = (int) ((1-ratio)*height);
mProgressPath.moveTo(width,rightTop);
mProgressPath.lineTo(width,height);
mProgressPath.lineTo(0,height);
mProgressPath.lineTo(0,rightTop);
//畫貝塞爾曲線,形成波浪線
int count = (int) Math.ceil(width*1.0f/(mRippleTop *4));
//不是單擊animation狀態(tài)
if(!isSingleTapAnimation&&getProgress()>0) {
float top = (mTargetProgress-getProgress())*1.0f/mTargetProgress* mRippleTop;
for(int i=0; i<count; i++) {
mProgressPath.rQuadTo(mRippleTop,-top,2* mRippleTop,0);
mProgressPath.rQuadTo(mRippleTop,top,2* mRippleTop,0);
}
} else {
//單擊animation狀態(tài),為了將效果放大,將mRippleTop放大2倍
//同時偶數(shù)時曲線走向如圖所示,奇數(shù)時則曲線剛好相反
float top = (mSingleTapAnimationCount*1.0f/50)*10;
//奇偶數(shù)時曲線切換
if(mSingleTapAnimationCount%2==0) {
for(int i=0; i<count; i++) {
mProgressPath.rQuadTo(mRippleTop *2,top*2,2* mRippleTop,0);
mProgressPath.rQuadTo(mRippleTop *2,-top*2,2* mRippleTop,0);
}
} else {
for(int i=0; i<count; i++) {
mProgressPath.rQuadTo(mRippleTop *2,-top*2,2* mRippleTop,0);
mProgressPath.rQuadTo(mRippleTop *2,top*2,2* mRippleTop,0);
}
}
}
mProgressPath.close();
mPaintCanvas.drawPath(mProgressPath,mProgressPaint);
基本上重要的代碼與核心邏輯與代碼就在上面了。
注意點:
1、當(dāng)drawCircle時要考慮到padding,則circle的寬和高為getWidth與getHeight減去padding值,代碼如下:
//自定義bitmap的寬和高 int width = getWidth()-getPaddingLeft()-getPaddingRight(); int height = getHeight()-getPaddingTop()-getPaddingBottom(); //畫圓 mPaintCanvas.drawCircle(width/2,height/2,height/2,mCirclePaint);
2、當(dāng)drawText時,不是從text的height的中間開始draw的,而是從baseline開始draw的

那如何獲取baseline的height坐標(biāo)呢
Paint.FontMetrics metrics = mTextPaint.getFontMetrics(); //因為ascent在baseline之上,所以ascent為負數(shù)。descent+ascent為負數(shù),所以是減而不是加 float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2;
drawText的全部代碼如下:
//畫進度文字 String text = ((int)(ratio*100))+"%"; //獲得文字的寬度 float textWidth = mTextPaint.measureText(text); Paint.FontMetrics metrics = mTextPaint.getFontMetrics(); //descent+ascent為負數(shù),所以是減而不是加 float baseLine = height*1.0f/2 - (metrics.descent+metrics.ascent)/2; mPaintCanvas.drawText(text,width/2-textWidth/2,baseLine,mTextPaint);
3、因為要顧及到padding,記得將onDraw中的canvas translate到(getPaddingLeft(),getPaddingTop())處。
canvas.translate(getPaddingLeft(),getPaddingTop()); canvas.drawBitmap(mBitmap,0,0,null);
最后記得將自定義的bitmap draw到onDraw中的canvas上。到這兒自定義水面上漲效果的進度條于寫完了。
總結(jié)
以上就是這篇文章的全部內(nèi)容,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作帶來一定的幫助,如果有疑問大家可以留言交流。
- Android 自定義View的使用介紹
- Android自定義View設(shè)定到FrameLayout布局中實現(xiàn)多組件顯示的方法 分享
- Android自定義View實現(xiàn)廣告信息上下滾動效果
- Android自定義View實現(xiàn)帶數(shù)字的進度條實例代碼
- Android自定義View之酷炫圓環(huán)(二)
- Android自定義view制作絢麗的驗證碼
- Android自定義View之酷炫數(shù)字圓環(huán)
- Android自定義View過程解析
- Android自定義View軟鍵盤實現(xiàn)搜索
- 實例講解Android中的View類以及自定義View控件的方法
相關(guān)文章
PullToRefreshListView實現(xiàn)多條目加載上拉刷新和下拉加載
這篇文章主要為大家詳細介紹了PullToRefreshListView實現(xiàn)多條目加載上拉刷新和下拉加載,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01
Android如何實現(xiàn)設(shè)備的異顯功能詳解
這篇文章主要給大家介紹了關(guān)于Android如何實現(xiàn)設(shè)備的異顯功能的相關(guān)資料,這篇文章通過示例代碼介紹的非常詳細,對各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2022-02-02
Android?RecyclerView實現(xiàn)九宮格效果
這篇文章主要為大家詳細介紹了Android?RecyclerView實現(xiàn)九宮格效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-06-06
Android開源框架的SlidingFragment的使用示例
今天小編就為大家分享一篇關(guān)于Android開源框架的SlidingFragment的使用示例,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03

