为了账号安全,请及时绑定邮箱和手机立即绑定

自定义View合辑(8)-跳跃的小球(贝塞尔曲线)

为了加强对自定义 View 的认知以及开发能力,我计划这段时间陆续来完成几个难度从易到难的自定义 View,并简单的写几篇博客来进行介绍,所有的代码也都会开源,也希望读者能给个 star 哈
GitHub 地址:https://github.com/leavesC/CustomView
也可以下载 Apk 来体验下:https://www.pgyer.com/CustomView

先看下效果图:

一、思路解析

可以看出来这是一个具有“弹性”效果的小球,小球加速下落,减速上升,小球在碰到水平线的时候,水平线会被下压一定距离,在小球被弹起时,水平线会有一个上下回弹的“黏性”效果

设计这样一个自定义View的步骤可以分为以下几步:

  • 绘制一条水平线
  • 在最高点绘制一个红色小球,X坐标居于水平线中间
  • 通过 ValueAnimator 提供的加速插值器 AccelerateInterpolator 来逐渐增大小球的 Y 坐标,使之加速下落
  • 当小球触碰到水平线的同时,通过改变贝塞尔曲线的控制点坐标,使得水平线和小球一直保持接触状态,即绘制出一条符合条件的曲线
  • 当小球落到最低点时,通过减速插值器 DecelerateInterpolator 来逐渐减小小球的 Y 坐标,使之减速上升
  • 当小球被反弹超出水平线一定高度内,水平线依然和小球保持接触
  • 当小球离开水平线后,改变贝塞尔曲线的控制点来绘制出水平线的上下回弹效果

二、代码解析

上述过程中需要一直改变两个点的坐标系,即小球和贝塞尔曲线的控制点

    private static class Point {

        private float x;

        private float y;

        private float radius;

    }

    //小球
    private Point ballPoint;

    //贝塞尔曲线控制点
    private Point controlPoint;

根据View的宽高大小,以一定的比例来计算小球最高点坐标、最低点坐标,水平线的起始点坐标这些参数值

    private float lineY;

    private float lineXLeft;

    private float lineXRight;

    //小球最高点Y坐标
    private float pointYMin;

    @Override
    protected void onSizeChanged(int contentWidth, int contentHeight, int oldW, int oldH) {
        super.onSizeChanged(contentWidth, contentHeight, oldW, oldH);
        lineY = contentHeight * 0.5f;
        lineXLeft = contentWidth * 0.15f;
        lineXRight = contentWidth * 0.85f;

        //小球最低点Y坐标
        float pointYMax = contentHeight * 0.55f;
        pointYMin = contentHeight * 0.22f;

        ballPoint.x = contentWidth * 0.5F;
        ballPoint.radius = 26;
        ballPoint.y = pointYMin;

        controlPoint.x = ballPoint.x;

        long speed = 1800;
        downAnimator.setFloatValues(pointYMin, pointYMax);
        upAnimator.setFloatValues(pointYMax, pointYMin);
        downAnimator.setDuration(speed);
        upAnimator.setDuration((long) (0.8 * speed));
        start();
    }

