自定义 View —— 步数显示器

圆环步数显示器的实现

目录

圆环步数显示器的实现

先说一下绘制的基本步骤:

一、先分析效果组成部分

二、自定义view 的属性

三、在代码中获取自定义属性

四、在 onMeasure() 中 测量大小

五、在 onDraw() 中绘制图形

具体代码如下:

ViewGroup 默认不走 onDraw() 方法(源码)及解决方案


先看一下图片

先说一下绘制的基本步骤:

1、分析效果
2、在 attrs.xml 文件中 自定义属性
3、导入命名空间,在布局中使用自定义属性
4、在代码中获取自定义属性
5、onMeasure()
6、onDraw 绘制 外环、内环、文字
7、其他伴随行为

 

一、先分析效果组成部分

由外圆环、内圆环、以及中显示的步数,三部分组成

二、自定义view 的属性

包括外圆环的 Color、borderWidth,内圆环的 Color,还有步数字体的Size、Color

    <declare-styleable name="StepView">
        <attr name="stepTextSize" format="dimension"/>
        <attr name="stepTxtColor" format="color"/>
        <attr name="outerColor" format="color"/>
        <attr name="innerColor" format="color"/>
        <attr name="borderWidth" format="dimension"/>
    </declare-styleable>

三、在代码中获取自定义属性

        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.StepView);
        mOuterColor = array.getColor(R.styleable.StepView_outerColor, mOuterColor);
        mInnerColor = array.getColor(R.styleable.StepView_innerColor, mInnerColor);
        mBorderWidth = (int) array.getDimension(R.styleable.StepView_borderWidth, mBorderWidth);
        mTextColor = array.getColor(R.styleable.StepView_stepTxtColor, mTextColor);
        mTextSize = array.getDimensionPixelSize(R.styleable.StepView_stepTextSize, mTextSize);
        // 记得先回收
        array.recycle();

四、在 onMeasure() 中 测量大小

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 5、onMeasure()
        // 获取大小
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        // 获取测量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (widthMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.AT_MOST){
            throw new IllegalArgumentException("宽、高请给固定值,这样圆环才不会那么丑");
        }
        // 设定值  取宽高的最小值,保证 View 是正方形
        setMeasuredDimension(width > height ? height : width, width > height ? height : width);
    }

五、在 onDraw() 中绘制图形

我们需要绘制的有三个 外圆环、内圆环、文字

先放两张图

基线 baseLine的计算:

        // baseLine = 中心位 + dy
        // dy = 高度的一半 - bottom
        // FontMetricsInt 中 top 是 baseLine 到最上面的距离 负值
        // bottom 是baseLine 到最下面的距离 正值
        Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
        int dy = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
        int baseLine = getHeight() / 2 + dy;

还有画圆环的角度问题:

圆弧的角度是在 canvas.drawArc() 使用的, 135 是圆弧开始的角度,270 是圆弧顺时针扫描的角度

具体代码:

canvas.drawArc(rectF, 135, 270, false, mOutPaint);

如果我们想绘制一个完整的圆弧,就可以这么写:

canvas.drawArc(rectF, 0, 360, false, mOutPaint);

具体代码如下:

StepView.class,里面有注释的:

public class StepView extends View {

    private int mOuterColor = Color.BLUE;
    private int mInnerColor = Color.RED;
    private int mBorderWidth = 10;
    private int mTextColor = Color.RED;
    private int mTextSize = 10;
    // 画笔
    private Paint mOutPaint, mInnerPaint, mTextPaint;
    // 内环总进度
    private int mMaxProgress = 0;
    // 内环当前进度
    private int mCurrentProgress = 0;

    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);
