爲了增強對自定義 View 的認知以及開發能力,我計劃這段時間陸續來完成幾個難度從易到難的自定義 View,並簡單的寫幾篇博客來進行介紹,全部的代碼也都會開源,也但願讀者能給個 star 哈 GitHub 地址:github.com/leavesC/Cus… 也能夠下載 Apk 來體驗下:www.pgyer.com/CustomViewjava
先看下效果圖:git
能夠看出來這是一個具備「彈性」效果的小球,小球加速下落,減速上升,小球在碰到水平線的時候,水平線會被下壓必定距離,在小球被彈起時,水平線會有一個上下回彈的「黏性」效果github
設計這樣一個自定義View的步驟能夠分爲如下幾步:canvas
上述過程當中須要一直改變兩個點的座標系,即小球和貝塞爾曲線的控制點ide
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 中動態改變小球和貝塞爾曲線的控制點這兩個點的座標系this
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();
}
});
}
複製代碼
而後繪製出每個動畫值所呈現的畫面便可spa
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();
}
}
}
複製代碼