自定义View(3)仿QQ运动步数进度

前面两篇讲解了自定义TextView,这篇将以之前的为基础,自定义View来仿QQ运动进度效果,如效果图显示。画一个缺的圆再内部嵌套一个缺的圆,然后将自定义TextView放在图形中间显示。

请添加图片描述

1. 仿QQ运动进度效果思路

思路:先画一个外圈表示最多能有多少步,线有宽度,填充为透明,且从135°画到270°即可。再画一个内圈表示当前走了多少步,与外圈同理,因为内圈是后画的,刚好可以覆盖在外圈上。最后加上步数文字即可。
在这里插入图片描述

        // 1 分析效果
        // 2 确定自定义属性,编写attrs.xml
        // 3 在布局中使用
        // 4 在自定义View中获取自定义属性
        // 5 onMeasure
        // 6 画外圆弧、圆弧、文字
            // 6.1 画外圆弧
            // 6.2 画内圆弧
            // 6.3 画文本
        // 7 其他

2. 仿QQ运动进度效果实现

2.1 自定义View

这里的自定义View与之前自定义的TextView方式一样。
自定义属性attrs:

<!-- /CustomView/View2/app/src/main/res/values/attrs.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="QQStepView">
        <attr name="outerColor" format="color"/>    
        <attr name="innerColor" format="color"/>
        <attr name="borderWidget" format="dimension"/>
        <attr name="stepText" format="string"/>
        <attr name="stepTextSize" format="dimension"/>
        <attr name="stepTextColor" format="color"/>
    </declare-styleable>

</resources>

新建QQStepView继承View,重写他的三个构造方法:

// /CustomView/View2/app/src/main/java/com/example/view2/QQStepView.java
public class QQStepView extends View {
    private Paint mOuterPaint;  // 总进度条画笔
    private int mOuterColor = Color.RED;  // 总进度条颜色,并设置默认
    private int mMaxStep = 2400;   // 最大步数
    private int mStartAngle = 135;  // 弧度开始角度
    private int mEndAngle = 270;   // 弧度结束角度

    private Paint mInnerPaint;  // 已走画笔
    private int mInnerColor = Color.BLUE;  // 已走颜色,并设置默认
    private int mStep = 1000;    // 设置已走步数

    private float mBorderWidget = 20;  // 默认圆弧宽度

    private Paint mTextPaint;  // 文字画笔
    private String mStepText;   // 已走步数
    private int mTextColor = Color.GREEN;  // 字体颜色
    private int mTextSize = 20;  // 默认字体大小
    
    private Rect mTextBounds;

    // 不管调用哪个构造方法,都能进入第三个构造方法
    public QQStepView(Context context) {
        this(context, null);
    }

    public QQStepView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public QQStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 1 分析效果
        // 2 确定自定义属性,编写attrs.xml
        // 3 在布局中使用
        // 4 在自定义View中获取自定义属性
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.QQStepView);
        mStepText = ta.getString(R.styleable.QQStepView_stepText);
        mOuterColor = ta.getColor(R.styleable.QQStepView_outerColor, mOuterColor);
        mInnerColor = ta.getColor(R.styleable.QQStepView_innerColor, mInnerColor);
        mBorderWidget = ta.getDimension(R.styleable.QQStepView_borderWidget, mBorderWidget);
        mTextSize = ta.getDimensionPixelSize(R.styleable.QQStepView_stepTextSize, sp2px(mTextSize));
        mTextColor = ta.getColor(R.styleable.QQStepView_stepTextColor, mTextColor);
        ta.recycle();  // 回收

        mOuterPaint = new Paint(); // 创建总画笔
        mOuterPaint.setAntiAlias(true); // 取消锯齿
        mOuterPaint.setColor(mOuterColor);  // 设置颜色
        mOuterPaint.setStrokeWidth(mBorderWidget);  // 设置圆弧宽度
        mOuterPaint.setStyle(Paint.Style.STROKE);  // 设置圆弧Style
        mOuterPaint.setStrokeCap(Paint.Cap.ROUND);  // 设置起始,结束圆角

        mInnerPaint = new Paint();  // 创建进度画笔
        mOuterPaint.setAntiAlias(true);  // 抗锯齿
        mInnerPaint.setColor(mInnerColor);  // 设置颜色
        mInnerPaint.setStrokeWidth(mBorderWidget);  // 设置圆弧宽度
        mInnerPaint.setStyle(Paint.Style.STROKE);  // 设置圆弧Style
        mInnerPaint.setStrokeCap(Paint.Cap.ROUND);  // 设置起始,结束圆角

        mTextPaint = new Paint();   // 文字画笔
        mTextPaint.setAntiAlias(true);  // 抗锯齿
        mTextPaint.setTextSize(mTextSize);
        mTextPaint.setColor(mTextColor);
        mTextBounds = new Rect();
    }

    /**
     * 将sp值转换为px值,保证文字大小不变
     */
    public int sp2px(int spValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, getResources().getDisplayMetrics());
    }
}

