自定义计步器

Android自定义View是Android开发中比较重要的一项,也是很多开发者比较怕的一个东西。其实只要认真去学习,自定义View其实没有那么可怕;相反的,我们还能从自定义View中找到很多乐趣。

自定义一个计步器分析:

1、首先需要画一个固定不动的大圆弧

2、其次需要画一个跟着步数变化的小圆弧

3、最后画圆弧中间的步数显示的文字

实现效果图

android计步器界面 安卓简单计步器开发_android

自定义属性

首先我们在values目录下新建attrs.xml文件;

然后在里面自定义属性:

创建StepView

创建我们的StepView,继承自View并重写构造方法。

public class StepView extends View {
public StepView(Context context) {
this(context, null);
}
public StepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public StepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}

这里在前两个构造方法中调this,保证使用每个构造方法初始化都能走到第三个构造方法。

在View中获取属性

创建好View,重写构造方法以后就要在View中获取我们的自定义属性:

//获取自定义属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.StepView);
mOutColor = array.getColor(R.styleable.StepView_outColor, Color.GREEN);
mInnerColor = array.getColor(R.styleable.StepView_innerColor, Color.BLUE);
mBorderWidth = (int) array.getDimension(R.styleable.StepView_borderWidth, 10);
mStepTextSize = array.getDimensionPixelSize(R.styleable.StepView_stepTextSize, 20);
mStepTextColor = array.getColor(R.styleable.StepView_stepTextColor, Color.RED);
array.recycle();

重写onMeasure()方法

然后需要重写我们的onMeasure方法,这个方法是测量并设置View的宽高,因为我们这个View比较简单,只需要保证是宽高一致,所以只需要获取我们的宽高,然后设置就行。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(Math.min(width, height), Math.min(width, height));
}

重写onDraw()方法

onDraw方法就是绘制View的内容,这里我们要分三步:

1、绘制大圆弧

首先绘制固定的大圆弧:

//画外圆弧
int center = getWidth() / 2;
int radius = getWidth() / 2 - mBorderWidth;
int left = center - radius;
int top = center - radius;
int right = center + radius;
int bottom = center + radius;
mRectF.set(left, top, right, bottom);
canvas.drawArc(mRectF, START_ANGLE, TOTAL_ANGLE, false, mPaintOuter);

首先要定义一个矩形区域mRectF,然后设置四个边界值。center就是圆心位置,radius就是半径,这里要减去圆弧的宽度。

然后调用drawArc(),这个方法主要有四个参数:

oval : 生成椭圆的矩形

startAngle : 弧开始的角度 (X轴正方向为0度,顺时针弧度增大)

sweepAngle : 绘制多少弧度 (注意不是结束弧度)

useCenter : 是否有弧的两边 true有两边 false无两边

2、绘制小圆弧

小圆弧是会跟着当前的步数动态变化的,所以我们要根据步数计算:

//画内圆弧
float sweepAngle = (float) mCurrentStepCount / mMaxStepCount;
canvas.drawArc(mRectF, START_ANGLE, sweepAngle * TOTAL_ANGLE, false, mPaintInner);

计算完成同样是调用drawArc()绘制圆弧。

3、绘制文字

接下来就是绘制显示在圆弧中心的文字就是我们的当前步数:

//画文字
if (mCurrentStepCount == 0) {
return;
}
String currentStepText = String.valueOf(mCurrentStepCount);
mPaintText.getTextBounds(currentStepText, 0, currentStepText.length(), mBounds);
int startX = getWidth() / 2 - mBounds.width() / 2;
Paint.FontMetricsInt fm = mPaintText.getFontMetricsInt();
int dy = (fm.bottom - fm.top) / 2 - fm.descent;
int baseline = getHeight() / 2 + dy;
canvas.drawText(currentStepText, startX, baseline, mPaintText);

首先根据画笔去测量文字的边界,然后计算开始的x坐标和baseline,最后调用drawText()绘制;

这里不知道怎么计算的可以转到另一篇博客,怎么绘制真正的居中文本,里面有详细介绍计算推理过程。

在xml中使用

最后就是在xml中使用我们的自定义View,要让它动起来,还需要借助我们的属性动画,动态去改变当前的步数值,然后去重绘。

这里贴一下xml中使用代码:

xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
android:id="@+id/stepView"
android:layout_width="120dp"
android:layout_height="120dp"
app:outColor="@color/design_default_color_secondary"
app:innerColor="@color/design_default_color_primary"
app:stepTextColor="@color/black"
app:stepTextSize="16sp"
app:borderWidth="6dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

然后是在activity使用:

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
StepView stepView = findViewById(R.id.stepView);
stepView.setMaxStep(3000);
ValueAnimator valueAnimator = ObjectAnimator.ofFloat(0,1800);
valueAnimator.setDuration(2000);
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(animation -> {
float animatedValue = (float) animation.getAnimatedValue();
stepView.setStep((int) animatedValue);
});
valueAnimator.start();
}
}

StepView类全部代码

package com.px.test.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import com.px.test.R;
import androidx.annotation.Nullable;
public class StepView extends View {
private static final String TAG = "StepView";
private static final int START_ANGLE = 135;
private static final int TOTAL_ANGLE = 270;
private int mOutColor;
private int mInnerColor;
private int mBorderWidth;
private int mStepTextSize;
private int mStepTextColor;
private Paint mPaintOuter;
private RectF mRectF;
private Paint mPaintInner;
private Paint mPaintText;
private Rect mBounds;
private int mMaxStepCount;
private int mCurrentStepCount;
public StepView(Context context) {
this(context, null);
}
public StepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public StepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取自定义属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.StepView);
mOutColor = array.getColor(R.styleable.StepView_outColor, Color.GREEN);
mInnerColor = array.getColor(R.styleable.StepView_innerColor, Color.BLUE);
mBorderWidth = (int) array.getDimension(R.styleable.StepView_borderWidth, 10);
mStepTextSize = array.getDimensionPixelSize(R.styleable.StepView_stepTextSize, 20);
mStepTextColor = array.getColor(R.styleable.StepView_stepTextColor, Color.RED);
array.recycle();
mPaintOuter = new Paint();
mPaintOuter.setAntiAlias(true);
mPaintOuter.setColor(mOutColor);
mPaintOuter.setStrokeWidth(mBorderWidth);
mPaintOuter.setStrokeCap(Paint.Cap.ROUND);
mPaintOuter.setStyle(Paint.Style.STROKE);
mPaintInner = new Paint();
mPaintInner.setAntiAlias(true);
mPaintInner.setColor(mInnerColor);
mPaintInner.setStrokeWidth(mBorderWidth);
mPaintInner.setStrokeCap(Paint.Cap.ROUND);
mPaintInner.setStyle(Paint.Style.STROKE);
mPaintText = new Paint();
mPaintText.setAntiAlias(true);
mPaintText.setColor(mStepTextColor);
mPaintText.setTextSize(mStepTextSize);
mBounds = new Rect();
mRectF = new RectF();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(Math.min(width, height), Math.min(width, height));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画外圆弧
int center = getWidth() / 2;
int radius = getWidth() / 2 - mBorderWidth;
int left = center - radius;
int top = center - radius;
int right = center + radius;
int bottom = center + radius;
mRectF.set(left, top, right, bottom);
canvas.drawArc(mRectF, START_ANGLE, TOTAL_ANGLE, false, mPaintOuter);
//画内圆弧
float sweepAngle = (float) mCurrentStepCount / mMaxStepCount;
canvas.drawArc(mRectF, START_ANGLE, sweepAngle * TOTAL_ANGLE, false, mPaintInner);
//画文字
if (mCurrentStepCount == 0) {
return;
}
String currentStepText = String.valueOf(mCurrentStepCount);
mPaintText.getTextBounds(currentStepText, 0, currentStepText.length(), mBounds);
int startX = getWidth() / 2 - mBounds.width() / 2;
Paint.FontMetricsInt fm = mPaintText.getFontMetricsInt();
int dy = (fm.bottom - fm.top) / 2 - fm.descent;
int baseline = getHeight() / 2 + dy;
canvas.drawText(currentStepText, startX, baseline, mPaintText);
}
public void setMaxStep(int maxStepCount) {
mMaxStepCount = maxStepCount;
}
public void setStep(int currentStepCount) {
mCurrentStepCount = currentStepCount;
invalidate();
}
}

整个实现过程并不复杂,有时候有些事情并没有我们想的那么难,只要我们去做了,就会发现其实就那样。

完整工程文件下载