Android魔术——手把手教你实现水晶球波浪进度条
目錄
1、效果展示
2、波浪函數
3、波浪填充
1)原理分析
2)代碼實現
4、實現波浪運動效果
1)實現橫向運動
2)實現波浪消退效果
5、總結
源碼:
1、效果展示
本篇文章講解如何實現一個水晶球波浪進度條,實現后效果如下:2、波浪函數
我們來觀察其中一幀的畫面,如下可以看到在一瞬間的波浪其實是兩條不同的正弦函數曲線疊加在一起,而波浪的運動實際上這兩條正弦函數在移動。由于兩條曲線的振幅、周期和移動速率完全不同,所以產生了波浪的效果。
所以實現波浪的效果我們需要用到一個正弦函數:
a*sin(b*(x + c))+d
其中:
- a - 振幅,影響的是波浪的浪高
- b - 周期,影響的是兩個浪頭之間的距離
- c - 偏移,改變這個參數來實現曲線的移動
- d - 高度,即水平線的高度,曲線在這個高度上下波動(實際上是進度,后面會講到)
實現這個函數:
/*** 波浪的函數,用于求y值* 函數為a*sin(b*(x + c))+d* @param x x軸* @param offset 偏移* @param waveHeight 振幅* @param waveCycle 周期* @return*/ private double getWaveY(int x, int offset, int waveHeight, float waveCycle) {return waveHeight * Math.sin(waveCycle * (x + offset)) + (1 - mProgress / 100.0) * getHeight(); }?
3、波浪填充
下面就是最關鍵的部分,我們如何使用這個函數實現想要的效果?1)原理分析
在a、b、c、d確定的情況下,通過上面的函數我們只能得到一條線,如圖 (圖2) 但我們實際上想要一個填充的效果,解決辦法是我們利用這個曲線上的點與基線(x軸)上對應的點連線,如下圖 (圖3) 當這些線足夠多足夠密集的時候,就實現了填充效果。如圖 (圖4) 有了這個效果還不夠,因為我們需要的是一個圓形,這時候就需要使用遮罩來處理。為上面的圖形加上一個圓形的遮罩,遮罩設置為DST_IN,就可以得到想要的效果,如圖 (圖5) 這樣當我們有兩條不同的曲線,經過(圖5)處理后區域疊加在一起的時候,就形成了(圖1)的波浪效果。2)代碼實現
下面我們來看看具體代碼怎么處理 @Override protected void onDraw(Canvas canvas) {super.onDraw(canvas);if (getHeight() > 0 && getWidth() > 0) {//繪制邊緣Paint paint = new Paint();paint.setColor(mWaveColor);paint.setStyle(Paint.Style.STROKE);RectF edge = new RectF(0, 0, getWidth(), getHeight());canvas.drawArc(edge, 0, 360, false, paint);canvas.drawColor(Color.TRANSPARENT);int sc = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);if (isWaveMoving) {/*** 如果有波浪,則繪制兩條波浪* 波浪實際上是一條條一像素的直線組成的,線的頂端是根據正弦函數得到的*/for (int i = 0; i < getWidth(); i++) {canvas.drawLine(i, (int) getWaveY(i, mOffsetA, mWaveHeightA, mWaveACycle), i, getHeight(), mWavePaint);canvas.drawLine(i, (int) getWaveY(i, mOffsetB, mWaveHeightB, mWaveBCycle), i, getHeight(), mWavePaint);}} else {/*** 如果沒有波浪,則繪制兩次矩形* 之所以繪制兩次,是因為波浪有兩條,所以除了浪尖的部分,其他部分都是重合的,顏色較重*/float height = (1 - mProgress / 100.0f) * getHeight();canvas.drawRect(0, height, getWidth(), getHeight(), mWavePaint);canvas.drawRect(0, height, getWidth(), getHeight(), mWavePaint);}//設置遮罩效果,繪制遮罩mWavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));canvas.drawBitmap(mBallBitmap, 0, 0, mWavePaint);mWavePaint.setXfermode(null);canvas.restoreToCount(sc);} }我們在onDraw中來處理這部分的繪制。
繪制分三個部分。
(1)第一部分繪制一個圓環,這是球的邊緣。
(2)第二部分繪制(圖4)區域。在這一部分中通過判斷isWaveMoving做兩種不同的處理。
當ture時表示現在波浪在運動,通過getWaveY生成兩條參數完全不同的曲線上的點,以這些點為基礎繪制直線達到填充效果。
當false時表示不在運動,這時沒有波浪,即水平線是平的,直接繪制兩個矩形即可。
(3)第三部分繪制遮罩,產生(圖5)的效果。
遮罩是一個圓形的bitmap,遮罩模式我們使用DST_IN。
遮罩bitmap由于基本不會改變,所以不需要每次onDraw的時候去創建,它的創建放在onSizeChanged中,代碼如下:
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);if (w > 0 && h > 0) {/*** 根據寬高初始化波浪的一些參數* 波浪的速度根據寬度的一定比例,這樣不同寬度波浪移動的效果保持差不多* 波浪的振幅根據高度和默認值,當高度太小就設為高度的一定比例,這樣保證不同高度下波浪效果明顯* 波浪的周期固定即可*/mWaveSpeedA = w / 10;mWaveSpeedB = w / 17;mWaveHeightA = DisplayUtils.dip2px(getContext(), 10);mWaveHeightB = DisplayUtils.dip2px(getContext(), 5);if (h / 10 < mWaveHeightA) {mWaveHeightA = h / 10;mWaveHeightB = h / 20;}initStopAnimator(mWaveHeightA, mWaveHeightB);mWaveACycle = (float) (3 * Math.PI / w);mWaveBCycle = (float) (4 * Math.PI / w);/*** 初始化圓形遮罩* 圓形遮罩是一個與組件同大小的橢圓,并且四周為透明* 注意:* 不在onDraw中直接繪制這個遮罩,因為那樣繪制后遮罩只是一個橢圓,使用DST_IN的話在橢圓外的部分* 就不會做任何處理,達不到效果;而先做成bitmap的話遮罩是一個方形,橢圓外部分就會去掉,達到效果**/mBallBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(mBallBitmap);RectF ball = new RectF(0, 0, w, h);canvas.drawOval(ball, mWavePaint);} }這樣可以在Size改變的時候,重新創建一個符合的遮罩。
可以看到在onSizeChanged中,我們同樣做了一些參數的初始化工作,根據組件的寬和高,為A和B兩條曲線初始化不同的且合適的振幅、周期、移動速度。
4、實現波浪運動效果
1)實現橫向運動
到此我們繪制出了(圖1)的效果,但是這只是一個靜態圖案,我們如果讓波浪動起來呢?
波浪的運動包含兩個方向的運動:上下運動和左右運動。
上下運動與參數d有關,在getWaveY函數中可以看到參數d是由mProgress這個參數決定的,所以改變這個參數就可以實現波浪的漲落。
左右運動本質上是曲線的偏移,由參數c控制,在onDraw代碼中可以看到分別是mOffsetA和mOffsetB。
使用屬性動畫來動態改變這幾個參數就可以實現波浪的運動效果,具體代碼如下
/*** 設置進度,并且以動畫的形式上漲到該進度* @param progress 進度* @param duration 持續時間* @param delay 延時*/ public void startProgress(int progress, long duration, long delay){if(mProgressAnimator != null && mProgressAnimator.isRunning()){mProgressAnimator.cancel();}if(mWaveStopAnimator != null && mWaveStopAnimator.isRunning()){mWaveStopAnimator.cancel();}isWaveMoving = true;mProgressAnimator = ObjectAnimator.ofInt(this, "Progress", progress);mProgressAnimator.setDuration(duration);mProgressAnimator.setStartDelay(delay);mProgressAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {}@Overridepublic void onAnimationEnd(Animator animation) {mWaveStopAnimator.start();}@Overridepublic void onAnimationCancel(Animator animation) {}@Overridepublic void onAnimationRepeat(Animator animation) {}});mProgressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator valueAnimator) {//改變曲線的偏移,達到波浪運動的效果mOffsetA += mWaveSpeedA;mOffsetB += mWaveSpeedB;invalidate();}});mProgressAnimator.start(); }先重點關注mProgressAnimator這個動畫。
ObjectAnimator.ofInt(this, "Progress", progress)
第二個參數表示改變的屬性,會調用setProgress和getProgress方法改變對應的屬性,最終改變的是mProgress屬性
第三個參數是最終值,表示這個動畫會將該屬性的從當前值逐漸改變成設定值(即最終值)
這樣波浪的漲落就可以通過mProgressAnimator產生了。
然后可以看到為mProgressAnimator添加了AnimatorUpdateListener,所以在改變mProgress的同時,也在動態的改變mOffsetA和mOffsetB并重繪,這樣同時波浪的左右也實現了。
注意在onSizeChanged函數中初始化mWaveSpeedA和mWaveSpeedB的值是不同的,這樣兩條曲線每次改變的位移會不同,就會產生兩個波浪反復交錯的效果。如果mWaveSpeedA和mWaveSpeedB一樣,那么兩條曲線就會相對靜止,波浪效果很差。
到此,我們通過startProgress函數來改變WaveBallProgress的進度值,就會產生波浪漲落的效果。
離我們的最終效果只差一步了,因為當波浪漲到新的進度時,我們希望水面可以慢慢平靜下來。
2)實現波浪消退效果
如果我們在mProgressAnimator動畫結束時立刻讓水面恢復平靜,會顯得很突兀。我們需要讓波浪逐漸變小直至恢復平靜,所以在mProgressAnimator動畫結束(onAnimationEnd)時我們啟動了另外一個動畫mWaveStopAnimator。
這個動畫的初始化是在onSizeChanged函數中進行的,初始化函數代碼如下
private void initStopAnimator(final int waveHeightA, final int waveHeightB){/*** 創建波浪停止動畫* 兩條波浪振幅逐漸減小*/PropertyValuesHolder holderA = PropertyValuesHolder.ofInt("WaveHeightA", 0);PropertyValuesHolder holderB = PropertyValuesHolder.ofInt("WaveHeightB", 0);mWaveStopAnimator = ObjectAnimator.ofPropertyValuesHolder(this, holderA, holderB);mWaveStopAnimator.setDuration(1000);mWaveStopAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {}@Overridepublic void onAnimationEnd(Animator animation) {isWaveMoving = false;mWaveHeightA = waveHeightA;mWaveHeightB = waveHeightB;}@Overridepublic void onAnimationCancel(Animator animation) {mWaveHeightA = waveHeightA;mWaveHeightB = waveHeightB;}@Overridepublic void onAnimationRepeat(Animator animation) {}});mWaveStopAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator valueAnimator) {//改變曲線的偏移,達到波浪運動的效果mOffsetA += mWaveSpeedA;mOffsetB += mWaveSpeedB;invalidate();}}); }在這里使用了PropertyValueHolder,讓一個動畫同時實現多個效果。我們同時減小兩條曲線的振幅直到為0,這樣波浪就會逐漸變小直到變成一條直線。
同第一個動畫一樣,在動畫過程中繼續改變offset保證波浪運動。
在動畫結束時或cancel時重置mWaveHeightA和mWaveHeightB,保證下一次startProgress使用正確的振幅。
再回頭看startProgress函數一開始,判斷兩個動畫是否在進行中,如果是cancle掉。保證在頻繁改變進度的時候不會出現幾個動畫一起運行的情況。
5、總結
到此所有功能都完成了,我們實現了一個水晶球波浪進度條。
總結一下,在本篇文章里我們主要使用了遮罩和屬性動畫這兩個功能:
(1)遮罩對于實現一些特殊形狀的繪制很有幫助,而且遮罩有很多種模式,不同的模式解決不同的問題。
(2)屬性動畫是Android幾種動畫之一,除了ObjectAnimator還有其他類,而且支持多種方式,比如同時執行、依次執行、反復執行等等。屬性動畫應用場景很多,可以解決很多問題,是Android開發者進階必會的一個知識點。
大家有興趣可以自己手動實現一下,對這兩個功能有更深入的了解。
源碼:
關注公眾號:BennuCTech,發送“FastWidget”獲取完整源碼總結
以上是生活随笔為你收集整理的Android魔术——手把手教你实现水晶球波浪进度条的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解决wiremock中velocity脚
- 下一篇: 解决Picasso在Android 5.