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

来源:互联网 发布:淘宝美工做什么工作的 编辑:程序博客网 时间:2024/06/05 19:26

请尊重个人劳动成果,转载注明出处,谢谢!
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);                }            }        });    }}

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

1 0
原创粉丝点击