Android自定义View之可随时暂停、开启的圆形下载进度条

请尊重个人劳动成果,转载注明出处,谢谢!
http://blog.csdn.net/xiaxiazaizai01/article/details/52355558

这是一个一言不合就手撸一个自定义View的任性时代,因此最近一段时间一直在学习自定义View相关的知识,也看了很多与此相关的博客,有句话叫做不要重复造轮子,别人写好的直接拿过来改吧改吧,能用就行,但是,要想像那些任性的大牛一样,分分钟撸一个自定义View,就得不断的重复造轮子,学习大神们的设计思路, 站在牛人的肩膀上不断前行,每篇开篇之前都要啰嗦半天,急性子的童鞋可以直接跳过。看到yissan大牛写了一篇自定义圆形进度条,思路很清晰,就照着也撸了一遍,果然是酸爽啊,在这里非常感谢yissan大牛,哈哈。。。为了让大家能一遍就看懂,我会把注释写的非常非常详细,秒懂哦,,什么??你不能秒懂。。注释都写的辣么详细了,面壁思过去。。哈哈

下面看下效果图:
这里写图片描述

1、首先创建View

(1)设置自定义View属性,通常做法是在res/values里面创建一个attrs文件夹,来写我们的自定义属性,一般我们设置属性的name时,一般习惯性的将我们自定义的类名作为name
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 自定义圆形进度条,属性设置 -->
    <declare-styleable name="CustomCircleProgress">
        <!-- 默认圆的颜色 -->
        <attr name="progress_default_color" format="color"/>
        <!-- 进度圆的颜色 -->
        <attr name="progress_reached_color" format="color"/>
        <!-- 进度条的高度 -->
        <attr name="progress_reached_height" format="dimension"/>
        <!-- 无进度时(默认圆)的边框高 -->
        <attr name="progress_default_height" format="dimension"/>
        <!-- 圆的半径 -->
        <attr name="circle_radius" format="dimension"/>
    </declare-styleable>
