跳动的小球动画

跳动的小球

平时对牛逼动画,高级UI,都深入的不多!近日,某头条,推了一个android技术类视频(平时在头条关注技术比较多),讲的是加载动画效果,是动脑学院讲的公开课,160分钟,我硬是拿着小手机看完了!边看,便记,然后整理了一下,规范了一下代码
做这个动画,需掌握:
1、属性动画
2、Path类、Canvas类
3、贝塞尔曲线
4、SurfaceView用法
5、自定义attr属性
6 、架构: 状态模式,控制器
7 、自由落体,抛物线等概念
不多说了,直接上码

1.DancingView.java

public class DancingView extends SurfaceView implements SurfaceHolder.Callback {

    public static final int STATE_DOWN = 1;//向下状态
    public static final int STATE_UP = 2;//向上状态

    public static final int DEFAULT_POINT_RADIUS = 10;
    public static final int DEFAULT_BALL_RADIUS = 13;
    public static final int DEFAULT_LINE_WIDTH = 200;
    public static final int DEFAULT_LINE_HEIGHT = 2;
    public static final int DEFAULT_LINE_COLOR = Color.parseColor("#FF9800");
    public static final int DEFAULT_POINT_COLOR = Color.parseColor("#9C27B0");
    public static final int DEFAULT_BALL_COLOR = Color.parseColor("#FF4081");

    public static final int DEFAULT_DOWN_DURATION = 600;//ms
    public static final int DEFAULT_UP_DURATION = 600;//ms
    public static final int DEFAULT_FREEDOWN_DURATION = 1000;//ms

    public static final int MAX_OFFSET_Y = 50;//水平下降最大偏移距离


    public int PONIT_RADIUS = DEFAULT_POINT_RADIUS;//小球半径
    public int BALL_RADIUS = DEFAULT_BALL_RADIUS;//小球半径

    private Paint mPaint;
    private Path mPath;
    private int mLineColor;
    private int mPonitColor;
    private int mBallColor;
    private int mLineWidth;
    private int mLineHeight;

    private float mDownDistance;
    private float mUpDistance;
    private float freeBallDistance;

    private ValueAnimator mDownController;//下落控制器
    private ValueAnimator mUpController;//上弹控制器
    private ValueAnimator mFreeDownController;//自由落体控制器

    private AnimatorSet animatorSet;
    private int state;

    private boolean ismUpControllerDied = false;
    private boolean isAnimationShowing = false;
    private boolean isBounced = false;
    private boolean isBallFreeUp = false;

    public DancingView(Context context) {
        super(context);
        init(context, null);
    }