在这里插入图片描述
在activity_main中使用自定义的QQStepView:

    <com.example.view2.QQStepView
        android:id="@+id/step_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/red"
        app:borderWidget="10dp"
        app:innerColor="@color/colorAccent"
        app:outerColor="@color/colorPrimary" />

2.2 onMeasure

// /CustomView/View2/app/src/main/java/com/example/view2/QQStepView.java
    // 5 onMeasure
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 在布局文件中,可能是wrap_content ,也可能宽高不一致
        // 获取宽高的模式
        int widgetMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        // 获取宽高的大小
        int widgetSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int min_Size = Math.min(widgetSize, heightSize);
        if(widgetMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.AT_MOST){
            min_Size = Math.min(min_Size, sp2px(200));
        }
        // 高度不一致,取最小值,确保是个正方形
        setMeasuredDimension(min_Size, min_Size);
    }

如果不设置为正方形,那么可能就会变成这样:
在这里插入图片描述

2.3 onDraw

// /CustomView/View2/app/src/main/java/com/example/view2/QQStepView.java
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 6.1 画外圆弧
        // 问题:边缘显示不全   描边有宽度  mBorderWidget
        float half = mBorderWidget / 2;
        // 描边有宽度borderWidget,如果不重新算rectF,绘制就会显示不全。这里将整个图多绘制半个宽度后,就刚刚好了。
        @SuppressLint("DrawAllocation") RectF rectF = new RectF(half, half, getWidth() - half, getHeight() - half);
        canvas.drawArc(rectF, mStartAngle, mEndAngle, false, mOuterPaint); // 这里如果是True的话,下面就是闭合的

        // 6.2 画内圆弧
        if (mMaxStep == 0) return;
        float sweepAngle = (float) mStep / mMaxStep;
        canvas.drawArc(rectF, mStartAngle, sweepAngle * mEndAngle, false, mInnerPaint);


        // 6.3 画文本
        mStepText = mStep + "";
        // 计算的宽度 与 字体的长度有关  与字体的大小  用画笔来测量
        // 获取文字排版信息
        Paint.FontMetricsInt fontMetricsInt = mTextPaint.getFontMetricsInt();
        int baseLine = getHeight() / 2 + (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;  // 计算基线
        // 获取文本宽度
        float textWidget = mTextPaint.measureText(mStepText);
//        // 使用mTextPaint对象的样式设置来测量mStepText的边界,并将边界存储在mTextBounds中。可使用mTextBounds对象来获取文本的宽度、高度等信息
//        mTextPaint.getTextBounds(mStepText, 0, mStepText.length(), mTextBounds);
//        float textWidget2 = mTextBounds.width();
        int dx = (int) (getWidth() / 2 - textWidget / 2);  // 控件的一半减去文字的一半,算出文字的初始绘制值
        canvas.drawText(mStepText, dx, baseLine, mTextPaint);
    }

