自定义实现播放暂停Drawable

本文一步步解析自定义播放暂定 Drawable,该 Drawable 可以用于控件的背景,和自定义View是大同小异的。

这篇文章的来源是一个开源项目的动画效果,我下载下来看了下,感觉是个入门自定义View很好的例子,所以写了这篇文章~~

那个开源项目的名字是 Timber,是个音乐播放器!

废话不多说,进入正文~~

先看效果图

自定义实现播放暂停Drawable

放个大一点的

自定义实现播放暂停Drawable

好,当我第一次看到这个效果的时候,我的表情是这样的

自定义实现播放暂停Drawable

不急,我们慢慢来解析一下是怎么实现的。

几个问题~

第一:我们要怎么把暂停的图变成三角形的图?直接分开画?不行,要有动画的效果,就必须一个过渡的状态,而不是一闪而过

第二:旋转是如何处理的?

 

接下来就解决这两个问题!!!

第一步:我们要怎么把它变成三角形呢?

首先是一个暂停的图,

自定义实现播放暂停Drawable自定义实现播放暂停Drawable

要有过渡状态,想一下,其实两个矩形变成一个三角形很简单,我们是不是先把两个矩形中间的间隔去掉,就变成这样了

自定义实现播放暂停Drawable自定义实现播放暂停Drawable

然后呢?是不是只要把 左边矩形的左上角右边矩形的右上角 移动到中间就行了?

自定义实现播放暂停Drawable

同时我们把 左边矩形的左下角右边矩形的右下角 适当拉开,并把 底边 适当抬高

自定义实现播放暂停Drawable

然后再 旋转

让上面这几步在同一时间进行,就会想动画一样,这样不就完成了么? 

自定义实现播放暂停Drawable

接下来我们看代码怎么写

这个类继承自 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);
        }
    }

可能有人看完心情是这样的: 

自定义实现播放暂停Drawable

不急,听我慢慢道来:

首先,我们画的时候是把画布移动到 左边矩形的左下角 ,然后进行画操作的:

自定义实现播放暂停Drawable

也就是 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;
    }

我第一眼看到时是懵逼的,这是什么东西?

自定义实现播放暂停Drawable

后来想一想,这其实是根据 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/wuxiaogui593/MyWidgetLib/tree/master/myview/src/main/java/com/example/myview/drawable

喜欢点个赞~~互勉