    public DancingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }


    public DancingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }


    private void init(Context context, AttributeSet attrs) {
        initAttributes(context, attrs);

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(mLineHeight);
        mPaint.setStrokeCap(Paint.Cap.ROUND);

        mPath = new Path();
        getHolder().addCallback(this);

        initController();
    }

    private void initAttributes(Context context, AttributeSet attrs) {
        TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.DancingView);
        mLineColor = typeArray.getColor(R.styleable.DancingView_lineColor, DEFAULT_LINE_COLOR);
        mLineWidth = typeArray.getDimensionPixelOffset(R.styleable.DancingView_lineWidth, DEFAULT_LINE_WIDTH);
        mLineHeight = typeArray.getDimensionPixelOffset(R.styleable.DancingView_lineHeight, DEFAULT_LINE_HEIGHT);
        mPonitColor = typeArray.getColor(R.styleable.DancingView_pointColor, DEFAULT_POINT_COLOR);
        mBallColor = typeArray.getColor(R.styleable.DancingView_ballColor, DEFAULT_BALL_COLOR);
        typeArray.recycle();
    }


    private void initController() {
        mDownController = ValueAnimator.ofFloat(0, 1);
        mDownController.setDuration(DEFAULT_DOWN_DURATION);
        mDownController.setInterpolator(new DecelerateInterpolator());
        mDownController.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mDownDistance = MAX_OFFSET_Y * (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        mDownController.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                state = STATE_DOWN;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
            }

            @Override
            public void onAnimationCancel(Animator animation) {
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
            }
        });

        mUpController = ValueAnimator.ofFloat(0, 1);
        mUpController.setDuration(DEFAULT_UP_DURATION);
        mUpController.setInterpolator(new DancingInterpolator());
        mUpController.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mUpDistance = MAX_OFFSET_Y * (float) animation.getAnimatedValue();
                if (mUpDistance >= MAX_OFFSET_Y) {
                    //进入自由落体状态
                    isBounced = true;
                    if (!mFreeDownController.isRunning() && !mFreeDownController.isStarted() && !isBallFreeUp) {
                        mFreeDownController.start();
                    }
                }
                postInvalidate();
            }
        });
        mUpController.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                state = STATE_UP;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                ismUpControllerDied = true;
            }

            @Override
            public void onAnimationCancel(Animator animation) {
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

        mFreeDownController = ValueAnimator.ofFloat(0, 8f);
        mFreeDownController.setDuration(DEFAULT_FREEDOWN_DURATION);
        mFreeDownController.setInterpolator(new DecelerateInterpolator());
        mFreeDownController.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //该公式解决上升减速 和 下降加速
                float t = (float) animation.getAnimatedValue();
                freeBallDistance = 40 * t - 5 * t * t;

                if (ismUpControllerDied) {//往上抛,到临界点
                    postInvalidate();
                }
            }
        });
        mFreeDownController.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                isBallFreeUp = true;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                isAnimationShowing = false;
                //循环第二次
                startAnimations();
            }

            @Override
            public void onAnimationCancel(Animator animation) {
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
            }
        });

        animatorSet = new AnimatorSet();
        animatorSet.play(mDownController).before(mUpController);
        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                isAnimationShowing = true;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
            }

            @Override
            public void onAnimationCancel(Animator animation) {
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
            }
        });
    }

    /**
     * 启动动画,外部调用
     */
    public void startAnimations() {
        if (isAnimationShowing) {
            return;
        }
        if (animatorSet.isRunning()) {
            animatorSet.end();
            animatorSet.cancel();
        }
        isBounced = false;
        isBallFreeUp = false;
        ismUpControllerDied = false;

        animatorSet.start();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);


        // 一条绳子用左右两部分的二阶贝塞尔曲线组成
        mPaint.setColor(mLineColor);
        mPath.reset();
        //起始点
        mPath.moveTo(getWidth() / 2 - mLineWidth / 2, getHeight() / 2);

        if (state == STATE_DOWN) {//下落
            /**************绘制绳子开始*************/
            //左部分 的贝塞尔
            mPath.quadTo((float) (getWidth() / 2 - mLineWidth / 2 + mLineWidth * 0.375), getHeight() / 2 + mDownDistance,
                    getWidth() / 2, getHeight() / 2 + mDownDistance);
            //右部分 的贝塞尔
            mPath.quadTo((float) (getWidth() / 2 + mLineWidth / 2 - mLineWidth * 0.375), getHeight() / 2 + mDownDistance,
                    getWidth() / 2 + mLineWidth / 2, getHeight() / 2);

            mPaint.setStyle(Paint.Style.STROKE);
            canvas.drawPath(mPath, mPaint);
            /**************绘制绳子结束*************/


            /**************绘制弹跳小球开始*************/
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(mBallColor);
            canvas.drawCircle(getWidth() / 2, getHeight() / 2 + mDownDistance - BALL_RADIUS, BALL_RADIUS, mPaint);
            /**************绘制弹跳小球结束*************/
        } else if (state == STATE_UP) { //向上弹
            /**************绘制绳子开始*************/
            //左部分的贝塞尔
            mPath.quadTo((float) (getWidth() / 2 - mLineWidth / 2 + mLineWidth * 0.375), getHeight() / 2 + 50 - mUpDistance,
                    getWidth() / 2,
                    getHeight() / 2 + (50 - mUpDistance));

            //右部分的贝塞尔
            mPath.quadTo((float) (getWidth() / 2 + mLineWidth / 2 - mLineWidth * 0.375), getHeight() / 2 + 50 - mUpDistance,
                    getWidth() / 2 + mLineWidth / 2,
                    getHeight() / 2);

            mPaint.setStyle(Paint.Style.STROKE);
            canvas.drawPath(mPath, mPaint);
            /**************绘制绳子结束*************/

            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(mBallColor);

            //弹性小球,自由落体
            if (!isBounced) {
                //上升
                canvas.drawCircle(getWidth() / 2, getHeight() / 2 + (MAX_OFFSET_Y - mUpDistance) - BALL_RADIUS, BALL_RADIUS, mPaint);
            } else {
                //自由落体
                canvas.drawCircle(getWidth() / 2, getHeight() / 2 - freeBallDistance - BALL_RADIUS, BALL_RADIUS, mPaint);
            }
        }
        mPaint.setColor(mPonitColor);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(getWidth() / 2 - mLineWidth / 2, getHeight() / 2, PONIT_RADIUS, mPaint);
        canvas.drawCircle(getWidth() / 2 + mLineWidth / 2, getHeight() / 2, PONIT_RADIUS, mPaint);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Canvas canvas = holder.lockCanvas();//锁定整个SurfaceView对象,获取该Surface上的Canvas.
        draw(canvas);
        holder.unlockCanvasAndPost(canvas);//释放画布,提交修改
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }
}

2.DancingInterpolator.java

 public class DancingInterpolator implements Interpolator {
    @Override
    public float getInterpolation(float input) {
        return (float) (1 - Math.exp(-3 * input) * Math.cos(10 * input));
    }
}

3.自定义属性 styles.xml

<declare-styleable name="DancingView">
       <attr name="lineWidth" format="dimension" />
       <attr name="lineHeight" format="dimension" />
       <attr name="pointColor" format="reference|color" />
       <attr name="lineColor" format="reference|color" />
       <attr name="ballColor" format="reference|color" />
</declare-styleable>

颜色,尺寸,参数可以自己测试,调整,仅供学习!

--每天进步一点,终会成长为老司机

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

推荐阅读更多精彩内容