//         1、分析效果
//         2、在 attrs.xml 文件中 自定义属性
//         3、导入命名空间,在布局中使用自定义属性
//         4、在代码中获取自定义属性
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.StepView);
        mOuterColor = array.getColor(R.styleable.StepView_outerColor, mOuterColor);
        mInnerColor = array.getColor(R.styleable.StepView_innerColor, mInnerColor);
        mBorderWidth = (int) array.getDimension(R.styleable.StepView_borderWidth, mBorderWidth);
        mTextColor = array.getColor(R.styleable.StepView_stepTxtColor, mTextColor);
        mTextSize = array.getDimensionPixelSize(R.styleable.StepView_stepTextSize, mTextSize);
        array.recycle();
//         5、onMeasure()
//         6、onDraw 绘制 外环、内环、文字
//         7、其他伴随行为

        // 外环画笔
        mOutPaint = new Paint();
        mOutPaint.setAntiAlias(true);
        mOutPaint.setColor(mOuterColor);
        mOutPaint.setStrokeWidth(mBorderWidth);
        mOutPaint.setStyle(Paint.Style.STROKE);     // 圆环空心
        mOutPaint.setStrokeCap(Paint.Cap.ROUND);    // 进出口为圆形
        // 内环画笔
        mInnerPaint = new Paint();
        mInnerPaint.setAntiAlias(true);
        mInnerPaint.setColor(mInnerColor);
        mInnerPaint.setStrokeWidth(mBorderWidth);
        mInnerPaint.setStyle(Paint.Style.STROKE);     // 圆环空心
        mInnerPaint.setStrokeCap(Paint.Cap.ROUND);    // 进出口为圆形
        // 文字画笔
        mTextPaint = new Paint();
        mTextPaint.setTextSize(mTextSize);
        mTextPaint.setColor(mTextColor);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 5、onMeasure()
        // 获取大小
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        // 获取测量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (widthMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.AT_MOST){
            throw new IllegalArgumentException("宽、高请给固定值,这样圆环才不会那么丑");
        }
        // 设定值  取宽高的最小值
        setMeasuredDimension(width > height ? height : width, width > height ? height : width);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 6、onDraw 绘制
        // 6.1 画外环
        int center = getWidth() / 2;
        int radius = getWidth() / 2 - mBorderWidth / 2;
        RectF rectF = new RectF(center - radius, center - radius, center + radius, center + radius);
        canvas.drawArc(rectF, 135, 270, false, mOutPaint);
        // 6.2 画内环
        if (mMaxProgress == 0) return;
        // 获取百分比
        float sweepAngle = (float) mCurrentProgress / mMaxProgress;
        canvas.drawArc(rectF, 135, sweepAngle * 270, false, mInnerPaint);
        // 6.3 画文字
        String text = mCurrentProgress + "";
        // 获取 x baseLine 的位置
        Rect textBounds = new Rect();
        mTextPaint.getTextBounds(text, 0, text.length(), textBounds);
        int x = getWidth() / 2 - textBounds.width() / 2;
        Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();
        int dy = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
        int baseLine = getHeight() / 2 + dy;
        canvas.drawText(text,x, baseLine, mTextPaint);
    }

    // 7、其他,让使用者设置最大值、当前进度
    public synchronized void setMaxProgress(int maxProgress){
        mMaxProgress = maxProgress;
    }

    public synchronized void setCurrentProgress(int currentProgress){
        mCurrentProgress = currentProgress;
        // 记得重绘 更新时,不断调用 onDraw
        invalidate();
    }

attrs.xml 

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="StepView">
        <attr name="stepTextSize" format="dimension"/>
        <attr name="stepTxtColor" format="color"/>
        <attr name="outerColor" format="color"/>
        <attr name="innerColor" format="color"/>
        <attr name="borderWidth" format="dimension"/>
    </declare-styleable>
      
</resources>

activity_mian.xml 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">
    
    <com.xy.view.StepView
        android:id="@+id/custom_sv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:outerColor="#2ae0c8"
        app:innerColor="#fe6673"
        app:borderWidth="20dp"
        app:stepTextSize="30sp"
        app:stepTxtColor="#fe6673"/>

</LinearLayout>

