Android-自定义View-自定义一个圆形进度条

之前研究了相关的绘制,单个控件的测量,以及简单的类内部事件处理。我们先自定义个一个圆形进度条作为实践。然后再继续更复杂的自定义控件:

看效果:

image

有几个要素:

1. 内圈圆的绘制+中心进度的绘制

2. 最外圈的外切范围的计算+进度的绘制

3. 第二层外圈的外切范围的计算+进度比最外层稍慢的绘制处理

4. 当然最麻烦的是就是你需要去兼容内圈半径不能超过控件范围、最外层环不能太大以及覆盖住内圈的问题、第二层外圈(黑色部分)正好在最外层外环和内圈的问题,最后的一系列处理也就是为了做到兼容适配,不管用户怎么设置都没问题。当然初始阶段我觉得可以先做效果,然后进行版本迭代....

我想先把代码、属性、布局贴出来吧:

attrs.xml --- 属性配置

    
      
      
      
      
      
      
      
      
      
   

activity_main.xml --- 布局文件




    
    
    
    
    
    


CircleProgressView.java --- 奉上自定义控件文件

package me.heyclock.hl.customcopy;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
/*
 *@Description: 自定义圆形进度条
 *@Author: hl
 *@Time: 2018/10/12 9:37
 */
public class CircleProgressView extends View {
    /* 官方文档:
        https://developer.android.google.cn/reference/android/graphics/Canvas
        https://developer.android.google.cn/reference/android/graphics/Paint
    */
    private Context context;        ///< 上下文
    private Canvas canvas;          ///< 画布
    private Paint paintInside;      ///< 内圈
    private Paint paintOutside;     ///< 外圈
    private Paint paintOutsideS;    ///< 外圈第二层
    private int outsideThick;       ///< 外环厚度
    private int outsideThickS;      ///< 外环厚度第二层
    private Paint paintText;        ///< 文本
    private int textSize;           ///< 文本大小

    ///< 做红色点击区域限制
    private boolean bIsDownInRedRegion = false;
    ///< 圆圈半径
    private int radius;
    ///< 圆圈颜色
    private String inside_color;
    private String outside_color;
    private String text_color;
    ///< 控件自定义背景
    private Bitmap bgDrawable = null;
    ///< 控件宽度和高度
    private int width = 12;
    private int height = 12;

    ///< 进度百分比
    private float mCurPercent = 1;

    /**
     * 刷新绘制+增量变化
     */
    private static final int STEP_RADIUS = 10;  ///< 每次半径增加10

    public CircleProgressView(Context context) {
        this(context, null);
    }

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

