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

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

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 的良心之作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,504评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,898评论 1 300
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,218评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,322评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,693评论 3 290
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,812评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,010评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,747评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,476评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,700评论 2 251
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,190评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,541评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,206评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,129评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,903评论 0 199
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,894评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,748评论 2 274

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,585评论 25 707
  • 这次不是十分钟动画,今天要分享的是PathMeasure的玩法。 首先我们来回顾一下童年吧~~90后满满的记忆 小...
    Anonymous___阅读 7,231评论 47 147
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,325评论 6 30
  • 一波又一波的巨浪涌来,五颜六色的花朵旋转在这舞台,秀丽的彩带飘荡在整个大厅,正中央是一名歌手在放声歌唱。 我是这众...
    九色玲珑阅读 119评论 0 0
  • 其次
    曉劍阅读 94评论 0 1