自定义View之进度条

利用清明假期好好的学习了一番自定义View,稍有心得,今天来记录一下。

工作中经常想要实现一些View效果,但是有没有现成的,那么久需要我们来自定义一个了。自定义View大致上有以下三种应用情况:

  • 在现有的控件上,做些个性化的处理(继承自ImageView等 )
  • 现有的控件不满足于我们的需求,需要自己去创造(继承自View)
  • 讲几个控件组合到一起,新生成一个(继承自ViewGroup)

这里这要记录一下第二种情况。

首先要自定义一个类CircleImageProgressBar,使之继承自View,同时实现其构造方法:

public CircleImageProgressBar(Context context) {
        this(context, null);//调用两个参数的构造方法
    }

    public CircleImageProgressBar(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);//调用三个参数的构造方法
    }

    public CircleImageProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);//调用四个参数的构造方法
    }

    public CircleImageProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

在这里为了方便,我们其他的三个构造方法都使用this关键字调用到四个参数的构造方法。然后我们需要在在res/values/  下建立一个attrs.xml , 在里面定义我们自定义View的属性和声明我们的整个样式。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleImageProgressBar">
        <attr name="circleColor" format="color"/>
        <attr name="radius" format="dimension"/>
        <attr name="strokeWidth" format="dimension"/>
        <attr name="progressColor" format="color"/>
        <attr name="progressWidth" format="dimension"/>

    </declare-styleable>

在这里定义了以后,就可以在布局文件中使用这些参数。接下来我们在自定义View的构造方法中做初始化:

public class CircleImageProgressBar extends View {
    /** 画笔 */
    private Paint circlePaint;
    /** 圆的颜色 */
    private float circleColor;
    /** 圆的半径 */
    private float radius;
    /** 圆的宽度 */
    private float strockWidth;
    /** 进度的颜色,要与圆的颜色区分 */
    private float progressColor;
    /** 进度的画笔 */
    private Paint progressPaint;
    /** 弧线 */
    private RectF rectF;
    /** 当前进度 */
    private float currentProgress;

    //文字画笔
    private Paint textPaint;
    //为了密度转换,需要一个context
    private Context context;

    //先写构造函数,在这里统一使用四个参数的
    .......

    public CircleImageProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        this.context=context;
        //初始化属性
        initAttrs(context, attrs);
        //创建并初始化画笔,因为Android要求尽量不要在布局(onLayout)和绘制(onDraw)阶段初始化对象,因为这些方法会被频繁调用,损耗性能。所以在此处就提前初始化了
        initVariables();
    }

    //在attrs.xml中定义好属性后,这些就会传入到自定义View的构造方法的AttributeSet类型的参数中。
    //我们可以通过context.obtainStyledAttributes()方法返回一个TypedArray对象。然后用TypedArray对象获取自定义View的属性的值。
    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleImageProgressBar, 0, 0);
        radius = typedArray.getDimension(R.styleable.CircleImageProgressBar_radius, 200);//获取半径,默认值为100
        circleColor = typedArray.getColor(R.styleable.CircleImageProgressBar_circleColor, Color.RED);//获取圆环的颜色,默认为红色
        strockWidth = typedArray.getDimension(R.styleable.CircleImageProgressBar_strokeWidth, 20);//获取圆环的宽度,默认为20
        progressColor=typedArray.getColor(R.styleable.CircleImageProgressBar_progressColor,Color.RED);
        typedArray.recycle();//TypedArray对象是共享的资源,所以在获取完值之后必须要调用recycle()方法来回收。
    }

    private void initVariables() {
        circlePaint = new Paint();
        circlePaint.setAntiAlias(true);//画笔去掉锯齿
        circlePaint.setColor((int) circleColor);//设置为红色
        circlePaint.setStyle(Paint.Style.STROKE);//设置样式,有实心,和空心之分
        circlePaint.setStrokeWidth(strockWidth);//设置圆的线条宽度

        rectF=new RectF();

        progressPaint=new Paint();
        progressPaint.setAntiAlias(true);
        progressPaint.setColor((int) progressColor);
        progressPaint.setStyle(Paint.Style.STROKE);
        progressPaint.setStrokeWidth(strockWidth);

        textPaint=new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setColor((int) progressColor);
        textPaint.setStyle(Paint.Style.STROKE);
        textPaint.setTextSize(DensityUtils.sp2px(context, 22));
    }
}