</resources>
(2)设置完了自定义属性,下一步当然是在我们的自定义View类中去获取。(我们都习惯在参数多的构造方法中去获取自定义属性,其他构造方法则去通过this去调用,注意这里是this而不是super,super的话则指向的是父类,这里我犯了一个常识性错误,一键生成几个构造方法,忘了将super改成this,导致获取属性的方法没有被调用执行,大家在调用的时候可以打断点试试)
public CustomCircleProgress(Context context) {
        this(context,null);
    }

    public CustomCircleProgress(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CustomCircleProgress(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性的值
        TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.CustomCircleProgress);
        //默认圆的颜色
        mDefaultColor = array.getColor(R.styleable.CustomCircleProgress_progress_default_color, PROGRESS_DEFAULT_COLOR);
        //进度条的颜色
        mReachedColor = array.getColor(R.styleable.CustomCircleProgress_progress_reached_color, PROGRESS_REACHED_COLOR);
        //默认圆的高度
        mDefaultHeight = (int) array.getDimension(R.styleable.CustomCircleProgress_progress_default_height, mDefaultHeight);
        //进度条的高度
        mReachedHeight = (int) array.getDimension(R.styleable.CustomCircleProgress_progress_reached_height, mReachedHeight);
        //圆的半径
        mRadius = (int) array.getDimension(R.styleable.CustomCircleProgress_circle_radius, mRadius);
        //最后不要忘了回收 TypedArray
        array.recycle();

        //设置画笔(new画笔的操作一般不要放在onDraw方法中,因为在绘制的过程中onDraw方法会被多次调用)
        setPaint();
我们在new我们的画笔时一般不要在onDraw()方法中去new,因为view在不断的绘制过程中onDraw()方法会不断的被调用,这样就会造成不停的new我们的画笔实例。
//设置画笔
private void setPaint() {
        mPaint = new Paint();
        //下面是设置画笔的一些属性
        mPaint.setAntiAlias(true);//抗锯齿
        mPaint.setDither(true);//防抖动,绘制出来的图要更加柔和清晰
        mPaint.setStyle(Paint.Style.STROKE);//设置填充样式
        /**
         *  Paint.Style.FILL    :填充内部
         *  Paint.Style.FILL_AND_STROKE  :填充内部和描边
         *  Paint.Style.STROKE  :仅描边
         */
        mPaint.setStrokeCap(Paint.Cap.ROUND);//设置画笔笔刷类型


    }

2、处理View的布局,即测量onMeasure( )

当我们在xml文件中给这个view设置android:layout_width=”“android:layout_height=”“属性为固定值、wrap_parent、match_parent 时,表明开发者向ViewGroup沟通表明我需要的空间。ViewGroup收到了开发者对View大小的说明,然后ViewGroup会综合考虑自己的空间大小以及开发者的请求,然后生成两个MeasureSpec对象(width与height)传给View,这两个对象是ViewGroup向子View提出的要求,就相当于告诉子View:“我已经与你的使用者(开发者)商量过了,现在把我们商量确定的结果告诉你,你的宽度不能违反width MeasureSpec对象的要求,你的高度不能违反height MeasureSpec对象的要求,现在,你赶紧根据这个要求确定下自己要多大空间,只许少,不许多哦。”对于超过ViewGroup为我们分配的空间时,就需要进行测量处理,然后再将处理后的结果反馈给ViewGroup,如果不是很了解的话可以点击查看上一篇博客,有详细的说明

/**
     * 使用onMeasure方法是因为我们的自定义圆形View的一些属性(如:进度条宽度等)都交给用户自己去自定义了,所以我们需要去测量下
     * 看是否符合要求
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int paintHeight = Math.max(mReachedHeight, mDefaultHeight);//比较两数,取最大值

        if(heightMode != MeasureSpec.EXACTLY){
            //如果用户没有精确指出宽高时,我们就要测量整个View所需要分配的高度了,测量自定义圆形View设置的上下内边距+圆形view的直径+圆形描边边框的高度
            int exceptHeight = getPaddingTop() + getPaddingBottom() + mRadius*2 + paintHeight;
            //然后再将测量后的值作为精确值传给父类,告诉他我需要这么大的空间,你给我分配吧
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(exceptHeight, MeasureSpec.EXACTLY);
        }
        if(widthMode != MeasureSpec.EXACTLY){
            //这里在自定义属性中没有设置圆形边框的宽度,所以这里直接用高度代替
            int exceptWidth = getPaddingLeft() + getPaddingRight() + mRadius*2 + paintHeight;
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(exceptWidth, MeasureSpec.EXACTLY);
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

我们需要考虑开发者有时会给View设置一些padding属性

3、绘制View,即onDraw()

(1)这里我们需要绘制默认的内部圆以及表示进度的外层圆弧,根据进度值的变化来绘制圆弧。在绘制外层表示进度的圆弧时,需要首先确定圆弧的外接矩形(进度也就成了内切圆)的坐标,如下图所示
这里写图片描述

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

        /**
         * 这里canvas.save();和canvas.restore();是两个相互匹配出现的,作用是用来保存画布的状态和取出保存的状态的
         * 当我们对画布进行旋转,缩放,平移等操作的时候其实我们是想对特定的元素进行操作,但是当你用canvas的方法来进行这些操作的时候,其实是对整个画布进行了操作,
         * 那么之后在画布上的元素都会受到影响,所以我们在操作之前调用canvas.save()来保存画布当前的状态,当操作之后取出之前保存过的状态,
         * (比如:前面元素设置了平移或旋转的操作后,下一个元素在进行绘制之前执行了canvas.save();和canvas.restore()操作)这样后面的元素就不会受到(平移或旋转的)影响
         */
        canvas.save();
        //为了保证最外层的圆弧全部显示,我们通常会设置自定义view的padding属性,这样就有了内边距,所以画笔应该平移到内边距的位置,这样画笔才会刚好在最外层的圆弧上
        //画笔平移到指定paddingLeft, getPaddingTop()位置
        canvas.translate(getPaddingLeft(),getPaddingTop());
        mPaint.setStyle(Paint.Style.STROKE);
        //画默认圆(边框)的一些设置
        mPaint.setColor(mDefaultColor);
        mPaint.setStrokeWidth(mDefaultHeight);
        canvas.drawCircle(mRadius,mRadius,mRadius,mPaint);

        //画进度条的一些设置
        mPaint.setColor(mReachedColor);
        mPaint.setStrokeWidth(mReachedHeight);
        //根据进度绘制圆弧
        float sweepAngle = getProgress() * 1.0f / getMax() * 360;
        canvas.drawArc(new RectF(0, 0, mRadius * 2, mRadius *2), 0, sweepAngle, false, mPaint);//drawArc:绘制圆弧

        canvas.restore();

    }

我们做个定时器,让进度条动起来

public class MainActivity extends AppCompatActivity {

    private CustomCircleProgress circleProgress;
    private int progress;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        circleProgress = (CustomCircleProgress) findViewById(R.id.circleProgress);

        Timer timer = new Timer();
         TimerTask task = new TimerTask() {
            @Override
            public void run() {
                if(progress >= 100){
                    progress = 0;
                    circleProgress.setProgress(0);
                }else{
                    progress = circleProgress.getProgress();
                    circleProgress.setProgress(++progress);
                }
            }
        };
        timer.schedule(task,0,100);

    }
}

这样得到的效果图是这样的
这里写图片描述
有的伙计该说了,为什么进度条的起始位置不是从最上面开始的,因为这里我设置的canvas.drawArc(new RectF(0, 0, mRadius * 2, mRadius 2), 0, sweepAngle, false, mPaint);中我设置的参数为0,表示圆弧的起始位置从0开始,即X轴的正方向。这里我们只需将圆弧的起始位置设置成-90度即可,canvas.drawArc(new RectF(0, 0, mRadius 2, mRadius *2), -90, sweepAngle, false, mPaint);我们再来看下效果图
这里写图片描述
完美,,哈哈,,,,