在 ValueAnimator 中动态改变小球和贝塞尔曲线的控制点这两个点的坐标系

  private void initAnimator() {
        downAnimator = new ValueAnimator();
        //加速下降
        downAnimator.setInterpolator(new AccelerateInterpolator());
        downAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                ballPoint.y = (float) animation.getAnimatedValue();
                if (ballPoint.y + ballPoint.radius <= lineY) {
                    controlPoint.y = lineY;
                } else {
                    controlPoint.y = lineY + 2 * (ballPoint.y + ballPoint.radius - lineY);
                }
                invalidate();
            }
        });
        downAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                startUpAnimator();
            }
        });

        upAnimator = new ValueAnimator();
        //减速上升
        upAnimator.setInterpolator(new DecelerateInterpolator());
        upAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                ballPoint.y = (float) animation.getAnimatedValue();
                if (ballPoint.y + ballPoint.radius >= lineY) { //还处于水平线以下
                    controlPoint.y = lineY + 2 * (ballPoint.y + ballPoint.radius - lineY);
                } else {
                    //小球总的要上升的距离
                    float tempY = lineY - pointYMin;
                    //小球最低点距离水平线的距离,即小球已上升的距离
                    float distance = lineY - ballPoint.y - ballPoint.radius;
                    //上升比例
                    float percentage = distance / tempY;
                    if (percentage <= 0.2) {  //线从水平线升高到最高点
                        controlPoint.y = lineY + 2 * (ballPoint.y + ballPoint.radius - lineY);
                    } else if (percentage <= 0.28) { //线从最高点降落到水平线
                        controlPoint.y = lineY - (distance - tempY * 0.2f);
                    } else if (percentage <= 0.34) { //线从水平线降落到最低点
                        controlPoint.y = lineY + (distance - tempY * 0.28f);
                    } else if (percentage <= 0.39) { //线从最低点升高到水平线
                        controlPoint.y = lineY - (distance - tempY * 0.34f);
                    } else {
                        controlPoint.y = lineY;
                    }
                }
                invalidate();
            }
        });
        upAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                startDownAnimator();
            }
        });
    }

然后绘制出每一个动画值所呈现的画面即可

    private Path path = new Path();

    @Override
    protected void onDraw(Canvas canvas) {
        paint.setColor(Color.WHITE);
        paint.setStrokeWidth(8f);

        path.reset();
        path.moveTo(lineXLeft, lineY);
        path.quadTo(controlPoint.x, controlPoint.y, lineXRight, lineY);
        paint.setStyle(Paint.Style.STROKE);
        canvas.drawPath(path, paint);

        paint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(lineXLeft, lineY, 16, paint);
        canvas.drawCircle(lineXRight, lineY, 16, paint);

        paint.setColor(Color.parseColor("#f7584d"));
        paint.setStrokeWidth(0f);
        canvas.drawCircle(ballPoint.x, ballPoint.y, ballPoint.radius, paint);
    }

总的代码是这样的

/**
 * 作者:leavesC
 * 时间:2019/5/1 23:04
 * 描述:
 * GitHub:https://github.com/leavesC
 * Blog:https://www.jianshu.com/u/9df45b87cfdf
 */
public class PointBeatView extends BaseView {

    private static class Point {

        private float x;

        private float y;

        private float radius;

    }

    //小球
    private Point ballPoint;

    //贝塞尔曲线控制点
    private Point controlPoint;

    private ValueAnimator downAnimator;

    private ValueAnimator upAnimator;

    private float lineY;

    private float lineXLeft;

    private float lineXRight;

    //小球最高点Y坐标
    private float pointYMin;

    private Paint paint;

    public PointBeatView(Context context) {
        this(context, null);
    }

