自定義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(); } } }