//绘制圆
public void drawCircle (float cx, float cy, float radius, Paint paint)
//参数说明
/**
* cx:圆心的x坐标。
  cy:圆心的y坐标。
  radius:圆的半径。
  paint:绘制时所使用的画笔。
*/

(2)接下来,我们开始绘制里面的暂停(完成)状态时的三角形,以及开启状态时的两条竖线,首先我们通过枚举的方式定义这两种状态,并提供set/get方法供外界调用。首先我们需要Path mPath = new Path();然后通过mPath.moveTo()确定三角形的第一个点的坐标,然后通过mPath.lineTo()链接其他几个点的坐标,如果当我们设置画笔的样式为mPaint.setStyle(Paint.Style.STROKE);则我们需要执行close形成封闭的三角形,或者你也可以直接再来一条mPath.lineTo()再将第一个点的坐标给连接起来,这样也形成了一个封闭的三角形。

//通过path路径绘制三角形
        mPath = new Path();
        //让三角形的长度等于圆的半径(等边三角形)
        triangleLength = mRadius;
        //绘制三角形,首先我们需要确定三个点的坐标
        float firstX = (float) ((mRadius*2 - Math.sqrt(3.0) / 2 * mRadius) / 2);//左上角第一个点的横坐标,根据勾三股四弦五定律,Math.sqrt(3.0)表示开方
        //为了显示的好看些,这里微调下firstX横坐标
        float mFirstX = (float)(firstX + firstX*0.2);
        float firstY = mRadius - triangleLength / 2;
        //同理,依次可得出第二个点(左下角)第三个点的坐标
        float secondX = mFirstX;
        float secondY = (float) (mRadius + triangleLength / 2);
        float thirdX = (float) (mFirstX + Math.sqrt(3.0) / 2 * mRadius);
        float thirdY =  mRadius;
        mPath.moveTo(mFirstX,firstY);
        mPath.lineTo(secondX,secondY);
        mPath.lineTo(thirdX,thirdY);
        mPath.lineTo(mFirstX,firstY);

然后我们在onDraw()方法中去判断绘制不同状态下的view

//有了path之后就可以在onDraw中绘制三角形的End和Starting状态了
        if(mStatus == Status.End){//未开始状态,画笔填充三角形
            mPaint.setStyle(Paint.Style.FILL);
            //设置颜色
            mPaint.setColor(Color.parseColor("#01A1EB"));
            //画三角形
            canvas.drawPath(mPath,mPaint);
        }else{//正在进行状态,画两条竖线
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(dp2px(5));
            mPaint.setColor(Color.parseColor("#01A1EB"));
            canvas.drawLine(mRadius*2/3, mRadius*2/3, mRadius*2/3, 2*mRadius*2/3, mPaint);
            canvas.drawLine(2*mRadius - (mRadius*2/3), mRadius*2/3, 2*mRadius - (mRadius*2/3), 2*mRadius*2/3, mPaint);
        }

4、处理与用户的交互

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.customview.MainActivity">

    <com.example.customview.CustomCircleProgress
        android:id="@+id/circleProgress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:padding="10dp"
        />
</RelativeLayout>

最后是我们的MainActivity类

public class MainActivity extends AppCompatActivity {

    private CustomCircleProgress circleProgress;
    private int progress;

    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case PROGRESS_CIRCLE_STARTING:
                    progress = circleProgress.getProgress();
                    circleProgress.setProgress(++progress);
                    if(progress >= 100){
                        handler.removeMessages(PROGRESS_CIRCLE_STARTING);
                        progress = 0;
                        circleProgress.setProgress(0);
                        circleProgress.setStatus(CustomCircleProgress.Status.End);//修改显示状态为完成
                    }else{
                        //延迟100ms后继续发消息,实现循环,直到progress=100
                        handler.sendEmptyMessageDelayed(PROGRESS_CIRCLE_STARTING, 100);
                    }
                    break;
            }
        }
    };
    public static final int PROGRESS_CIRCLE_STARTING = 0x110;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        circleProgress = (CustomCircleProgress) findViewById(R.id.circleProgress);

        circleProgress.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(circleProgress.getStatus() == CustomCircleProgress.Status.Starting){//如果是开始状态
                    //点击则变成关闭暂停状态
                    circleProgress.setStatus(CustomCircleProgress.Status.End);
                    //注意,当我们暂停时,同时还要移除消息,不然的话进度不会被停止
                    handler.removeMessages(PROGRESS_CIRCLE_STARTING);
                }else{
                    //点击则变成开启状态
                    circleProgress.setStatus(CustomCircleProgress.Status.Starting);
                    Message message = Message.obtain();
                    message.what = PROGRESS_CIRCLE_STARTING;
                    handler.sendMessage(message);
                }
            }
        });
    }
}

最后,希望对你能有所帮助,有问题欢迎留言,大家一块探讨,写博客确实挺累的。。。有需要源码的,可以点击下载源码

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值