当这些初始化完成以后,我们就可以写onDraw()方法了,这个是自定义View 的一个非常重要的方法,自定义View的绘制要在这里完成:

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int minimun=Math.min(getWidth()/2,getHeight()/2);//获取屏幕宽度和高度的最小值
        radius=radius>=minimun?minimun:radius;//获取半径
        //四个参数,分别为圆心的x,圆心的y,半径,画笔
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius-strockWidth/2, circlePaint);

        //获取进度圆的矩形区域,此时getWidth才有值。
        rectF.top=getHeight()/2-radius+strockWidth/2;
        rectF.left=getWidth()/2-radius+strockWidth/2;
        rectF.right=getWidth()/2+radius-strockWidth/2;
        rectF.bottom=getHeight()/2+radius-strockWidth/2;

        //动态圆的总进度
        int totalProgress=100;
        //本质其实是画一个圆弧型的矩形
        //RectF oval:就是椭圆的矩形区域,如果我们设定一个正方向区域,那么绘制的就是一个圆的弧线,否则即为椭圆的弧线。
        //float startAngle:起始的角度,0度角对应钟表的3点钟方向,所以起始位置为-90度。
        //float sweepAngle:就是前进的角度
        //boolean useCenter:true就是绘制扇形,false仅仅绘制弧线
        //Paint paint:就是画笔了。
        canvas.drawArc(rectF,-90,currentProgress/totalProgress*360,false,progressPaint);
        //这里还要画一个文字,来显示进度
        String text=currentProgress+"%";
        float textWidth=textPaint.measureText(text,0,text.length());
        Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
        float textHeight=Math.abs(fontMetrics.ascent)-fontMetrics.descent;
        canvas.drawText(currentProgress+"%",(getWidth()-textWidth)/2,(getHeight()+textHeight)/2,textPaint);
    }

通过onDraw()方法我们就把需要的视图基本都画出来了,但显然不够生动,进度是死的,下面我们就通过view 的postInvalidate()方法更新进度:

在view中:

public void updateProgress(int currentProgress){
    this.currentProgress=currentProgress;
    postInvalidate();
}

然后在activity的onResume()方法中书写:

@Override
    protected void onResume() {
        super.onResume();
        new Thread(new Runnable() {
            @Override
            public void run() {
                int progress=0;
                while (progress<=100){
                    circleImageProgressBar.updateProgress(progress);
                    try{
                        Thread.sleep(100);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

接下来再看一下自定义view的onMeasure()方法:

自定义View的测量核心就是系统提供给我们的MeasureSpec,而MeasureSpec的核心就是两个属性:mode和size。
MeasureSpe类把测量模式和大小组合到一个32位的int型的数值中,其中高2位表示模式,低30位表示大小而在计算中使用位运算的原因是为了提高并优化效率。

首先来看mode
  • UNSPECIFIED:要多大给多大,一般不关心这个模式。
  • EXACTLY:即精确值模式,当控件的layout_width属性或layout_height属性指定为具体数值时,例如android:layout_width="100dp",或者指定为match_parent属性时,系统使用的是EXACTLY 模式。
  • AT_MOST:即最大值模式,当控件的layout_width属性或layout_height属性指定为warp_content时,控件大小一般随着控件的子控件或者内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可。

下面我们重写onMeasure()方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  
{  
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);  
    int width;  
    int height ;  
    if (widthMode == MeasureSpec.EXACTLY)  
    {  
        width = widthSize;  
    } else  
    {  
        mPaint.setTextSize(mTitleTextSize);  
        mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);  
        float textWidth = mBounds.width();  
        int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());  
        width = desired;  
    }  
  
    if (heightMode == MeasureSpec.EXACTLY)  
    {  
        height = heightSize;  
    } else  
    {  
        mPaint.setTextSize(mTitleTextSize);  
        mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);  
        float textHeight = mBounds.height();  
        int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());  
        height = desired;  
    }  
    setMeasuredDimension(width, height);  
}  

这样我们的自定义view就基本写完了,为了更好的体验,我还想扩展一下,就是当我们点击的时候,将进度 重置为 0 。这里有两个方法可以实现,performClick()和onTouchEvent();这两个方法都可以实现监听,但是当实现了该View的onTouchEvent触摸监听,那么无论触摸监听的返回值是什么,performClick()都无法执行了,一个解决方案是在onTouchEvent()中调用performClick()方法:

 @Override
    public boolean performClick() {
        currentProgress=0;
        postInvalidate();
        return super.performClick();
    }

    //有触摸监听的时候performClick()会失效,所以要在触摸监听的方法中调用persormClick()方法
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction()==MotionEvent.ACTION_DOWN){
            Toast.makeText(context, "按下", Toast.LENGTH_SHORT).show();
            return  true;
        }else if (event.getAction()==MotionEvent.ACTION_MOVE){
            Toast.makeText(context, "移动", Toast.LENGTH_SHORT).show();
            return true;
        }else if (event.getAction()==MotionEvent.ACTION_UP){
            Toast.makeText(context, "松开", Toast.LENGTH_SHORT).show();
            performClick();
            return true;
        }
        return super.onTouchEvent(event);
    }
欧拉,关于自定义view 的记录就写这么多吧,各位看官有什么好的建议可以告诉我,谢谢!


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值