Android tabbar自定義切換動畫

公司美工突然從網上找了個切換動畫,叫我照着gif圖給tabbar寫個動畫。看了那個gif半天沒看出什麼頭緒,只要硬着頭皮 用貝塞爾曲線實現了tabbar切換動畫。
先貼個代碼git地址https://gitee.com/rookieci/TabBarAnimView

再上個效果圖
tabbar貝塞爾曲線切換動畫
主要由TabBarAnimView繪製而成,使用方式如下

    <com.xcc.viewlibrary.TabBarAnimView
        android:id="@+id/viewTabBarAnim"
        android:layout_width="match_parent"
        android:layout_height="0px"
        android:background="#fff"
        app:tColor="#FE672E"
        app:tMaxDia="40dp"
        app:tMinDia="20dp"
        app:tNumb="5" />

app:tNumb 配置tab數量
app:tColor 配置繪製顏色
app:tMaxDia 配置大圓直徑
app:tMinDia 配置小圓直徑(這兒用於繪製貝塞爾曲線時的定位點)

製作控件時發現 三次貝塞爾曲線會比二次貝塞爾曲線更好看,此處使用的是二次貝塞爾曲線。

以下是TabBarAnimView實現代碼

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="tColor" format="color" />
    <declare-styleable name="TabBarAnimView">
        <attr name="tColor" />
        <attr name="tNumb" format="integer" />
        <attr name="tMinDia" format="dimension" />
        <attr name="tMaxDia" format="dimension" />
    </declare-styleable>
</resources>

TabBarAnimView.java

package com.xcc.viewlibrary;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.OvershootInterpolator;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

/**
 * 用於TabBar背景動畫
 */
public class TabBarAnimView extends View {
    public TabBarAnimView(Context context) {
        super(context);
        init(context, null);
    }

