Android自定義View實(shí)現(xiàn)橫向的雙水波紋進(jìn)度條
先看效果圖:

網(wǎng)上垂直的水波紋進(jìn)度條很多,但橫向的很少,將垂直的水波紋改為水平的還遇到了些麻煩,現(xiàn)在完善后發(fā)布出來,希望遇到的人少躺點(diǎn)坑。
思路分析
整體效果可分為三個(gè),繪制圓角背景和圓角矩形,繪制第一條和第二條水波浪,根據(jù)自定義進(jìn)度變化效果。
功能實(shí)現(xiàn)
1、繪制圓角背景和圓角矩形邊框
圓角矩形邊框:
private RectF rectBorder;if (rectBorder == null) {rectBorder = new RectF(0.5f * dp1, 0.5f * dp1, waveActualSizeWidth - 0.5f * dp1, waveActualSizeHeight - 0.5f * dp1);}canvas.drawRoundRect(rectBorder, dp27, dp27, borderPaint);
我們創(chuàng)建一個(gè)新的畫布,然后在畫布里畫上圓角矩形背景和第一條和第二條水波浪:
//這里用到了緩存 根據(jù)參數(shù)創(chuàng)建新位圖if (circleBitmap == null) {circleBitmap = Bitmap.createBitmap(waveActualSizeWidth, waveActualSizeHeight, Bitmap.Config.ARGB_8888);}//以該bitmap為底創(chuàng)建一塊畫布if (bitmapCanvas == null) {bitmapCanvas = new Canvas(circleBitmap);}// 圓角矩形背景,為了能讓波浪填充完整個(gè)圓形背景if (rectBg == null) {rectBg = new RectF(0, 0, waveActualSizeWidth, waveActualSizeHeight);}bitmapCanvas.drawRoundRect(rectBg, dp27, dp27, backgroundPaint);//裁剪圖片canvas.drawBitmap(circleBitmap, 0, 0, null);
2、通過貝塞爾曲線實(shí)現(xiàn)雙水波
1)實(shí)現(xiàn)第一條水波
/*** 繪制波浪線*/private Path canvasWavePath() {//要先清掉路線wavePath.reset();//起始點(diǎn)移至(0,0) p0 -p1 的高度隨著進(jìn)度的變化而變化wavePath.moveTo((currentPercent) * waveActualSizeWidth, -moveDistance);//最多能繪制多少個(gè)波浪//其實(shí)也可以用 i < getWidth() ;i+=waveLength來判斷 這個(gè)沒那么完美//繪制p0 - p1 繪制波浪線 這里有一段是超出View的,在View右邊距的右邊 所以是* 2for (int i = 0; i < waveNumber * 2; i++) {wavePath.rQuadTo(waveHeight, waveLength / 2, 0, waveLength);wavePath.rQuadTo(-waveHeight, waveLength / 2, 0, waveLength);}//連接p1 - p2wavePath.lineTo(0, waveActualSizeHeight);//連接p2 - p0wavePath.lineTo(0, 0);//封閉起來填充wavePath.close();return wavePath;}
moveDistance為水波垂直方向移動的距離。
waveLength為水波長度,一個(gè)上弧加一個(gè)下弧為一個(gè)波長。
path的起始點(diǎn)為(0,0)可根據(jù)進(jìn)度動態(tài)改變,然后循環(huán)畫曲線,長度是有幾個(gè)波浪就是多長,然后連接到view高度的位置,最后到(0,0),形成一個(gè)封閉的區(qū)域,這樣就實(shí)現(xiàn)了一個(gè)填充的水波效果。
2)繪制第二條水波,第二條水波和第一條類似,只是起始點(diǎn)變了:
/*** 繪制第二層波浪*/private Path canvasSecondPath() {secondWavePath.reset();//初始點(diǎn)移動到下方secondWavePath.moveTo((currentPercent) * waveActualSizeWidth, waveActualSizeHeight + moveDistance);for (int i = 0; i < waveNumber * 2; i++) {secondWavePath.rQuadTo(waveHeight, -waveLength / 2, 0, -waveLength);secondWavePath.rQuadTo(-waveHeight, -waveLength / 2, 0, -waveLength);}secondWavePath.lineTo(0, 0);secondWavePath.lineTo(0, waveActualSizeHeight);secondWavePath.close();return secondWavePath;}
3、設(shè)置動畫使進(jìn)度和水波紋變化
/*** 設(shè)置進(jìn)度** @param currentProgress 進(jìn)度* @param duration 達(dá)到進(jìn)度需要的時(shí)間*/public void setProgress(int currentProgress, long duration, AnimatorListenerAdapter listenerAdapter) {float percent = currentProgress * 1f / maxProgress;this.currentProgress = currentProgress;//從0開始變化currentPercent = 0;moveDistance = 0;mProgressAnimator = ValueAnimator.ofFloat(0, percent);//設(shè)置動畫時(shí)間mProgressAnimator.setDuration(duration);//讓動畫勻速播放,避免出現(xiàn)波浪平移停頓的現(xiàn)象mProgressAnimator.setInterpolator(new LinearInterpolator());mProgressAnimator.addUpdateListener(listener);mProgressAnimator.addListener(listenerAdapter);mProgressAnimator.start();// 波浪線startWaveAnimal();}/*** 波浪動畫*/private void startWaveAnimal() {//動畫實(shí)例化if (waveProgressAnimator == null) {waveProgressAnimator = new WaveProgressAnimal();//設(shè)置動畫時(shí)間waveProgressAnimator.setDuration(2000);//設(shè)置循環(huán)播放waveProgressAnimator.setRepeatCount(Animation.INFINITE);//讓動畫勻速播放,避免出現(xiàn)波浪平移停頓的現(xiàn)象waveProgressAnimator.setInterpolator(new LinearInterpolator());//當(dāng)前視圖開啟動畫this.startAnimation(waveProgressAnimator);}}
其中波浪動畫是通過改變moveDistance的值改變縱坐標(biāo)達(dá)到,進(jìn)度主要是通過改變百分比currentPercent改變波浪的橫坐標(biāo)達(dá)到。
完整源碼:
/*** 橫向雙水波浪進(jìn)度條** @author jingbin**/public class HorizontalWaveProgressView extends View {//繪制波浪畫筆private Paint wavePaint;//繪制波浪Pathprivate Path wavePath;//波浪的寬度private final float waveLength;//波浪的高度private final float waveHeight;//波浪組的數(shù)量 一個(gè)波浪是一低一高private int waveNumber;//自定義View的波浪寬高private int waveDefaultWidth;private int waveDefaultHeight;//測量后的View實(shí)際寬高private int waveActualSizeWidth;private int waveActualSizeHeight;//當(dāng)前進(jìn)度值占總進(jìn)度值的占比private float currentPercent;//當(dāng)前進(jìn)度值private int currentProgress;//進(jìn)度的最大值private int maxProgress;//動畫對象private WaveProgressAnimal waveProgressAnimator;private ValueAnimator mProgressAnimator;private ValueAnimator mEndAnimator;//波浪平移距離private float moveDistance = 0;//圓形背景畫筆private Paint backgroundPaint;// 邊框private Paint borderPaint;//bitmapprivate Bitmap circleBitmap;//bitmap畫布private Canvas bitmapCanvas;//波浪顏色private final int wave_color;//圓形背景進(jìn)度框顏色private final int backgroundColor;//進(jìn)度條顯示值監(jiān)聽接口private UpdateTextListener updateTextListener;//是否繪制雙波浪線private boolean isShowSecondWave;//第二層波浪的顏色private final int secondWaveColor;//邊框色private final int borderColor;//第二層波浪的畫筆private Paint secondWavePaint;private Path secondWavePath;private int dp1;// 圓角角度private int dp27;public HorizontalWaveProgressView(Context context) {this(context, null);}public HorizontalWaveProgressView(Context context, @Nullable AttributeSet attrs) {this(context, attrs, 0);}public HorizontalWaveProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);//獲取attrs文件下配置屬性TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.HorizontalWaveProgressView);//獲取波浪寬度 第二個(gè)參數(shù),如果xml設(shè)置這個(gè)屬性,則會取設(shè)置的默認(rèn)值 也就是說xml沒有指定wave_length這個(gè)屬性,就會取Density.dip2px(context,25)waveLength = typedArray.getDimension(R.styleable.HorizontalWaveProgressView_wave_length, DensityUtil.dip2px(context, 25));//獲取波浪高度waveHeight = typedArray.getDimension(R.styleable.HorizontalWaveProgressView_wave_height, DensityUtil.dip2px(context, 5));//獲取波浪顏色wave_color = typedArray.getColor(R.styleable.HorizontalWaveProgressView_wave_color, Color.parseColor("#B76EFF"));//圓形背景顏色backgroundColor = typedArray.getColor(R.styleable.HorizontalWaveProgressView_wave_background_color, Color.WHITE);//當(dāng)前進(jìn)度currentProgress = typedArray.getInteger(R.styleable.HorizontalWaveProgressView_currentProgress, 0);//最大進(jìn)度maxProgress = typedArray.getInteger(R.styleable.HorizontalWaveProgressView_maxProgress, 100);//是否顯示第二層波浪isShowSecondWave = typedArray.getBoolean(R.styleable.HorizontalWaveProgressView_second_show, false);//第二層波浪的顏色secondWaveColor = typedArray.getColor(R.styleable.HorizontalWaveProgressView_second_color, Color.parseColor("#DEBCFF"));//邊框色borderColor = typedArray.getColor(R.styleable.HorizontalWaveProgressView_border_color, Color.parseColor("#DEBCFF"));//記得把TypedArray回收//程序在運(yùn)行時(shí)維護(hù)了一個(gè) TypedArray的池,程序調(diào)用時(shí),會向該池中請求一個(gè)實(shí)例,用完之后,調(diào)用 recycle() 方法來釋放該實(shí)例,從而使其可被其他模塊復(fù)用。//那為什么要使用這種模式呢?答案也很簡單,TypedArray的使用場景之一,就是上述的自定義View,會隨著 Activity的每一次Create而Create,//因此,需要系統(tǒng)頻繁的創(chuàng)建array,對內(nèi)存和性能是一個(gè)不小的開銷,如果不使用池模式,每次都讓GC來回收,很可能就會造成OutOfMemory。//這就是使用池+單例模式的原因,這也就是為什么官方文檔一再的強(qiáng)調(diào):使用完之后一定 recycle,recycle,recycletypedArray.recycle();init(context);}/*** 初始化一些畫筆路徑配置*/private void init(Context context) {//設(shè)置自定義View的寬高waveDefaultWidth = DensityUtil.dip2px(context, 152);waveDefaultHeight = DensityUtil.dip2px(context, 40);dp1 = DensityUtil.dip2px(getContext(), 1);dp27 = DensityUtil.dip2px(getContext(), 27);wavePath = new Path();wavePaint = new Paint();//設(shè)置畫筆為取交集模式wavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));//設(shè)置波浪顏色wavePaint.setColor(wave_color);//設(shè)置抗鋸齒wavePaint.setAntiAlias(true);//矩形背景backgroundPaint = new Paint();backgroundPaint.setColor(backgroundColor);backgroundPaint.setAntiAlias(true);//邊框borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);borderPaint.setColor(borderColor);borderPaint.setAntiAlias(true);borderPaint.setStrokeWidth(dp1);borderPaint.setStyle(Paint.Style.STROKE);if (isShowSecondWave) {//是否繪制雙波浪線secondWavePath = new Path();//初始化第二層波浪畫筆secondWavePaint = new Paint();secondWavePaint.setColor(secondWaveColor);secondWavePaint.setAntiAlias(true);//因?yàn)橐采w在第一層波浪上,且要讓半透明生效,所以選SRC_ATOP模式secondWavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));}//占比一開始設(shè)置為0currentPercent = currentProgress * 1f / maxProgress;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);//這里用到了緩存 根據(jù)參數(shù)創(chuàng)建新位圖circleBitmap = Bitmap.createBitmap(waveActualSizeWidth, waveActualSizeHeight, Bitmap.Config.ARGB_8888);//以該bitmap為底創(chuàng)建一塊畫布bitmapCanvas = new Canvas(circleBitmap);// 繪制背景,為了能讓波浪填充完整個(gè)圓形背景RectF rectBg = new RectF(0, 0, waveActualSizeWidth, waveActualSizeHeight);bitmapCanvas.drawRoundRect(rectBg, dp27, dp27, backgroundPaint);if (isShowSecondWave) {//繪制第二層波浪bitmapCanvas.drawPath(canvasSecondPath(), secondWavePaint);}//繪制波浪形bitmapCanvas.drawPath(canvasWavePath(), wavePaint);//裁剪圖片canvas.drawBitmap(circleBitmap, 0, 0, null);// 繪制邊框RectF rectBorder = new RectF(0.5f * dp1, 0.5f * dp1, waveActualSizeWidth - 0.5f * dp1, waveActualSizeHeight - 0.5f * dp1);canvas.drawRoundRect(rectBorder, dp27, dp27, borderPaint);}/*** 繪制波浪線*/private Path canvasWavePath() {//要先清掉路線wavePath.reset();//起始點(diǎn)移至(0,0) p0 -p1 的高度隨著進(jìn)度的變化而變化wavePath.moveTo((currentPercent) * waveActualSizeWidth, -moveDistance);// wavePath.moveTo(-moveDistance,(1-currentPercent) * waveActualSize);//最多能繪制多少個(gè)波浪//其實(shí)也可以用 i < getWidth() ;i+=waveLength來判斷 這個(gè)沒那么完美//繪制p0 - p1 繪制波浪線 這里有一段是超出View的,在View右邊距的右邊 所以是* 2for (int i = 0; i < waveNumber * 2; i++) {wavePath.rQuadTo(waveHeight, waveLength / 2, 0, waveLength);wavePath.rQuadTo(-waveHeight, waveLength / 2, 0, waveLength);}//連接p1 - p2wavePath.lineTo(waveActualSizeWidth, waveActualSizeHeight);//連接p2 - p3wavePath.lineTo(0, waveActualSizeHeight);//連接p3 - p0 p3-p0d的高度隨著進(jìn)度變化而變化wavePath.lineTo(0, 0);//封閉起來填充wavePath.close();return wavePath;}/*** 繪制第二層波浪方法*/private Path canvasSecondPath() {float secondWaveHeight = waveHeight;secondWavePath.reset();//移動到右上方,也就是p1點(diǎn)secondWavePath.moveTo((currentPercent) * waveActualSizeWidth, waveActualSizeHeight + moveDistance);//p1 - p0for (int i = 0; i < waveNumber * 2; i++) {secondWavePath.rQuadTo(secondWaveHeight, -waveLength / 2, 0, -waveLength);secondWavePath.rQuadTo(-secondWaveHeight, -waveLength / 2, 0, -waveLength);}//p3-p0的高度隨著進(jìn)度變化而變化secondWavePath.lineTo(0, 0);//連接p3 - p2secondWavePath.lineTo(0, waveActualSizeHeight);secondWavePath.lineTo(waveActualSizeHeight, waveActualSizeWidth);//連接p2 - p1secondWavePath.lineTo(waveActualSizeWidth, waveActualSizeHeight + moveDistance);//封閉起來填充secondWavePath.close();return secondWavePath;}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int width = measureSize(waveDefaultWidth, widthMeasureSpec);int height = measureSize(waveDefaultHeight, heightMeasureSpec);//把View改為正方形setMeasuredDimension(width, height);//waveActualSize是實(shí)際的寬高waveActualSizeWidth = width;waveActualSizeHeight = height;//Math.ceil(a)返回求不小于a的最小整數(shù)// 舉個(gè)例子:// Math.ceil(125.9)=126.0// Math.ceil(0.4873)=1.0// Math.ceil(-0.65)=-0.0//這里是調(diào)整波浪數(shù)量 就是View中能容下幾個(gè)波浪 用到ceil就是一定讓View完全能被波浪占滿 為循環(huán)繪制做準(zhǔn)備 分母越小就約精準(zhǔn)waveNumber = (int) Math.ceil(Double.parseDouble(String.valueOf(waveActualSizeHeight / waveLength / 2)));}/*** 返回指定的值** @param defaultSize 默認(rèn)的值* @param measureSpec 模式*/private int measureSize(int defaultSize, int measureSpec) {int result = defaultSize;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);//View.MeasureSpec.EXACTLY:如果是match_parent 或者設(shè)置定值就//View.MeasureSpec.AT_MOST:wrap_contentif (specMode == MeasureSpec.EXACTLY) {result = specSize;} else if (specMode == MeasureSpec.AT_MOST) {result = Math.min(result, specSize);}return result;}//新建一個(gè)動畫類public class WaveProgressAnimal extends Animation {//在繪制動畫的過程中會反復(fù)的調(diào)用applyTransformation函數(shù),// 每次調(diào)用參數(shù)interpolatedTime值都會變化,該參數(shù)從0漸 變?yōu)?,當(dāng)該參數(shù)為1時(shí)表明動畫結(jié)束@Overrideprotected void applyTransformation(float interpolatedTime, Transformation t) {super.applyTransformation(interpolatedTime, t);//左邊的距離moveDistance = interpolatedTime * waveNumber * waveLength * 2;//重新繪制invalidate();}}/*** 直接結(jié)束** @param duration 結(jié)束時(shí)間*/public void setProgressEnd(long duration, AnimatorListenerAdapter listenerAdapter) {// 如果是100會不滿,因?yàn)樵诓▌?/span>if (currentProgress == maxProgress) {// 到底了就從頭開始currentPercent = 0;}mEndAnimator = ValueAnimator.ofFloat(currentPercent, 1.1f);mEndAnimator.setInterpolator(new DecelerateInterpolator());mEndAnimator.setDuration(duration);mEndAnimator.addUpdateListener(listener);mEndAnimator.addListener(listenerAdapter);mEndAnimator.start();// 波浪線startWaveAnimal();}/*** 設(shè)置進(jìn)度** @param currentProgress 進(jìn)度* @param duration 達(dá)到進(jìn)度需要的時(shí)間*/public void setProgress(int currentProgress, long duration, AnimatorListenerAdapter listenerAdapter) {float percent = currentProgress * 1f / maxProgress;this.currentProgress = currentProgress;//從0開始變化currentPercent = 0;moveDistance = 0;mProgressAnimator = ValueAnimator.ofFloat(0, percent);//設(shè)置動畫時(shí)間mProgressAnimator.setDuration(duration);//讓動畫勻速播放,避免出現(xiàn)波浪平移停頓的現(xiàn)象mProgressAnimator.setInterpolator(new LinearInterpolator());mProgressAnimator.addUpdateListener(listener);mProgressAnimator.addListener(listenerAdapter);mProgressAnimator.start();// 波浪線startWaveAnimal();}/*** 波浪動畫*/private void startWaveAnimal() {//動畫實(shí)例化if (waveProgressAnimator == null) {waveProgressAnimator = new WaveProgressAnimal();//設(shè)置動畫時(shí)間waveProgressAnimator.setDuration(2000);//設(shè)置循環(huán)播放waveProgressAnimator.setRepeatCount(Animation.INFINITE);//讓動畫勻速播放,避免出現(xiàn)波浪平移停頓的現(xiàn)象waveProgressAnimator.setInterpolator(new LinearInterpolator());//當(dāng)前視圖開啟動畫this.startAnimation(waveProgressAnimator);}}/*** 進(jìn)度的監(jiān)聽*/ValueAnimator.AnimatorUpdateListener listener = new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {// 當(dāng)前進(jìn)度百分比,[0,1]currentPercent = (float) animation.getAnimatedValue();//這里直接根據(jù)進(jìn)度值顯示if (updateTextListener != null) {updateTextListener.updateText(currentPercent, maxProgress);}}};public interface UpdateTextListener {/*** 提供接口 給外部修改數(shù)值樣式 等** @param currentPercent 當(dāng)前進(jìn)度百分比* @param maxProgress 進(jìn)度條的最大數(shù)值*/void updateText(float currentPercent, float maxProgress);}/*** 設(shè)置監(jiān)聽*/public void setUpdateTextListener(UpdateTextListener updateTextListener) {this.updateTextListener = updateTextListener;}/*** 停止動畫,銷毀對象*/public void stopAnimal() {if (waveProgressAnimator != null) {waveProgressAnimator.cancel();}if (mProgressAnimator != null && mProgressAnimator.isStarted()) {mProgressAnimator.removeAllListeners();mProgressAnimator.cancel();}if (mEndAnimator != null && mEndAnimator.isStarted()) {mEndAnimator.removeAllListeners();mEndAnimator.cancel();}}}
到這里就結(jié)束啦。
評論
圖片
表情