MainActivity.java 中:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // 测试代码
        final StepView stepView = findViewById(R.id.custom_sv);
        // 设置当前圆弧最大值
        stepView.setMaxProgress(5000);
        // 属性动画
        ValueAnimator animator = ObjectAnimator.ofFloat(0, 3586);
        animator.setDuration(1500);
        // 设置差值器 当前进度变化速度 由快变慢
        animator.setInterpolator(new DecelerateInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float currentProgress = (float) animation.getAnimatedValue();
                // 不断更新当前圆弧值
                stepView.setCurrentProgress((int) currentProgress);
            }
        });
        // 记得先写开启,不然容易忘
        animator.start();

    }
}

ViewGroup 默认不走 onDraw() 方法(源码)及解决方案

测试的时候,尝试继承 LinearLayout,发现没有绘制图案。LinearLayout 继承于ViewGroup 是不走 onDraw 方法的,在此记录一下原因:

LinearLayout 继承于 ViewGroup, ViewGroup 继承于 View,View 中绘制调用的是  draw(Canvas canvas),我们看一下这个方法

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        ........        

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        .......
    }

我们可以看到 dirtyOpaque 为 false 时,才会调用 onDraw。是否调用 onDraw() 与 dirtyOpaque 有关,而 dirtyOpaque 又与  mPrivateFlags 有关。

mPrivateFlags 的赋值,是在 View  的构造函数 中。

    public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        this(context);

        final TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);

        ......
        
        if (!setScrollContainer && (viewFlagValues&SCROLLBARS_VERTICAL) != 0) {
            setScrollContainer(true);
        }
        // 在这个方法中,为 mPrivateFlags 赋值
        computeOpaqueFlags();
    }

看一下 computeOpaqueFlags,计算 Flag 的值。

    protected void computeOpaqueFlags() {
        // Opaque if:
        //   - Has a background
        //   - Background is opaque
        //   - Doesn't have scrollbars or scrollbars overlay

        if (mBackground != null && mBackground.getOpacity() == PixelFormat.OPAQUE) {
            mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND;
        } else {
            mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;
        }

        final int flags = mViewFlags;
        if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) ||
                (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY ||
                (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_OUTSIDE_OVERLAY) {
            mPrivateFlags |= PFLAG_OPAQUE_SCROLLBARS;
        } else {
            mPrivateFlags &= ~PFLAG_OPAQUE_SCROLLBARS;
        }
    }

现在我们再看一下 ViewGroup 的构造函数:


    public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        
        // ViewGroup 构造函数中调用了 这个方法
        initViewGroup();
        initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
    }
    
    private void initViewGroup() {
        // 这里可以看到 initViewGroup 方法会重新赋值,默认不绘制
        // ViewGroup doesn't draw by default
        if (!debugDraw()) {
            setFlags(WILL_NOT_DRAW, DRAW_MASK);
        }
        .......
    }

可以看到最后因为 setFlags 的原因导致 viewGrop 中的 OnDraw 不可见,默认不绘制。

为什么设置背景时,又调用了 onDraw 方法?

因为 设置背景时,调用了  setBackgroundDrawable,而在 这个方法中,又重新为 flag 赋值,进入了之前的那个 if,调用了 onDraw 方法。

    public void setBackgroundDrawable(Drawable background) {
        // 又重新调用了 计算 flags 的方法
        computeOpaqueFlags();

        if (background == mBackground) {
            return;
        }
        .......
    }

解决方法有三:

1、将 onDraw 改为 dispatchDraw,在 View 中的 draw(Canvas canvas) 方法中可以看到 调用 dispatchDraw 方法没有 if 判断

2、在构造方法中,给自定义 View设置一个透明背景

3、在 View 源码中 搜索 setFlags ,发现在另一个方法中调用了 setFlags,我们可以在构造方法中 调用这个方法,重写为 flags 赋值。setWillNotDraw(false)

public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值