推ter的like動畫安卓版 - 備選方案

jopen 8年前發布 | 9K 次閱讀 Twitter Android開發 移動開發



英文原文: 推ter's like animation in Android - alternative


不久前推ter展示了具有現代感的心形動畫-作為star圖標的替代。

推ter的like動畫安卓版 - 備選方案

雖然心形標志更普遍和昂貴,但是今天我們嘗試復制新的動畫,使用舊的星星圖標。我們的效果如下(比gif圖快一點點):

推ter的like動畫安卓版 - 備選方案

雖然實現這個動畫最簡單的方法是使用 Frame Animation ,但是我們嘗試用更靈活的方法來實現-手動繪制并用屬性動畫。這篇文章只是概要,沒有深入的技術細節。

實現

我們將創建一個名叫LikeButtonView的view,它是一個由三個子view構成的FrameLayout- CircleView 顯示星星圖標下面的圓,ImageView (星星)以及代表按鈕周圍浮點的DotsView 。

CircleView

推ter的like動畫安卓版 - 備選方案

這個視圖負責繪制星星圖標下面的大圓。它本可以實現得更簡單(通過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

DotsView

推ter的like動畫安卓版 - 備選方案

這個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

LikeButtonView

最終的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();
}

全是關于恰當的時間和插值器。

推ter的like動畫安卓版 - 備選方案

我們的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號

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!