    public CircleProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, 0, 0);
    }

    public CircleProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        this.context = context;

        ///< TypedArray的方式
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView);
        ///< getDimension() getDimensionPixelOffset() getDimensionPixelSize()
        ///  --这三个方法都是根据DisplayMetrics获取相应的值,不同在于方法1直接保存float型数据,方法2直接对float取整,方法3对float小数先四舍五入后取整。
        radius = ta.getDimensionPixelOffset(R.styleable.CircleProgressView_inside_radius, 6);
        inside_color = ta.getString(R.styleable.CircleProgressView_inside_color);
        outside_color = ta.getString(R.styleable.CircleProgressView_outside_color);
        outsideThick = ta.getDimensionPixelOffset(R.styleable.CircleProgressView_outside_width, 18);
        text_color = ta.getString(R.styleable.CircleProgressView_text_color);
        textSize = ta.getInteger(R.styleable.CircleProgressView_text_size, 38);
        Drawable drawable = ta.getDrawable(R.styleable.CircleProgressView_bg_drawable);
        if (null != drawable) {
            BitmapDrawable bd = (BitmapDrawable) drawable;
            bgDrawable = bd.getBitmap();
        }
        ta.recycle();

        ///< 1\. 做一些绘制初始化
        canvas = new Canvas();  ///< 也可以指定绘制到Bitmap上面 -> Canvas(Bitmap bitmap)
        ///< 1.1 内环
        paintInside = new Paint();
        paintInside.setStyle(Paint.Style.FILL);     ///< 绘制实心圆
        paintInside.setColor(Color.parseColor(inside_color));
        paintInside.setAntiAlias(true);               ///< 消除锯齿

        ///< 1.2 中心文本
        paintText = new Paint();
        paintText.setTextSize(textSize);
        paintText.setColor(Color.parseColor(text_color));
        paintText.setAntiAlias(true);               ///< 消除锯齿

        ///< 1.3 外环进度
        paintOutside = new Paint();
        paintOutside.setColor(Color.parseColor(outside_color));
        paintOutside.setStrokeWidth(outsideThick);
        paintOutside.setStrokeCap(Paint.Cap.ROUND);     ///< 圆形头
        paintOutside.setStyle(Paint.Style.STROKE);     ///< 绘制空心圆
        paintOutside.setAntiAlias(true);               ///< 消除锯齿

        ///< 1.4 外环第二层
        paintOutsideS = new Paint(paintOutside);
        paintOutsideS.setColor(Color.BLACK);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //super.onDraw(canvas);

        ///< 2.进行绘制
        ///< 先绘制一个背景 - 如果自定义控件背景存在的情况下,则进行背景绘制
        if (null != bgDrawable) {
            canvas.drawBitmap(bgDrawable, 0, 0, paintOutside);
        }

        ///< 绘制一个圆圈吧-> drawCircle(float cx, float cy, float radius, Paint paint)
        canvas.drawCircle(width / 2, height / 2,
                radius / 2, paintInside);

        ///< 绘制圆圈中心的进度 - 白色
        Rect txRect = new Rect();
        String percentS = mCurPercent + "%";
        paintText.getTextBounds(percentS, 0, percentS.length(), txRect);
        ///< 关于起点,涉及到基线相关的知识(目前可以姑且暂时理解为绘制起始点是左下角)
        canvas.drawText(percentS, width / 2 - txRect.width() / 2,
                height / 2 + txRect.height() / 2, paintText);

        ///< 绘制外环
        RectF mCircle = new RectF(); ///< 背景圆的外接矩形
        mCircle.set(outsideThick,         ///< 外切矩形范围
                outsideThick,
                getWidth() - outsideThick,
                getHeight() - outsideThick);
        canvas.drawArc(mCircle, 270,
                360 * mCurPercent / 100,
                false, paintOutside);

        ///< 绘制第二层外圈
        RectF mCircleSecond = new RectF(); ///< 背景圆的外接矩形
        ///< 第二层的外切矩形范围左上角相当于第一层的左上角+第二层外圈的厚度,相应的右下角需要多减去一个厚度
        mCircleSecond.set(outsideThick + outsideThickS,         ///< 外切矩形范围
                outsideThick + outsideThickS,
                getWidth() - outsideThick - outsideThickS,
                getHeight() - outsideThick - outsideThickS);
        ///< 让第二层外圈稍微慢点走
        float realPercent = mCurPercent > 3 ? mCurPercent - 3 : mCurPercent;
        canvas.drawArc(mCircleSecond, 270,
                360 * realPercent / 100,
                false, paintOutsideS);

        ///< TODO 只有会专门研究相关的绘制以及相关参数的示意图
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ///< 采用默认的onMeasure看看
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        ///< 自己进行相关测量
        int defaultW = 12;
        int defaultH = 12;

        int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSize = MeasureSpec.getSize(heightMeasureSpec);

        ///< 在wrap_content的情况下默认长度为默认宽/高与背景图片对比后的最大值,假设背景图片是200*200,则显示200*200
        int minWSize = Math.max(dp2px(context, defaultW), getSuggestedMinimumWidth());
        int minHSize = Math.max(dp2px(context, defaultH), getSuggestedMinimumHeight());
        ///< 如果设置了半径的情况下,再跟半径做一次取值,保证用户设置wrap_content的,半径设置的情况下以半径为主
        minWSize = Math.max(minWSize, radius);
        minHSize = Math.max(minHSize, radius);

        ///< wrap_content的specMode是AT_MOST模式,这种情况下宽/高等同于specSize
        //  查表得这种情况下specSize等同于parentSize,也就是父容器当前剩余的大小
        //  在wrap_content的情况下如果不特殊处理,效果等同martch_parent
        if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
            width = minWSize;
            height = minHSize;
            setMeasuredDimension(minWSize, minHSize);
        } else if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY) {
            width = wSize;
            height = hSize;
            setMeasuredDimension(wSize, hSize);
        } else if (wSpecMode == MeasureSpec.AT_MOST) {
            width = minWSize;
            height = hSize;
            setMeasuredDimension(minWSize, hSize);
        } else if (hSpecMode == MeasureSpec.AT_MOST) {
            width = wSize;
            height = minHSize;
            setMeasuredDimension(wSize, minHSize);
        }

        ///< 做一个兼容,如果半径超过了控件宽或者高
        int minWH = width;
        if (width > height) {
            minWH = height;
        }
        ///< 1\. 兼容半径
        if ((radius * 2) > minWH) {
            radius = minWH / 2;
            Log.e("attrs", "纠正一下 " + radius);
        }

        ///< 2\. 兼容外圈厚度*2 不超过控件范围,同时不能覆盖内圈; 另外多减了radius/2作为中空部分占据
        if (outsideThick >= radius/4){
            outsideThick = radius/4;
            paintOutside.setStrokeWidth(outsideThick);
        }

        ///< 3\. 外圈第二层
        outsideThickS = (width - outsideThick * 2 - radius)/3;
        paintOutsideS.setStrokeWidth(outsideThickS);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        ///< 点击区域坐标范围
        int minX = (width - radius * 2) / 2;
        int maxX = width / 2 + radius;
        int minY = (height - radius * 2) / 2;
        int maxY = height / 2 + radius;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (x >= minX && x <= maxX &&
                        y >= minY && y <= maxY) {
                    bIsDownInRedRegion = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                if (bIsDownInRedRegion) {
                    bIsDownInRedRegion = false;

                    if (x >= minX && x <= maxX &&
                        y >= minY && y <= maxY) {
                        ///< 抬手时我们就可以启动定时器进行绘制刷新了
                        Log.e("test", "进度条区域点击了呀,sb");
                    }
                }
                break;
        }
        return true;
    }

    /**
     * 刷新绘制+增量变化
     */
    public void updateDraw(int perCent) {
        ///< 更新进度条
        mCurPercent = perCent;
        invalidate();
    }

    /**
     * dp转px
     *
     * @param dp
     * @return
     */
    public static int dp2px(Context context, int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
    }
}

