英文原文: Twitter's like animation in Android - alternative
不久前Twitter展示了具有现代感的心形动画-作为star图标的替代。
虽然心形标志更普遍和昂贵,但是今天我们尝试复制新的动画,使用旧的星星图标。我们的效果如下(比gif图快一点点):
虽然实现这个动画最简单的方法是使用 Frame Animation ,但是我们尝试用更灵活的方法来实现-手动绘制并用属性动画。这篇文章只是概要,没有深入的技术细节。
我们将创建一个名叫LikeButtonView的view,它是一个由三个子view构成的FrameLayout- CircleView 显示星星图标下面的圆,ImageView (星星)以及代表按钮周围浮点的DotsView 。
这个视图负责绘制星星图标下面的大圆。它本可以实现得更简单(通过xml <shape android:shape="oval"> ),但是这里我们应该考虑按钮下面的背景颜色。
我们在canvas上绘制圆的实现:
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); }
ondraw.java hosted with ❤ by GitHub
先使用CLEAR 模式绘制颜色以清除canvas。然后根据给定的进度(各自的进度是独立的)绘制内外圆。
内圆使用这样定义的mask paint :
maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
maskPaint.java hosted with ❤ by GitHub
意味着内圆将在外圆内部创建一个透明的洞。
我们视图中使用了tempBitmap的tempCanvas定义如下:
Override protected 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); }
onSizeChanged.java hosted with ❤ by GitHub
我们需要完全透明,不然的话内圆就会显示窗口颜色。
对于那些眼睛机灵的人应该还注意到了另外一件事-我们的外圆颜色是基于当前进度而变化的。这是通过 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)); }
updateCircleColor.java hosted with ❤ by GitHub
CircleView代码的剩余部分就是一个实现的问题了。完整的源代码可以在这里找到: CircleView 。
这个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]); } }
onDraw2.java hosted with ❤ by GitHub
圆点是基于currentProgress绘制的,背后是数学逻辑,老实说从安卓sdk的角度来看这里没有什么有趣的地方,倒是有两个跟数学相关的东西:
圆点分布在一个圆上-它们的位置决定于:
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));
意味着:在每个 OUTER_DOTS_POSITION_ANGLE 上设置圆点 (51 度).
每个圆点都有它自己的颜色动画:
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)); } }
这意味着圆点颜色在3个区间形式的值之间动画。我们再一次使用ArgbEvaluator 让它平滑。其余就很简单了。这个类的完整代码在这里: DotsView
最终的ViewGroup是由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>
likebutton.xml hosted with ❤ by GitHub
我们使用 Merge 标签帮助消除多余的ViewGroup。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(); }
全是关于恰当的时间和插值器。
我们的LikeButtonView还会响应触摸事件(缩放动画):
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: ivStar.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).setInterpolator(DECCELERATE_INTERPOLATOR); setPressed(true); break; case MotionEvent.ACTION_MOVE: float x = event.getX(); float y = event.getY(); boolean isInside = (x > 0 && x < getWidth() && y > 0 && y < getHeight()); if (isPressed() != isInside) { setPressed(isInside); } break; case MotionEvent.ACTION_UP: ivStar.animate().scaleX(1).scaleY(1).setInterpolator(DECCELERATE_INTERPOLATOR); if (isPressed()) { performClick(); setPressed(false); } break; } return true; }
以上就是全部。就如你看到的,这里没有神秘的东西,但是最终的效果却很赞。那么现在该干什么呢?让我们用它来美化自己的app吧。
所描述的项目的完整源代码可以在github的 repository 上获取。
Miroslaw Stanek
如果你喜欢这篇文章,你可以或者 关注我 !
写于2015年12月22号