前言
最近從Airbnb的Lottie開源程式中,挑了Favorite Star和Twitter Heart的效果來練習,這兩個動畫效果其實相仿,只是使用星星和愛心而已,這篇文章就是來解說如何達到這個效果,效果如下。
以下我歸納為幾個實作步驟:
- 製作CircleView類別(中間圓圈的繪製)
- 製作SideCircleView類別(外側大小圓圈的繪製)
- 製作HeartView類別
- 製作FavoriteView類別
製作CircleView類別(中間圓圈的繪製)
中間圓圈分成內圈和外圈兩種,基本上就是繪製兩層圓圈,只是內圈會使用延遲,看起來會有同心圓的效果,以下為重要的程式碼片段。
- Init(): 這個是初始化方法,分別設置內外圓圈的畫筆。
    private void init() {
        this.outerCirclePaint.setStyle(Paint.Style.FILL);
        this.outerCirclePaint.setColor(COLOR);
        this.innerCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        this.innerCirclePaint.setColor(COLOR);
    }- onSizeChanged(): 這個是視圖的生命週期的其中一個方法,當視圖尺寸有變更時所呼叫的方法,我們在這邊設置圓圈最大的尺寸。
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.maxCircleSize = w / 2;
        this.tempBitmap = Bitmap.createBitmap(this.getWidth(), this.getWidth(), Bitmap.Config.ARGB_8888);
        this.tempCanvas = new Canvas(this.tempBitmap);
    }- onDraw(): 在這邊我們繪製內外圓圈,並且設置兩個變數 - outerCircleRadiusProgress與innerCircleRadiusProgress,提供Animator在數值改變時,同時更新我們的進度。
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        this.tempCanvas.drawColor(0xffffff, PorterDuff.Mode.CLEAR);
        this.tempCanvas.drawCircle(this.getWidth() / 2, this.getHeight() / 2, this.outerCircleRadiusProgress * this.maxCircleSize, this.outerCirclePaint);
        this.tempCanvas.drawCircle(this.getWidth() / 2, this.getHeight() / 2, this.innerCircleRadiusProgress * this.maxCircleSize, this.innerCirclePaint);
        canvas.drawBitmap(this.tempBitmap, 0, 0, null);
    }
