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

作者: Anonymous___ | 来源:发表于2017-08-22 16:07 被阅读1350次

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

    SwitchBar.gif

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

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

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

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

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

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

    动画拆解

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

    然后怎么办,一帧一帧的看 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

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

    graphic5s.gif

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

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


    下期预告:

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

    magic_circle.jpg

    实现效果:

    magic_circle1.gif

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

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

    相关文章

      网友评论

      • 花季浅忆:大佬,感觉你好厉害啊,什么都会,我想问一下你自定义view是怎么学的,感觉你学会的很好,什么都能做出来,请教一下学习方法,谢谢
        Anonymous___:@JerryCao2017 HenCoder了解一下
      • swensun:后面的光能使者可以给个github链接吗?
        Anonymous___:@swensun 直接把类copy 出来即可
      • Moo_Night:前排关注:smiley:

      本文标题:十分钟搞定酷炫动画,定制 SwitchBar

      本文链接:https://www.haomeiwen.com/subject/qkyldxtx.html