然后调用方式,自定义了一个定时器不停的进行(调用部分代码):

  private CircleProgressView circleProgressView;
  private int perCent = 1;

  Then, call
  {
    circleProgressView = findViewById(R.id.am_ciclePro);

        final Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        circleProgressView.updateDraw(perCent++);
                        if (perCent > 100){
                            perCent = 1;
                            //timer.cancel();
                        }
                    }
                });
            }
        }, 0, 100);
  }

注意我们的内圈绘制时半径是: **radius **/ 2 - radius是宽高最小值的一半哟!

image

调试过程中的比较麻烦的就是两个外环(记住内圈半径是**radius **/ 2):

这样我们再来计算两个外圈半径厚度才能比较好处理....如下部分是厚度的处理:

image

或许我们需要这么分割一下才能便于我们进行计算:

image

在测量最外层环半径是可以这样: (minWH - radius/2 * 2 ) / 2(假设宽高一样, 就是控件宽度 - 内圈直径, 然后除以2,但是贴近红圈,这样黑色圈就不能放了。) 所以为了好看,我们再除以2空出一个内环的位置. (minWH - radius/2 * 2 ) / 2 / 2 = (minWH - radius/2 * 2 ) / 4 = (2*radius- radius/2 * 2 ) / 4 = radius/4;

目前就是大概就这样做了一个计算,总感觉不是很精准的样子。可能还有bug啥的。 后面具体深入paint的时候我们再回味吧....流程先搞了再说!

你可能感兴趣的:(Android-自定义View-自定义一个圆形进度条)