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

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

看效果:

image

有几个要素:

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

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

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

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

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

attrs.xml --- 属性配置

    <declare-styleable name="CircleProgressView">
      <attr name="inside_radius" format="dimension"></attr>
      <attr name="inside_color" format="string"></attr>
      <attr name="outside_color" format="string"></attr>
      <attr name="outside_width" format="dimension"></attr>
      <attr name="text_color" format="string"></attr>
      <attr name="bg_drawable" format="integer"></attr>
      <attr name="text_size" format="integer"></attr>
      <!--<attr name="android:layout_width"/>-->
      <!--<attr name="android:layout_height"/>-->
   </declare-styleable>

activity_main.xml --- 布局文件

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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">

    <!--android:background="@drawable/luffy"-->
    <!--android:minWidth=""-->
    <!--android:minHeight=""-->
    <!--app:bgdrawable="@drawable/luffy"-->
    <!--android:minWidth="12dp"-->
    <me.heyclock.hl.customcopy.CircleProgressView
        android:id="@+id/am_ciclePro"
        style="@style/MyTextView01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#00ffffff"
        app:inside_color="#FF0000"
        app:inside_radius="320dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:outside_color="#0000FF"
        app:outside_width="30dp"
        app:text_color="#FFFFFF"
        app:text_size="38" />

</android.support.constraint.ConstraintLayout>

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的时候我们再回味吧....流程先搞了再说!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容