Android自定义View仿QQ计步器

自定义计步器

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

自定义一个计步器分析:
1、首先需要画一个固定不动的大圆弧
2、其次需要画一个跟着步数变化的小圆弧
3、最后画圆弧中间的步数显示的文字

实现效果图

在这里插入图片描述

自定义属性

首先我们在values目录下新建attrs.xml文件;
然后在里面自定义属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="StepView">
        <attr name="outColor" format="color"/>
        <attr name="innerColor" format="color"/>
        <attr name="borderWidth" format="dimension"/>
        <attr name="stepTextSize" format="dimension"/>
        <attr name="stepTextColor" format="color"/>
    </declare-styleable>
</resources>

创建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中使用代码:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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">

    <com.px.test.view.StepView
        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" />

</androidx.constraintlayout.widget.ConstraintLayout>

然后是在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();
    }
}

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

完整工程文件下载

下载链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值