美文网首页Android自定义ViewAndroid 进阶之路Android开发经验谈
自定义View合辑(8)-跳跃的小球(贝塞尔曲线)

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

作者: 业志陈 | 来源:发表于2019-05-11 22:46 被阅读12次

    为了加强对自定义 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();
            }
        }
    
    }
    

    相关文章

      网友评论

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

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