    public TabBarAnimView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public TabBarAnimView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TabBarAnimView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs);
    }

    protected void init(Context context, AttributeSet attrs) {
        if (attrs != null) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabBarAnimView);
            color = a.getColor(R.styleable.TabBarAnimView_tColor, 0xFF000000);
            tabNumb = a.getInt(R.styleable.TabBarAnimView_tNumb, 4);
            maxDia = a.getDimension(R.styleable.TabBarAnimView_tMaxDia, 4);
            minDia = a.getDimension(R.styleable.TabBarAnimView_tMinDia, 4);
            a.recycle();
        }
        addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
            public void onViewAttachedToWindow(View v) {
                isViewAttachedToWindow = true;
            }

            public void onViewDetachedFromWindow(View v) {
                isViewAttachedToWindow = false;
            }
        });
        currentIndex = -1;
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(color);
        if (BuildConfig.DEBUG) paint.setStrokeWidth(3);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!isViewAttachedToWindow) return;
        if (getWidth() == 0) return;
        if (type == 1) {
            onFirstDraw(canvas);
        } else if (type == 2) onMoveAnim(canvas);
    }

    private void onFirstDraw(Canvas canvas) {
        canvas.drawCircle(getCircleX(currentIndex), getCircleY(), cDia / 2, paint);
    }

    private void onMoveAnim(Canvas canvas) {
        final int i = stepTime / 3;
        float currentCircleX = getCircleX(currentIndex);//起始位置
        float selectCircleX = getCircleX(selectIndex);//目標位置
        boolean toRight = selectCircleX > currentCircleX;//向右 -->
        float circleY = getCircleY();
        int runTime = i << 1;
        if (currStepTime < runTime) {//階段一
            float circleX = (selectCircleX - currentCircleX) * currStepTime / runTime + currentCircleX;
            canvas.drawCircle(circleX, circleY, maxDia / 2, paint);//繪製大圓

            Path path = new Path();
            path.moveTo(circleX, circleY - maxDia / 2);//大圓上部定點

            //計算小圓位置
            float cDist = currentCircleX - circleX;//圓心距離
            float abs = Math.abs(cDist);
            float minCX;//小圓x
            if (abs < maxDia) {//距離小於直徑
                minCX = currentCircleX;
            } else minCX = circleX + (toRight ? -maxDia : maxDia);
            //canvas.drawCircle(minCX, circleY, minDia / 2, paint);//用於定位的小圓
            if (toRight) {
                minCX -= minDia;
            } else minCX += minDia;
            //二次貝塞爾曲線不好看
            /*path.quadTo(minCX, circleY, circleX, circleY + maxDia / 2);*/
            path.cubicTo(minCX, circleY - minDia / 2
                    , minCX, circleY + minDia / 2
                    , circleX, circleY + maxDia / 2);//三次
            path.close();
            canvas.drawPath(path, paint);
        } else {//階段二
            canvas.drawCircle(selectCircleX, circleY, maxDia / 2, paint);//繪製大圓

            Path path = new Path();
            path.moveTo(selectCircleX, circleY - maxDia / 2);//大圓上部定點
            int overTime = stepTime - currStepTime;
            float v = maxDia * overTime / i;
            float minCX = selectCircleX + (toRight ? -v : v);//小圓x
            //canvas.drawCircle(minCX, circleY, minDia / 2, paint);
            if (toRight) {
                minCX -= minDia;
            } else minCX += minDia;
            /*path.quadTo(minCX, circleY, circleX, circleY + maxDia / 2);*/
            path.cubicTo(minCX, circleY - minDia / 2
                    , minCX, circleY + minDia / 2
                    , selectCircleX, circleY + maxDia / 2);//三次
            path.close();
            canvas.drawPath(path, paint);
        }
    }

    private boolean isViewAttachedToWindow;
    private int tabNumb;//tab數量
    private int color;
    private float minDia; //小圓直徑
    private float maxDia;//大圓直徑
    private int currentIndex;//當前選中 -1表示沒有選中
    private int selectIndex;//選中 從0開始
    private int waitSelectIndex;//等待選中
    private Paint paint;
    private int type;//繪製模式 1開始位置的繪製
    private float cDia;
    private static final int stepTime = 480;//繪製步進時長 分三個階段
    private int currStepTime;//當前步進時長
    private boolean runAnim;//執行動畫中

    /**
     * @param index 選中哦index
     */
    private float getCircleX(int index) {
        float tabW = getWidth() * 1.0f / tabNumb;
        return tabW * (index + 0.5f);
    }

    //獲取高度一半作爲圓中心點
    private float getCircleY() {
        return getHeight() / 2.0f;
    }

    public void setSelectIndex(int selectIndex) {
        if (currentIndex == -1) {
            currentIndex = selectIndex;
            this.selectIndex = selectIndex;
            openFirstAnim();
        } else {
            this.waitSelectIndex = selectIndex;
            if (runAnim) return;
            if (currentIndex == selectIndex) {
                openFirstAnim();
                return;
            }
            runAnim = true;
            this.selectIndex = selectIndex;
            openMoveAnim();
        }
    }

    private void openFirstAnim() {
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(minDia, maxDia);
        valueAnimator.addUpdateListener(animation -> {
            type = 1;
            cDia = (Float) animation.getAnimatedValue();
            if (!isViewAttachedToWindow) return;
            invalidate();
        });
        valueAnimator.setInterpolator(new OvershootInterpolator());//回彈
        valueAnimator.setDuration(500);
        valueAnimator.start();
    }

    private void openMoveAnim() {
        //計算位置
        ValueAnimator valueAnimator = ValueAnimator.ofInt(0, stepTime);
        valueAnimator.addUpdateListener(animation -> {
            type = 2;
            currStepTime = (Integer) animation.getAnimatedValue();
            if (!isViewAttachedToWindow) return;
            invalidate();
        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            public void onAnimationEnd(Animator animation) {
                currentIndex = selectIndex;
                runAnim = false;
                startWaitSelect();
            }
        });
        valueAnimator.setDuration(stepTime);
        valueAnimator.start();
    }

    private void startWaitSelect() {
        if (waitSelectIndex == currentIndex) return;
        setSelectIndex(waitSelectIndex);
    }
}

最後再貼出代碼git地址https://gitee.com/rookieci/TabBarAnimView

總結:實際上在這個動畫當中,用橢圓繪製的動畫應該比貝塞爾曲線繪製的更好看。但是公司需求多變,回頭這個動畫可能就會被刪除,所以暫時不想去改效果了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章