为了加强对自定义 View 的认知以及开发能力,我计划这段时间陆续来完成几个难度从易到难的自定义 View,并简单的写几篇博客来进行介绍,所有的代码也都会开源,也希望读者能给个 star 哈 GitHub 地址:github.com/leavesC/Cus… 也可以下载 Apk 来体验下: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;@Overrideprotected 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() {@Overridepublic 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() {@Overridepublic void onAnimationEnd(Animator animation) {startUpAnimator();}});upAnimator = new ValueAnimator();//减速上升upAnimator.setInterpolator(new DecelerateInterpolator());upAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic 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() {@Overridepublic void onAnimationEnd(Animator animation) {startDownAnimator();}});}
复制代码
然后绘制出每一个动画值所呈现的画面即可
private Path path = new Path();@Overrideprotected 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();}@Overrideprotected 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);}@Overrideprotected 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();@Overrideprotected 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() {@Overridepublic 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() {@Overridepublic void onAnimationEnd(Animator animation) {startUpAnimator();}});upAnimator = new ValueAnimator();//减速上升upAnimator.setInterpolator(new DecelerateInterpolator());upAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic 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() {@Overridepublic void onAnimationEnd(Animator animation) {startDownAnimator();}});}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();stop();}@Overrideprotected 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();}}}
复制代码