十分钟搞定酷炫动画,定制 SwitchBar

1,878 阅读6分钟

哈哈哈,十分钟动画又来了~~ 照惯例,先上图吧

SwitchBar.gif
SwitchBar.gif

看起来不太好看,主要是因为原图设计不给版权,不过SwitchBar这个控件的 ui 效果是一毛一样的。

这个控件眨看不太好实现,但是分析出思路之后,其实只需要10分钟。

这是我简书上一个小伙伴私聊我给的需求,找我帮忙实现。当然,也不是无偿帮助,给我发了几个小红包,哈哈哈哈~~

如果有小伙伴遇到棘手的动画可以找我哦。如果不需要我写demo,尬聊不要钱,我知道动画的实现方式或者想到什么动画的思路也会告诉你的。

然后,在开始之前,我想说一下,其实这一期的动画很简单,但凡看了扔物线 HenCoder自定义 view 系列的博客,然后再跟着写了课后作业的人都能写出来。这个给扔物线大神打一个广告,就当交了学费吧。哈哈哈哈

好了,不废话了,开始分析动画~~

动画拆解

首先,我拿到的需求是一个 gif 图,然后看到的就是如上图所示。看不出什么嘛,没有设计给动画轨迹的实现是很扯淡的。

然后怎么办,一帧一帧的看 gif 的动画过程,就像酱紫

slow.gif
slow.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 运行。

可能有些同学还是一头雾水,我这里为了便于大家理解,把运动轨迹也都绘制出来了,这一下相信大家都看得懂了。

graphic.gif
graphic.gif

还看不懂?那给你一个放慢10倍的轨迹运动

graphic5s.gif
graphic5s.gif

哈哈,很简单吧。反正我是觉得讲清楚了,代码里面注释也都有。如果有看了分析,然后再读过代码还是没懂的同学,欢迎留言提问。

有什么改进的建议也可以留言哦,我尽量听进去。哈哈~~


下期预告:

小时候很多童鞋都看过光能使者吧,没记错的话,我小学的时候在数学书上画了一个光能使者阵,然后被家长打了一顿。。。。。不说题外话了,先回顾一下光能使者阵吧~

magic_circle.jpg
magic_circle.jpg

实现效果:

magic_circle1.gif
magic_circle1.gif

很酷炫啊,有木有。这次的光能使者阵是教我用 PathMeasure 那个小伙伴的原创,主要的实现也是基于 PathMeasure 的 APi 实现的。 学会了这个,像 SearchView、NavigationView 的箭头在打开 DrawerLayout 之后变成三条横线、路径动画等等~~~

最后,还是宣传一下凯哥的 HenCoder 吧,学习自定义 View 的良心之作。