自定義實現播放暫停Drawable

本文一步步解析自定義播放暫定 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

喜歡點個贊~~互勉

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章