最近在github很火的一个动画效果,代码写得还是不错的,拿来学习下,做个总结。
先来看下最终的实现效果:
最简单直接的实现方式就是使用Frame Animiations,但是性能和可扩张性都不高,这里通过更复杂的一个方式:自己绘制图形并且利用ObjectAnimator来执行动画。
具体实现
整个动画控件由一个FrameLayout 组成,这个FrameLayout包含三个子View,
- CircleView ,显示五角星下面的圆形动画
- ImageView ,是一个五角星
- DotsView 代表最外边飞动的小圆点
1. CircleView
仔细观察,整个变化过程由三部分组成:1. 一个实心圆半径(这个圆形的大小)由小变大,2. 在大小变化的过程中,颜色也在同时变化,3. 实心圆变到一个定大小时,开始出现一个内径圆,是白色的,然后半径由小变大,最终将黄色的圆覆盖掉。这里感觉比较难实现的是内心圆的动画。
具体的绘制必然是在onDraw()方法中
Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
tempCanvas.drawColor(0xffffff, PorterDuff.Mode.CLEAR);
tempCanvas.drawCircle(getWidth() / 2, getHeight() / 2, outerCircleRadiusProgress * maxCircleSize, circlePaint);
tempCanvas.drawCircle(getWidth() / 2, getHeight() / 2, innerCircleRadiusProgress * maxCircleSize, maskPaint);
canvas.drawBitmap(tempBitmap, 0, 0, null);}
首先利用绘制颜色(使用Clear模式)将整个画布做一个初始化,然后根据具体的进度(outerCircleRadiusProgress,innerCircleRadiusProgress)绘制内径圆和外面的圆
内径圆使用的mash paint如下面定义:
maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
这样外面的圆就会在内部创建一个透明的洞,也即是内径圆。
里面的tempCanvas在如下代码创建:
Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
maxCircleSize = w / 2;
tempBitmap = Bitmap.createBitmap(getWidth(), getWidth(), Bitmap.Config.ARGB_8888);
tempCanvas = new Canvas(tempBitmap);
}
这样可以避免内径圆显示window color。
那么颜色是怎么变化的呢?是通过ArgbEvaluator 这个类来根据给定的比率在两个颜色值之间变化。
private void updateCircleColor() {
float colorProgress = (float) Utils.clamp(outerCircleRadiusProgress, 0.5, 1);
colorProgress = (float) Utils.mapValueFromRangeToRange(colorProgress, 0.5f, 1f, 0f, 1f);
this.circlePaint.setColor((Integer) argbEvaluator.evaluate(colorProgress, START_COLOR, END_COLOR));
}
CircleView主要的代码就分析完了,下面来看DotsView。
2. DotsView
这个View来显示外围的小圆点动画,同CircleView 一样,也是在onDraw()绘画的:
@Override
protected void onDraw(Canvas canvas) {
drawOuterDotsFrame(canvas);
drawInnerDotsFrame(canvas);
}
private void drawOuterDotsFrame(Canvas canvas) {
for (int i = 0; i < DOTS_COUNT; i++) {
int cX = (int) (centerX + currentRadius1 * Math.cos(i * OUTER_DOTS_POSITION_ANGLE * Math.PI / 180));
int cY = (int) (centerY + currentRadius1 * Math.sin(i * OUTER_DOTS_POSITION_ANGLE * Math.PI / 180));
canvas.drawCircle(cX, cY, currentDotSize1, circlePaints[i % circlePaints.length]);
}
private void drawInnerDotsFrame(Canvas canvas) {
for (int i = 0; i < DOTS_COUNT; i++) {
int cX = (int) (centerX + currentRadius2 * Math.cos((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180));
int cY = (int) (centerY + currentRadius2 * Math.sin((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180));
canvas.drawCircle(cX, cY, currentDotSize2, circlePaints[(i + 1) % circlePaints.length]);
}
}
可以看到通过循环创建了一些小圆点,每隔OUTER_DOTS_POSITION_ANGLE (51)绘制一组小圆点。 每个圆点的颜色变化是通过paint.setColor控制的:
private void updateDotsPaints() {
if (currentProgress < 0.5f) {
float progress = (float) Utils.mapValueFromRangeToRange(currentProgress, 0f, 0.5f, 0, 1f);
circlePaints[0].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_1, COLOR_2));
circlePaints[1].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_2, COLOR_3));
circlePaints[2].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_3, COLOR_4));
circlePaints[3].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_4, COLOR_1));
} else {
float progress = (float) Utils.mapValueFromRangeToRange(currentProgress, 0.5f, 1f, 0, 1f);
circlePaints[0].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_2, COLOR_3));
circlePaints[1].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_3, COLOR_4));
circlePaints[2].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_4, COLOR_1));
circlePaints[3].setColor((Integer) argbEvaluator.evaluate(progress, COLOR_1, COLOR_2));
}}
这里的颜色变化也是使用ArgbEvaluator 来根据progress生成具体的颜色值。
3. LikeButtonView
这个view继承了FrameLayout,将CircleView, ImageView 和 DotsView组合到一起
<?xml version="1.0" encoding="utf-8"?><merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<frogermcs.io.likeanimation.DotsView
android:id="@+id/vDotsView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"/>
<frogermcs.io.likeanimation.CircleView
android:id="@+id/vCircle"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center"/>
<ImageView
android:id="@+id/ivStar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_star_rate_off"/>
</merge>
这里使用Merger标签来去除多余的层级嵌套,LikeButtonView 本身就是一个FrameLayout ,没有必要使用两次。
最后完整的动画效果就是将上面的动画效果组合到一起,放入到AnimatorSet来一起播放。
@Override
public void onClick(View v) {
//...
animatorSet = new AnimatorSet();
ObjectAnimator outerCircleAnimator = ObjectAnimator.ofFloat(vCircle, CircleView.OUTER_CIRCLE_RADIUS_PROGRESS, 0.1f, 1f);
outerCircleAnimator.setDuration(250);
outerCircleAnimator.setInterpolator(DECCELERATE_INTERPOLATOR);
ObjectAnimator innerCircleAnimator = ObjectAnimator.ofFloat(vCircle, CircleView.INNER_CIRCLE_RADIUS_PROGRESS, 0.1f, 1f);
innerCircleAnimator.setDuration(200);
innerCircleAnimator.setStartDelay(200);
innerCircleAnimator.setInterpolator(DECCELERATE_INTERPOLATOR);
ObjectAnimator starScaleYAnimator = ObjectAnimator.ofFloat(ivStar, ImageView.SCALE_Y, 0.2f, 1f);
starScaleYAnimator.setDuration(350);
starScaleYAnimator.setStartDelay(250);
starScaleYAnimator.setInterpolator(OVERSHOOT_INTERPOLATOR);
ObjectAnimator starScaleXAnimator = ObjectAnimator.ofFloat(ivStar, ImageView.SCALE_X, 0.2f, 1f);
starScaleXAnimator.setDuration(350);
starScaleXAnimator.setStartDelay(250);
starScaleXAnimator.setInterpolator(OVERSHOOT_INTERPOLATOR);
ObjectAnimator dotsAnimator = ObjectAnimator.ofFloat(vDotsView, DotsView.DOTS_PROGRESS, 0, 1f);
dotsAnimator.setDuration(900);
dotsAnimator.setStartDelay(50);
dotsAnimator.setInterpolator(ACCELERATE_DECELERATE_INTERPOLATOR);
animatorSet.playTogether(
outerCircleAnimator,
innerCircleAnimator,
starScaleYAnimator,
starScaleXAnimator,
dotsAnimator
);
//...
animatorSet.start();
}
基本的实现框架就是这样的,具体完整的代码见:
https://github.com/frogermcs/LikeAnimation/