自定义实现播放暂停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);
好了,文章到此结束~~~
喜欢点个赞~~互勉