自定義View合輯(8)-跳躍的小球(貝塞爾曲線)

葉志陳發表於2019-05-11

為了加強對自定義 View 的認知以及開發能力,我計劃這段時間陸續來完成幾個難度從易到難的自定義 View,並簡單的寫幾篇部落格來進行介紹,所有的程式碼也都會開源,也希望讀者能給個 star 哈 GitHub 地址:github.com/leavesC/Cus… 也可以下載 Apk 來體驗下:www.pgyer.com/CustomView

先看下效果圖:

自定義View合輯(8)-跳躍的小球(貝塞爾曲線)

一、思路解析

可以看出來這是一個具有“彈性”效果的小球,小球加速下落,減速上升,小球在碰到水平線的時候,水平線會被下壓一定距離,在小球被彈起時,水平線會有一個上下回彈的“黏性”效果

設計這樣一個自定義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();
        }
    }

}
複製程式碼

相關文章