本文一步步解析自定義播放暫定 Drawable,該 Drawable 可以用於控件的背景,和自定義View是大同小異的。
這篇文章的來源是一個開源項目的動畫效果,我下載下來看了下,感覺是個入門自定義View很好的例子,所以寫了這篇文章~~
那個開源項目的名字是 Timber,是個音樂播放器!
廢話不多說,進入正文~~
先看效果圖
放個大一點的
好,當我第一次看到這個效果的時候,我的表情是這樣的
不急,我們慢慢來解析一下是怎麼實現的。
幾個問題~
第一:我們要怎麼把暫停的圖變成三角形的圖?直接分開畫?不行,要有動畫的效果,就必須一個過渡的狀態,而不是一閃而過
第二:旋轉是如何處理的?
接下來就解決這兩個問題!!!
第一步:我們要怎麼把它變成三角形呢?
首先是一個暫停的圖,
要有過渡狀態,想一下,其實兩個矩形變成一個三角形很簡單,我們是不是先把兩個矩形中間的間隔去掉,就變成這樣了
然後呢?是不是隻要把 左邊矩形的左上角 和 右邊矩形的右上角 移動到中間就行了?
同時我們把 左邊矩形的左下角 和 右邊矩形的右下角 適當拉開,並把 底邊 適當擡高
然後再 旋轉
讓上面這幾步在同一時間進行,就會想動畫一樣,這樣不就完成了麼?
接下來我們看代碼怎麼寫
這個類繼承自 Drawable
public class PlayPauseDrawable extends Drawable
提供兩個函數進行動畫播放,一個是從暫停變爲播放,一個是從播放變爲暫停
public void transformToPause(boolean animated) {
if (isPlay) {
if (animated) {
toggle();
} else {
isPlay = false;
setProgress(0.0F);
}
}
}
public void transformToPlay(boolean animated) {
if (!isPlay) {
if (animated) {
toggle();
} else {
isPlay = true;
setProgress(1.0F);
}
}
}
這裏有個參數 isPlay,當它爲 true 時,代表當前是 三角形狀態,當它爲 false 時,代表當前是 兩個矩形的狀態
然後是 animated 是決定是否使用動畫,默認爲 true,我們看 toggle() 方法
private void toggle() {
if (animator != null) {
animator.cancel();
}
// 用插值器 改變 PROGRESS 的值
animator = ObjectAnimator.ofFloat(this, PROGRESS, isPlay ? 1.0F : 0.0F, isPlay ? 0.0F : 1.0F);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//動畫結束時將 isPlay 取反
isPlay = !isPlay;
Log.e(TAG, "onAnimationEnd: "+isPlay);
}
});
animator.setInterpolator(new DecelerateInterpolator());
animator.setDuration(200);
animator.start();
}
當 isPlay 爲 true,也就是說接下來的動畫是從 三角形 變爲 矩形 ,PROGRESS 的值就從 1 到 0。
當 isPlay 爲 false,也就是說接下來的動畫是從 矩形 變爲 三角形,PROGRESS 的值就從 0 到1。
PROGRESS 的定義如下,他會改變 成員變量 progress 的值並調用invalidate方法進行重繪
private float progress;
private static final Property<PlayPauseDrawable, Float> PROGRESS =
new Property<PlayPauseDrawable, Float>(Float.class, "progress") {
@Override
public Float get(PlayPauseDrawable d) {
return d.getProgress();
}
@Override
public void set(PlayPauseDrawable d, Float value) {
d.setProgress(value);
}
};
private void setProgress(float progress) {
this.progress = progress;
invalidateSelf();
}
好了,重點來了
就是 draw() 方法的代碼了
private final Path leftPauseBar = new Path();
private final Path rightPauseBar = new Path();
@Override
public void draw(Canvas canvas) {
long startDraw = System.currentTimeMillis();
// 重置左邊矩形和右邊矩形的 path
leftPauseBar.rewind();
rightPauseBar.rewind();
// 設定 單個矩形的高度、寬度和兩個矩形的距離
float pauseBarHeight = 7.0F / 12.0F * ((float) getBounds().height());
float pauseBarWidth = pauseBarHeight / 3.0F;
float pauseBarDistance = pauseBarHeight / 3.6F;
// 根據 progress 求出當前 兩個矩形的距離
final float barDist = interpolate(pauseBarDistance, 0.0F, progress);
// 根據 progress 求出當前 左邊矩形左下角 和 右邊矩形右下角 距離中心線的距離
final float barWidth = interpolate(pauseBarWidth, pauseBarHeight / 1.75F, progress);
// 根據 progress 求得第一個矩形的左上角的 x座標
final float firstBarTopLeft = interpolate(0.0F, barWidth, progress);
// 根據 progress 求得第二個矩形的右上角的 x座標
final float secondBarTopRight = interpolate(2.0F * barWidth + barDist, barWidth + barDist, progress);
// 畫左邊矩形的 path
leftPauseBar.moveTo(0.0F, 0.0F);
leftPauseBar.lineTo(firstBarTopLeft, -pauseBarHeight);
leftPauseBar.lineTo(barWidth, -pauseBarHeight);
leftPauseBar.lineTo(barWidth, 0.0F);
leftPauseBar.close();
// 畫右邊矩形的 path
rightPauseBar.moveTo(barWidth + barDist, 0.0F);
rightPauseBar.lineTo(barWidth + barDist, -pauseBarHeight);
rightPauseBar.lineTo(secondBarTopRight, -pauseBarHeight);
rightPauseBar.lineTo(2.0F * barWidth + barDist, 0.0F);
rightPauseBar.close();
// 保存 canvas 的狀態
canvas.save();
// 這裏就是上面我們說的一個步驟,將底部擡高的步驟
canvas.translate(interpolate(0.0F, pauseBarHeight / 8.0F, progress), 0.0F);
// (1) Pause --> Play: 順時針旋轉 0 到 90 度
// (2) Play --> Pause: 順時針旋轉 90 到 180 度
final float rotationProgress = isPlay ? 1.0F - progress : progress;// play->pause時progress是從1到0,所以這裏有區別
// 初始角度
final float startingRotation = isPlay ? 90.0F : 0.0F;
// 根據 progress 計算旋轉的角度,以中點爲圓心旋轉
canvas.rotate(interpolate(startingRotation, startingRotation + 90.0F, rotationProgress), getBounds().width() / 2.0F, getBounds().height() / 2.0F);
// Position the pause/play button in the center of the drawable's bounds.
// 移動canvas到左邊矩形的左下角
canvas.translate(getBounds().width() / 2.0F - ((2.0F * barWidth + barDist) / 2.0F), getBounds().height() / 2.0F + (pauseBarHeight / 2.0F));
// 畫兩個矩形
canvas.drawPath(leftPauseBar, paint);
canvas.drawPath(rightPauseBar, paint);
canvas.restore();
long timeElapsed = System.currentTimeMillis() - startDraw;
if (timeElapsed > 16) {
Log.e(TAG, "Drawing took too long=" + timeElapsed);
}
}
可能有人看完心情是這樣的:
不急,聽我慢慢道來:
首先,我們畫的時候是把畫布移動到 左邊矩形的左下角 ,然後進行畫操作的:
也就是 canvas 的(0,0)點是在 左下角的,所以我們畫的時候需要注意 正負值。
然後上面有個方法被多次調用
// 根據 progress 求出當前 兩個矩形的距離
final float barDist = interpolate(pauseBarDistance, 0.0F, progress);
// 根據 progress 求出當前 左邊矩形左下角 和 右邊矩形右下角 距離中心線的距離
final float barWidth = interpolate(pauseBarWidth, pauseBarHeight / 1.75F, progress);
// 根據 progress 求得第一個矩形的左上角的 x座標
final float firstBarTopLeft = interpolate(0.0F, barWidth, progress);
// 根據 progress 求得第二個矩形的右上角的 x座標
final float secondBarTopRight = interpolate(2.0F * barWidth + barDist, barWidth + barDist, progress);
這個 interpolate() 方法時做什麼的呢
private static float interpolate(float a, float b, float t) {
return a + (b - a) * t;
}
我第一眼看到時是懵逼的,這是什麼東西?
後來想一想,這其實是根據 t 計算從 a 到 b 中間值的方法。也就是說,當 t 等於0時,返回值是a,當 t 等於 1 時,返回值是 b。
這麼說懂了吧,也就是說根據 progress ,計算各個座標的值。
這個懂了,其他地方就很好懂了,註釋也寫的很清楚了,只要仔細想想,就很容易理解~~~
使用的時候只要實例化這個Drawable,作爲某個View(比如 ImageView)的 圖標,比如
mImageView.setImageDrawable(playPauseDrawable);
然後在需要動畫的時候調用
playPauseDrawable.transformToPlay(true);
playPauseDrawable.transformToPause(true);
好了,文章到此結束~~~
源碼地址:https://github.com/SkUnK-cc/MyWidgetLib/tree/master/myview/src/main/java/com/example/myview/drawable
喜歡點個贊~~互勉