在这里插入图片描述
在这里插入图片描述

3. 自定义View的动画-监听

// /CustomView/View2/app/src/main/java/com/example/view2/QQStepView.java
    // 7 其他
    public synchronized void setMaxStep(int maxStep) {
        this.mMaxStep = maxStep;
    }

    public synchronized void setStep(int step) {
        this.mStep = step;
        //不断绘制 invalidate中会调用onDraw方法
        invalidate();
    }

    public synchronized void setAnimatorStep(int step) {
        // 属性动画
        ValueAnimator valueAnimator = ObjectAnimator.ofFloat(0,step);  // 从0变化到step
        valueAnimator.setDuration(1000);   // 设置动画一秒
        valueAnimator.setInterpolator(new DecelerateInterpolator());  // 设置差时器,让其先快后慢,效果更明显
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(@NonNull ValueAnimator animation) {
                float currentStep = (float) animation.getAnimatedValue();
                setStep((int) currentStep);
            }
        });
        valueAnimator.start();
    }
// /CustomView/View2/app/src/main/java/com/example/view2/MainActivity.java
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final QQStepView qqStepView = (QQStepView) findViewById(R.id.step_view);
        qqStepView.setMaxStep(4000);  // 设置最大值为4000
        qqStepView.setAnimatorStep(3000);   // 设置当前步数,并动画从0-3000
    }

请添加图片描述

4. invalidate()源码分析

// /CustomView/View2/app/src/main/java/com/example/view2/QQStepView.java
    public synchronized void setStep(int step) {
        this.mStep = step;
        //不断绘制
        invalidate();
    }
-------->
// /Android/Sdk/sources/android-33/android/view/View.java
    public void invalidate() {
        invalidate(true);
    }
-------->
    public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }
-------->
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
            ...
                p.invalidateChild(this, damage);  // Parent父类
-------->

// /Android/Sdk/sources/android-33/android/view/ViewParent.java
    @Deprecated
    public void invalidateChild(View child, Rect r);
-------->