製作SideCircleView類別(外側大小圓圈的繪製)
外側圓圈我們共繪製八個,其實你可以使用八種不同的色彩來進行顏色繪製,但這邊我直接使用同一種顏色(0xFF99BBFF),以下為重要的程式碼片段。- init(): 這邊初始化八個畫筆,你可以將顏色設定成隨機八種,為求方便,這邊只使用一種。
    private void init() {
        for (int i = 0; i < this.circlePaints.length; i++) {
            this.circlePaints[i] = new Paint();
            this.circlePaints[i].setStyle(Paint.Style.FILL);
            this.circlePaints[i].setColor(COLOR);
        }
    }- onSizeChanged(): 同CircleView類別,只是設定中心點的X與Y的位置、尺寸大小、大圓和小圓的半徑。大圓的半徑則為寬度一半小一點,小圓則是一樣,基本上這邊可以自由設置。
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.centerX = w / 2;
        this.centerY = h / 2;
        this.maxDotSize = 20;
        this.maxOuterDotsRadius = w / 2 - this.maxDotSize * 2;
        this.maxInnerDotsRadius = this.maxOuterDotsRadius;
    }- onDraw(): 這邊需要用到三角函數來計算每個大小圓被分配的弧度,也就是大小圓的實際X和Y,先計算出大圓的位置,小圓則是往左偏移,也就是將大圓的位置扣除欲偏移的距離即可。
    @Override
    protected void onDraw(Canvas canvas) {
        this.drawOuterDotsFrame(canvas);
        this.drawInnerDotsFrame(canvas);
    }    private void drawOuterDotsFrame(Canvas canvas) {
        for (int i = 0; i < DOTS_COUNT; i++) {
            int cX = (int) (centerX + this.currentRadius1 * Math.cos(i * OUTER_DOTS_POSITION_ANGLE * Math.PI / 180));
            int cY = (int) (centerY + this.currentRadius1 * Math.sin(i * OUTER_DOTS_POSITION_ANGLE * Math.PI / 180));
            canvas.drawCircle(cX, cY, this.currentDotSize1, this.circlePaints[i % this.circlePaints.length]);
        }
    }    private void drawInnerDotsFrame(Canvas canvas) {
        for (int i = 0; i < DOTS_COUNT; i++) {
            int cX = (int) (centerX + this.currentRadius2 * Math.cos((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180));
            int cY = (int) (centerY + this.currentRadius2 * Math.sin((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180));
            canvas.drawCircle(cX, cY, this.currentDotSize2, this.circlePaints[(i + 1) % this.circlePaints.length]);
        }
    }- setCurrentProgress(): 這個方法則是動畫更新時,我們要更新的大小圓繪製進度。大圓則是在進度70%前,維持原本尺寸,在70%後則逐漸消失;小圓分為三種進度,20%前維持原本尺寸,20%至50%則是逐漸出現,之後則是逐漸消失。
    public void setCurrentProgress(float currentProgress) {
        this.currentProgress = currentProgress;
        this.updateOuterDotsPosition();
        this.updateInnerDotsPosition();
        this.postInvalidate();
    }
    private void updateOuterDotsPosition() {
        this.currentRadius1 = (float) this.rangeValue(currentProgress, 0.0f, 1.0f, 0.0f, maxOuterDotsRadius);
        if (currentProgress < 0.7) {
            this.currentDotSize1 = maxDotSize;
        } else {
            this.currentDotSize1 = (float) this.rangeValue(currentProgress, 0.7f, 1.0f, maxDotSize, 0.0f);
        }
    }
    private void updateInnerDotsPosition() {
        this.currentRadius2 = (float) this.rangeValue(currentProgress, 0.0f, 1.0f, 0.0f, maxInnerDotsRadius);
        if (currentProgress < 0.2) {
            this.currentDotSize2 = maxDotSize;
        } else if (currentProgress < 0.5) {
            this.currentDotSize2 = (float) this.rangeValue(currentProgress, 0.2f, 0.5f, maxDotSize, 0.8f * maxDotSize);
        } else {
            this.currentDotSize2 = (float) this.rangeValue(currentProgress, 0.5f, 1.0f, maxDotSize * 0.8f, 0.0f);
        }
    }
    public double rangeValue(double value, double fromLow, double fromHigh, double toLow, double toHigh) {
        return toLow + ((value - fromLow) / (fromHigh - fromLow) * (toHigh - toLow));
    }
製作HeartView類別
先前已介紹過愛心的繪製,這邊就不多做說明,可以參考先前的文章:Android: 製作Heart Beat視圖。
製作FavoriteView類別
這個視圖則是將所有的視圖組合起來,並且加入動畫,以下為重要的程式碼片段。- custom_view_favorite.xml
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <com.xy.favoriteview.view.SideCircleView
        android:id="@+id/view_side_circle"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:layout_gravity="center" />
    <com.xy.favoriteview.view.CircleView
        android:id="@+id/view_circle"
        android:layout_width="58dp"
        android:layout_height="58dp"
        android:layout_gravity="center" />
    <com.xy.favoriteview.view.HeartView
        android:id="@+id/view_heart"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_gravity="center" />
</merge>- init()
    private void init() {
        View view = LayoutInflater.from(this.getContext()).inflate(R.layout.custom_view_favorite, this, true);
        this.circleView = (CircleView) view.findViewById(R.id.view_circle);
        this.sideCircleView = (SideCircleView) view.findViewById(R.id.view_side_circle);
        this.heartView = (HeartView) view.findViewById(R.id.view_heart);
    }- launchAnim()
    public void launchAnim() {
        this.circleView.setInnerCircleRadiusProgress(0);
        this.circleView.setOuterCircleRadiusProgress(0);
        this.sideCircleView.setCurrentProgress(0);
        final ObjectAnimator outerCircleAnimator = ObjectAnimator.ofFloat(this.circleView, CircleView.OUTER_CIRCLE_RADIUS_PROGRESS, 0.1f, 1f);
        outerCircleAnimator.setDuration(300);
        outerCircleAnimator.setInterpolator(this.decelerateInterpolator);
        final ObjectAnimator innerCircleAnimator = ObjectAnimator.ofFloat(this.circleView, CircleView.INNER_CIRCLE_RADIUS_PROGRESS, 0.1f, 1f);
        innerCircleAnimator.setDuration(350);
        innerCircleAnimator.setStartDelay(100);
        innerCircleAnimator.setInterpolator(this.decelerateInterpolator);
        final ObjectAnimator sideCircleAnimator = ObjectAnimator.ofFloat(this.sideCircleView, this.sideCircleView.SIDE_CIRCLE_PROGRESS, 0, 1f);
        sideCircleAnimator.setDuration(600);
        sideCircleAnimator.setStartDelay(300);
        sideCircleAnimator.setInterpolator(this.decelerateInterpolator);
        final ValueAnimator heartColorAnimator = ValueAnimator.ofInt(ORIGINAL_COLOR, FINAL_COLOR);
        heartColorAnimator.setEvaluator(this.argbEvaluator);
        heartColorAnimator.setDuration(500);
        heartColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                heartView.setHeartColor((int) animation.getAnimatedValue());
            }
        });
        final ObjectAnimator heartScaleYAnimator = ObjectAnimator.ofFloat(this.heartView, ImageView.SCALE_Y, 0.2f, 1f);
        heartScaleYAnimator.setDuration(500);
        heartScaleYAnimator.setInterpolator(this.overshootInterpolator);
        final ObjectAnimator heartScaleXAnimator = ObjectAnimator.ofFloat(this.heartView, ImageView.SCALE_X, 0.2f, 1f);
        heartScaleXAnimator.setDuration(500);
        heartScaleXAnimator.setInterpolator(this.overshootInterpolator);
        this.animatorSet = new AnimatorSet();
        this.animatorSet.playTogether(outerCircleAnimator, innerCircleAnimator, heartColorAnimator, heartScaleYAnimator, heartScaleXAnimator, sideCircleAnimator);
        this.animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationCancel(Animator animation) {
                circleView.setInnerCircleRadiusProgress(0);
                circleView.setOuterCircleRadiusProgress(0);
                sideCircleView.setCurrentProgress(0);
            }
        });
      
 
沒有留言:
張貼留言