哈哈哈,十分钟动画又来了~~
照惯例,先上图吧
看起来不太好看,主要是因为原图设计不给版权,不过SwitchBar这个控件的 ui 效果是一毛一样的。
这个控件眨看不太好实现,但是分析出思路之后,其实只需要10分钟。
这是我简书上一个小伙伴私聊我给的需求,找我帮忙实现。当然,也不是无偿帮助,给我发了几个小红包,哈哈哈哈~~
如果有小伙伴遇到棘手的动画可以找我哦。如果不需要我写demo,尬聊不要钱,我知道动画的实现方式或者想到什么动画的思路也会告诉你的。
然后,在开始之前,我想说一下,其实这一期的动画很简单,但凡看了扔物线 HenCoder自定义 view 系列的博客,然后再跟着写了课后作业的人都能写出来。这个给扔物线大神打一个广告,就当交了学费吧。哈哈哈哈
好了,不废话了,开始分析动画~~
动画拆解
首先,我拿到的需求是一个 gif 图,然后看到的就是如上图所示。看不出什么嘛,没有设计给动画轨迹的实现是很扯淡的。
然后怎么办,一帧一帧的看 gif 的动画过程,就像酱紫
看得出,图中应该是一个圆在按照一定的轨迹移动、然后在正中间的时候变个颜色。
只绘制圆角矩阵以及圆交在圆角矩阵上的部分,动画就完成了。
然后我们的问题来了:
1.怎么只绘制绘制圆角矩阵以及圆交在圆角矩阵上的部分。
2.怎么让圆在一个固定的path 上移动
解决了这两个问题,我们就只需要细条一下各类参数达到 UI 设计的效果即可。
解决问题1:
看过扔物线自定义 view 教学的小伙伴都知道。canvas.clip***** 系列方法可以指定 canvas 的绘制区域。
我们这里的圆角矩形边框里面(含边框)就是我们需要裁剪绘制的区域,但是,canvas.clip系列的方法中没有裁剪圆角矩形方法,于是只能通过 canva.clipPath 来实现。至于 path 怎么绘制一个圆角矩形,同学们还是移步扔物线的博客吧。免费的,不会的同学一定要去学。
解决问题2:
这个问题一开始我也不知道具体怎么弄,只知道 Path 可以实现。然后我在群里发了个小红包问了一下,怎么让一个 View 沿着一个 Path 位移。3分钟不到,就有小伙伴告诉我,PathMeasure 可以解决你的问题,并且反手甩了一篇博客给我。
好了,问题解决了。要开始动手写代码了。
源码
public class SwitchBar extends View {
private static final String TAG = "SwitchBar";
private static final long DEFAULT_DURATION = 5000;
private Paint mPaint;//主要画笔
private TextPaint mTextPaint;//文字画笔
private RectF mRectF;//圆角矩阵
private float mOverlayRadius;//覆盖物半径
private Path mClipPath;//裁剪区域
private float[] mCurrentPosition = new float[2];//遮盖物的坐标点
boolean misLeft = true;//tab选中位置
private boolean isAnimation;//是否正在切换条目中
private float mTotalleft;//view的left
private float mTotalTop;//view的top
private float mTotalRight;//view的right
private float mTotalBottom;//view的bottom
private float mTotalHeight;//bottom-top
private int mBaseLineY;//文字剧中线条
private String[] mText = {"1P", "2P"};//tab 文字内容
private OnClickListener mOnClickListener;
private int colorRed = Color.rgb(0xff,0x21,0x10);
private int colorPurple = Color.rgb(0x88,0x88,0xff);
public SwitchBar(Context context) {
this(context, null);
}
public SwitchBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SwitchBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/*
*
* 设置tab文字
* @param text 文字内容
*
* */
public void setText(String[] text) {
mText = text;
invalidate();
}
/*
*
* 设置tab文字的size
* @param size 文字大小
*
* */
public void setTestSize(int size) {
mTextPaint.setTextSize(size);
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
float top = fontMetrics.top;//为基线到字体上边框的距离,即上图中的top
float bottom = fontMetrics.bottom;//为基线到字体下边框的距离,即上图中的bottom
mBaseLineY = (int) (getHeight() / 2 - top / 2 - bottom / 2);
invalidate();
}
/*
*
* 切换条目 动画默认500ms
* @param isLeft true为左边的条目
*
* */
public void switchButton(boolean isLeft) {
switchB(isLeft, DEFAULT_DURATION);
}
public void switchButton(boolean isLeft, long duration) {
switchB(isLeft, duration);
}
/*
*
* 添加tab切换监听
*
* */
public void setOnClickListener(@Nullable OnClickListener listener) {
mOnClickListener = listener;
}
private void init() {
mPaint = new Paint();
mPaint.setStrokeWidth(10);
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setAntiAlias(true);
mTextPaint = new TextPaint();
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(48);
mTextPaint.setTypeface(Typeface.SERIF);
mTextPaint.setFakeBoldText(true);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = width / 3;
mTotalHeight = height - 10;
mTotalleft = 5;
mTotalTop = 5;
mTotalRight = width - 5;
mTotalBottom = height - 5;
mRectF = new RectF(mTotalleft, mTotalTop, mTotalRight, mTotalBottom);
RectF f = new RectF(mTotalleft + mTotalHeight / 2, mTotalTop - 5, mTotalRight - mTotalHeight / 2, mTotalBottom + 5);
mOverlayRadius = (mTotalRight - mTotalleft) * 0.36F;
mClipPath = new Path();
mClipPath.setFillType(Path.FillType.WINDING);
mClipPath.addRect(f, Path.Direction.CW);
mClipPath.addCircle(mTotalleft + mTotalHeight / 2
, mTotalTop + mTotalHeight / 2
, mTotalHeight / 2 + 6
, Path.Direction.CW);
mClipPath.addCircle(mTotalRight - mTotalHeight / 2
, mTotalTop + mTotalHeight / 2
, mTotalHeight / 2 + 6
, Path.Direction.CW);
mCurrentPosition = new float[2];
mCurrentPosition[0] = mTotalleft + mTotalHeight / 2 + 30;
mCurrentPosition[1] = mTotalBottom;
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
float top = fontMetrics.top;//为基线到字体上边框的距离,即上图中的top
float bottom = fontMetrics.bottom;//为基线到字体下边框的距离,即上图中的bottom
mBaseLineY = (int) (height / 2 - top / 2 - bottom / 2);
setMeasuredDimension(width, height);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (mOnClickListener != null) {
mOnClickListener.onClick(event.getX() > getWidth() / 2 ? 1 : 0
, mText[event.getX() > getWidth() / 2 ? 1 : 0]);
}
switchButton(event.getX() < getWidth() / 2);
return true;
}
return super.onTouchEvent(event);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawStroke(canvas);
drawOverlay(canvas);
drawText(canvas);
}
private void drawText(Canvas canvas) {
canvas.drawText(mText[0], getWidth() / 4, mBaseLineY, mTextPaint);
canvas.drawText(mText[1], getWidth() / 4 * 3, mBaseLineY, mTextPaint);
}
private void drawOverlay(Canvas canvas) {
mPaint.setColor(mCurrentPosition[0]>getWidth()/2?colorPurple:colorRed);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setStrokeWidth(1);
canvas.save();
canvas.clipPath(mClipPath);
canvas.drawCircle(mCurrentPosition[0], mCurrentPosition[1], mOverlayRadius, mPaint);
canvas.restore();
}
private void drawStroke(Canvas canvas) {
mPaint.setStrokeWidth(10);
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawRoundRect(mRectF, 1000, 1000, mPaint);
}
private void switchB(boolean isLeft, long duration) {
if (misLeft == isLeft || isAnimation)
return;
Path overlayPath = new Path();
RectF rectF = new RectF(mTotalleft + mTotalHeight / 2 + 30, mTotalBottom - mOverlayRadius, mTotalRight - mTotalHeight / 2 - 30, mTotalBottom + mOverlayRadius);
if (isLeft) {
overlayPath.addArc(rectF, 0, 180);//右到左
} else {
overlayPath.addArc(rectF, 180, -180);//左到右
}
PathMeasure pathMeasure = new PathMeasure(overlayPath, false);
startPathAnim(pathMeasure, duration);
}
private void startPathAnim(final PathMeasure pathMeasure, long duration) {
// 0 - getLength()
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, pathMeasure.getLength());
valueAnimator.setDuration(duration);
// 减速插值器
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue();
// 获取当前点坐标封装到mCurrentPosition
pathMeasure.getPosTan(value, mCurrentPosition, null);
postInvalidate();
}
});
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
isAnimation = true;
}
@Override
public void onAnimationEnd(Animator animation) {
misLeft = !misLeft;
isAnimation = false;
}
@Override
public void onAnimationCancel(Animator animation) {
isAnimation = false;
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
valueAnimator.start();
}
public interface OnClickListener {
void onClick(int position, String text);
}}
//踏马的,上面这个大括号换行就能不进代码格式区域,好气啊
源码中注释已经很清楚了,没有什么难点。代码大家都看得懂,而且代码也可以直接 copy 运行。
可能有些同学还是一头雾水,我这里为了便于大家理解,把运动轨迹也都绘制出来了,这一下相信大家都看得懂了。
还看不懂?那给你一个放慢10倍的轨迹运动
哈哈,很简单吧。反正我是觉得讲清楚了,代码里面注释也都有。如果有看了分析,然后再读过代码还是没懂的同学,欢迎留言提问。
有什么改进的建议也可以留言哦,我尽量听进去。哈哈~~
下期预告:
小时候很多童鞋都看过光能使者吧,没记错的话,我小学的时候在数学书上画了一个光能使者阵,然后被家长打了一顿。。。。。不说题外话了,先回顾一下光能使者阵吧~
实现效果:
很酷炫啊,有木有。这次的光能使者阵是教我用 PathMeasure 那个小伙伴的原创,主要的实现也是基于 PathMeasure 的 APi 实现的。
学会了这个,像 SearchView、NavigationView 的箭头在打开 DrawerLayout 之后变成三条横线、路径动画等等~~~
最后,还是宣传一下凯哥的 HenCoder 吧,学习自定义 View 的良心之作。