公司美工突然從網上找了個切換動畫,叫我照着gif圖給tabbar寫個動畫。看了那個gif半天沒看出什麼頭緒,只要硬着頭皮 用貝塞爾曲線實現了tabbar切換動畫。
先貼個代碼git地址:https://gitee.com/rookieci/TabBarAnimView
再上個效果圖
主要由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
總結:實際上在這個動畫當中,用橢圓繪製的動畫應該比貝塞爾曲線繪製的更好看。但是公司需求多變,回頭這個動畫可能就會被刪除,所以暫時不想去改效果了。