    public PointBeatView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PointBeatView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        ballPoint = new Point();
        controlPoint = new Point();
        initPaint();
        initAnimator();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = getSize(widthMeasureSpec, getResources().getDisplayMetrics().widthPixels);
        int height = getSize(heightMeasureSpec, getResources().getDisplayMetrics().heightPixels);
        setMeasuredDimension(width, height);
    }

    private void initPaint() {
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setDither(true);
    }

    @Override
    protected void onSizeChanged(int contentWidth, int contentHeight, int oldW, int oldH) {
        super.onSizeChanged(contentWidth, contentHeight, oldW, oldH);
        lineY = contentHeight * 0.5f;
        lineXLeft = contentWidth * 0.15f;
        lineXRight = contentWidth * 0.85f;

        //小球最低点Y坐标
        float pointYMax = contentHeight * 0.55f;
        pointYMin = contentHeight * 0.22f;

        ballPoint.x = contentWidth * 0.5F;
        ballPoint.radius = 26;
        ballPoint.y = pointYMin;

        controlPoint.x = ballPoint.x;

        long speed = 1800;
        downAnimator.setFloatValues(pointYMin, pointYMax);
        upAnimator.setFloatValues(pointYMax, pointYMin);
        downAnimator.setDuration(speed);
        upAnimator.setDuration((long) (0.8 * speed));
        start();
    }

    private Path path = new Path();

    @Override
    protected void onDraw(Canvas canvas) {
        paint.setColor(Color.WHITE);
        paint.setStrokeWidth(8f);

        path.reset();
        path.moveTo(lineXLeft, lineY);
        path.quadTo(controlPoint.x, controlPoint.y, lineXRight, lineY);
        paint.setStyle(Paint.Style.STROKE);
        canvas.drawPath(path, paint);

        paint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(lineXLeft, lineY, 16, paint);
        canvas.drawCircle(lineXRight, lineY, 16, paint);

        paint.setColor(Color.parseColor("#f7584d"));
        paint.setStrokeWidth(0f);
        canvas.drawCircle(ballPoint.x, ballPoint.y, ballPoint.radius, paint);
    }

    private void initAnimator() {
        downAnimator = new ValueAnimator();
        //加速下降
        downAnimator.setInterpolator(new AccelerateInterpolator());
        downAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                ballPoint.y = (float) animation.getAnimatedValue();
                if (ballPoint.y + ballPoint.radius <= lineY) {
                    controlPoint.y = lineY;
                } else {
                    controlPoint.y = lineY + 2 * (ballPoint.y + ballPoint.radius - lineY);
                }
                invalidate();
            }
        });
        downAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                startUpAnimator();
            }
        });

        upAnimator = new ValueAnimator();
        //减速上升
        upAnimator.setInterpolator(new DecelerateInterpolator());
        upAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                ballPoint.y = (float) animation.getAnimatedValue();
                if (ballPoint.y + ballPoint.radius >= lineY) { //还处于水平线以下
                    controlPoint.y = lineY + 2 * (ballPoint.y + ballPoint.radius - lineY);
                } else {
                    //小球总的要上升的距离
                    float tempY = lineY - pointYMin;
                    //小球最低点距离水平线的距离,即小球已上升的距离
                    float distance = lineY - ballPoint.y - ballPoint.radius;
                    //上升比例
                    float percentage = distance / tempY;
                    if (percentage <= 0.2) {  //线从水平线升高到最高点
                        controlPoint.y = lineY + 2 * (ballPoint.y + ballPoint.radius - lineY);
                    } else if (percentage <= 0.28) { //线从最高点降落到水平线
                        controlPoint.y = lineY - (distance - tempY * 0.2f);
                    } else if (percentage <= 0.34) { //线从水平线降落到最低点
                        controlPoint.y = lineY + (distance - tempY * 0.28f);
                    } else if (percentage <= 0.39) { //线从最低点升高到水平线
                        controlPoint.y = lineY - (distance - tempY * 0.34f);
                    } else {
                        controlPoint.y = lineY;
                    }
                }
                invalidate();
            }
        });
        upAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                startDownAnimator();
            }
        });
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        stop();
    }

    @Override
    protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
        super.onVisibilityChanged(changedView, visibility);
        switch (visibility) {
            case View.VISIBLE: {
                start();
                break;
            }
            case View.INVISIBLE:
            case View.GONE: {
                stop();
                break;
            }
        }
        Log.e(TAG, "onVisibilityChanged: " + visibility);
    }

    public void start() {
        startDownAnimator();
    }

    public void stop() {
        stopDownAnimator();
        stopUpAnimator();
    }

    private void startDownAnimator() {
        if (downAnimator != null && downAnimator.getValues() != null && downAnimator.getValues().length > 0 && !downAnimator.isRunning()) {
            downAnimator.start();
        }
    }

    private void stopDownAnimator() {
        if (downAnimator != null && downAnimator.isRunning()) {
            downAnimator.cancel();
        }
    }

    private void startUpAnimator() {
        if (upAnimator != null && upAnimator.getValues() != null && upAnimator.getValues().length > 0 && !upAnimator.isRunning()) {
            upAnimator.start();
        }
    }

    private void stopUpAnimator() {
        if (upAnimator != null && upAnimator.isRunning()) {
            upAnimator.cancel();
        }
    }

}
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
34
获赞与收藏
119

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消