公司美工突然从网上找了个切换动画,叫我照着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
总结:实际上在这个动画当中,用椭圆绘制的动画应该比贝塞尔曲线绘制的更好看。但是公司需求多变,回头这个动画可能就会被删除,所以暂时不想去改效果了。