// /Android/Sdk/sources/android-33/android/view/ViewGroup.java
    @Override
    public final void invalidateChild(View child, final Rect dirty) {
            do {
                ...
                // 通过不断地do-while循环,不停地调用parent,
                // 最后调到最外层View(ViewRootImpl)的invalidateChildInParent方法
                parent = parent.invalidateChildInParent(location, dirty); 
                if (view != null) {
                    // Account for transform on current parent
                    Matrix m = view.getMatrix();
                    if (!m.isIdentity()) {
                        RectF boundingRect = attachInfo.mTmpTransformRect;
                        boundingRect.set(dirty);
                        m.mapRect(boundingRect);
                        dirty.set((int) Math.floor(boundingRect.left),
                                (int) Math.floor(boundingRect.top),
                                (int) Math.ceil(boundingRect.right),
                                (int) Math.ceil(boundingRect.bottom));
                    }
                }
            } while (parent != null);
-------->
// /Android/Sdk/sources/android-33/android/view/ViewRootImpl.java
    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        invalidateRectOnScreen(dirty);
-------->
    private void invalidateRectOnScreen(Rect dirty) {
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();
-------->
    void scheduleTraversals() {
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
-------->
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
-------->
    void doTraversal() {
        if (mTraversalScheduled) {
            performTraversals();
-------->
    // 在UI绘制中,这个方法非常重要
    private void performTraversals() {
        if (mFirst || windowShouldResize || viewVisibilityChanged || params != null
                || mForceNextWindowRelayout) {
                     // Ask host how big it wants to be
                     // 测量
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
        if (didLayout) {
            // 摆放
            performLayout(lp, mWidth, mHeight);
        ...
        if (!isViewVisible) {
        } else {
            // 绘制
            if (!performDraw() && mSyncBufferCallback != null) {
-------->
// 调用invalidate()主要就是调用performDraw(),performMeasure()和performLayout()是不会进的,虽然这三个方法都非常重要。
    private boolean performDraw() {
        try {
            boolean canUseAsync = draw(fullRedrawNeeded, usingAsyncReport && mSyncBuffer);
-------->
    private boolean draw(boolean fullRedrawNeeded, boolean forceDraw) {
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
-------->
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
            mView.draw(canvas);  // mView是找到的最外层的View,他除了绘制自己,还会绘制childView
-------->
// /Android/Sdk/sources/android-33/android/view/View.java
    // 这个方法,自定义View(2)中有看过
    public void draw(Canvas canvas) {
            // Step 3, draw the content
            onDraw(canvas);   // 绘制自己
            // Step 4, draw the children
            dispatchDraw(canvas);   // 绘制child
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);  // 绘制背景

在这里插入图片描述

简单总结invalidate()流程: 从当前View一直往上找,找到最外层的View,调用draw() -> dispatchDraw() 一路往下画,最终画到当前View的draw方法。调用invalidate()方法会牵连整个Layout布局中的View,全部都会刷新。

5. 高级面试题讲解

  • 为什么不能在子线程里面更新UI?
// /Android/Sdk/sources/android-33/android/view/ViewRootImpl.java
    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
-------->
    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

开一个线程,更新UI,一般都会调用setText()、setImageView()等等方法,而这些方法,最终都会调用ViewRootImpl的checkThread()。checkThread()用于检测线程,Thread.currentThread()为当前线程,即子线程,而mThread在构造函数中初始化,一般为主线程,两个线程不一样,会在这里抛出异常。

5.1 如何像微信朋友圈一样优化过度渲染

如果看自己应用的界面有没有过渡渲染:开发者选项->调式GPU过度绘制->显示过度绘制区域,红色即为过度绘制
在这里插入图片描述
自己写的一些界面一般都会比较复杂,类似QQ空间,WX朋友圈这种,都是列表嵌套列表,Item布局切套图片、文字等。获取到数据,然后去设置setText、setImageView的时候,其实都是调用的onInvalidate()方法,从上到下所有View都会绘制,导致过度渲染,那如何解决呢?

  • 网上的解决方案

尽量不要嵌套
能不设置背景就不要设置背景

  • 最好的解决方案

不直接使用系统提供好的嵌套布局,自己画布局,其运行效率更高(虽然开发效率比较低)。比如不适用Item嵌套Item,而是自己画一个Item布局,直接包含Item内容的。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要实现倒计时圆形进度,你可以创建一个自定义View,使用 Canvas 和 Paint 来绘制圆形和进度条。以下是一个示例代码: ```java public class CountdownCircleView extends View { private int maxProgress = 100; // 总进度 private int progress = 100; // 当前进度 private int circleColor = Color.GRAY; // 圆形颜色 private int progressColor = Color.BLUE; // 进度条颜色 private Paint circlePaint; private Paint progressPaint; public CountdownCircleView(Context context) { super(context); init(); } public CountdownCircleView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public CountdownCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { circlePaint = new Paint(); circlePaint.setAntiAlias(true); circlePaint.setColor(circleColor); progressPaint = new Paint(); progressPaint.setAntiAlias(true); progressPaint.setColor(progressColor); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int viewWidth = getWidth(); int viewHeight = getHeight(); // 绘制圆形 int diameter = Math.min(viewWidth, viewHeight); int radius = diameter / 2; int centerX = viewWidth / 2; int centerY = viewHeight / 2; canvas.drawCircle(centerX, centerY, radius, circlePaint); // 绘制进度条 RectF rectF = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius); float sweepAngle = 360f * progress / maxProgress; canvas.drawArc(rectF, -90, sweepAngle, true, progressPaint); } public void setMaxProgress(int maxProgress) { this.maxProgress = maxProgress; } public void setProgress(int progress) { this.progress = progress; invalidate(); // 重新绘制 } } ``` 你可以将上述代码放入你的 Android 项目中,并在布局文件中使用 `CountdownCircleView`。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值