Android开发博客

安卓开发教你如何自定义View并实现炫酷进度条

打开Android应用,映入眼帘的就是各种各样的控件,Button,TextView,EditText…,虽然控件数不胜数,但这些基本控件都有一个共同父类就是View。我们在进行Android开发的时候,要是想实现手机app上炫酷的控件该怎么办?这些控件在安卓框架中都不是内置的,但是善解人意的安卓框架除了提供一些基本的控件外,它还支持自定义View控件,以满足不同的功能需求。那么如何自定义View控件呢?别急,先看个圆形进度条效果图,增强一下学习的乐趣。
Android开发博客_第1张图片

看到了吧,这就是一个自定义的View控件,下面介绍一下如何实现自定义View控件,它主要有以下4个步骤:

1、自定义View的属性,并编写 attr.xml文件
2、在View的构造方法中获得我们自定义的属性,在自定义控件中进行读取(构造方法可以拿到attr.xml文件的各种属性值,而这些属性值可以在布局文件中设置)
3、重写onMesure() 和 onDraw()等方法。
4、在 layou布局文件中引用,同时引用命名空间(不引入则写全限定类名)

对于第3步,下面介绍onDraw() 和 onMeasure()的方法(基于安卓开发官方文档)

onDraw() 方法为您提供了一个 Canvas,您可以在其上实现所需的任何东西:2D 图形、其他标准或自定义组件、样式文本或您可以想到的其他任何东西。注意:这不适用于实现 3D 图形。如果您想要使用 3D 图形,则必须扩展 SurfaceView(而不是 View),并从单独的线程绘制。如需了解详情,请参阅 GLSurfaceViewActivity 示例。

onMeasure() 涉及更多。onMeasure() 是组件与其容器之间的渲染约定的关键部分。应该替换 onMeasure(),以高效且准确地报告其所含部分的测量结果。由于父级的限制要求(传入 onMeasure() 方法),以及一旦计算出宽度和高度就要使用测量的宽度和高度调用 setMeasuredDimension() 方法的要求,这变得稍微有些复杂。如果未通过已替换的 onMeasure() 方法调用此方法,则结果会在测量时出现异常。
简要来说,实现 onMeasure() 的方式如下所示:
1、调用已替换的 onMeasure() 方法时,应指定宽度和高度测量规范(widthMeasureSpec 和 heightMeasureSpec 参数都是表示尺寸的整数代码),应将这些规范视为应该生成的宽度和高度的限制要求。如需对这些规范可能要求的限制的完整参考,请参阅 View.onMeasure(int, int) 下的参考文档(此参考文档也清楚地说明了整个测量操作)。
2、组件的 onMeasure() 方法应计算渲染组件所需的测量宽度和高度。此方法应尽量符合传入的规范,尽管可以选择超出这些规范(在这种情况下,父级可以根据不同的测量规范选择执行哪些操作,例如裁剪、滚动、抛出异常或要求 onMeasure() 重试)。
3、计算宽度和高度后,必须使用计算得出的测量值调用 setMeasuredDimension(int width, int height) 方法。如果不执行此操作,则会导致系统抛出异常。

下面介绍该圆形进度条的具体实现(按上面的4个步骤)

1.定义attr.xml文件,这是个属性文件,定义了圆形进度条的各种属性,你也可以根据实际情况自行增加或减少属性,下面的innerBackground定义了圆形内部的背景颜色,outerBackground定义了圆形进度条边框的颜色,roundWidth定义了半径,progressTextSize和progressTextColor则分别定义了进度条内的文字大小颜色



    
    
        
        
        
        
        
    

2.在构造方法中获取attr.xml中定义的属性,这一步很关键,而且要注意,由于属性文件与自定义控件的类是绑定的,而且安卓系统规定了获取并设置控件属性的值时,要用“R.styleable.类名_属性名”来获取并设置该属性,具体见代码及注释

public CircleProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 获取attr.xml文件中的属性
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar);
        // 注意,获取innerBackground属性不是R.styleable.innerBackground,而是R.styleable.CicleProgressBar_innerBackground
        mInnerBackground = array.getColor(R.styleable.CircleProgressBar_innerBackground, mInnerBackground);
        mOuterBackground = array.getColor(R.styleable.CircleProgressBar_outerBackground, mOuterBackground);
        mRoundWidth = (int) array.getDimension(R.styleable.CircleProgressBar_roundWidth, dip2px(10));
        mProgressTextSize = array.getDimensionPixelSize(R.styleable.CircleProgressBar_progressTextSize,
                sp2px(mProgressTextSize));
        mProgressTextColor = array.getColor(R.styleable.CircleProgressBar_progressTextColor, mProgressTextColor);
        array.recycle();
        mInnerPaint = new Paint();
        mInnerPaint.setAntiAlias(true);
        mInnerPaint.setColor(mInnerBackground);
        mInnerPaint.setStrokeWidth(mRoundWidth);
        mInnerPaint.setStyle(Paint.Style.STROKE);
        mOuterPaint = new Paint();
        mOuterPaint.setAntiAlias(true);
        mOuterPaint.setColor(mOuterBackground);
        mOuterPaint.setStrokeWidth(mRoundWidth);
        mOuterPaint.setStyle(Paint.Style.STROKE);
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(mProgressTextColor);
        mTextPaint.setTextSize(mProgressTextSize);
    }

3.重写onMesure() 和 onDraw()方法,这两个方法可以设置自定义的属性和可以自己用canvas对象去画自定义的控件,进而实现漂亮的外观效果

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(Math.min(width, height), Math.min(width, height));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 先画内圆
        int width = getWidth()/2;
        int radius = width;
        canvas.drawCircle(radius,radius,radius-mRoundWidth/2,mInnerPaint);
        //画圆弧
        @SuppressLint("DrawAllocation")
        RectF rectF=new RectF(mRoundWidth/2,mRoundWidth/2, getWidth()-mRoundWidth/2,getHeight()-mRoundWidth/2);
        //如果进度为0就不绘制
        if (mProgress == 0) {
            return;
        }
        float percent=(float)mProgress/mMax;
        canvas.drawArc(rectF,0,360*percent,false,mOuterPaint);

        // 画进度文字
        String text = ((int) (percent * 100)) + "%";
        @SuppressLint("DrawAllocation")
        Rect rect=new Rect();
        mTextPaint.getTextBounds(text,0,text.length(),rect);
        float dx=getWidth()/2-rect.width()/2;
        @SuppressLint("DrawAllocation")
        Paint.FontMetricsInt fontMetricsInt=new Paint.FontMetricsInt();
        int dy=(fontMetricsInt.bottom - fontMetricsInt.top)/2-fontMetricsInt.bottom;
        float baseLine=getHeight()/2+dy;
        canvas.drawText(text,dx,baseLine,mTextPaint);

    }

4.在main_activity.xml布局文件中使用自定义的控件CircleProgressBar,自定义控件的使用和安卓系统原生自带的控件使用方法是一样的,但是要注意,由于是自定义的,如果你使用控件直接用类名是不行的,安卓系统不能自动识别,这时你可以自己引入名称空间或者在标签上写全限定类名,这里采用的是第二种,并且可以在下面设置你已经定义好的属性值



    
    

再附加一个项目的目录结构图
Android开发博客_第2张图片

注意事项

1、在自定义类中获取自定义属性(attr.xml文件中的)时,要注意在属性的前面加上类名,诸如这种格式:CicleProgressBar_innerBackground,并在类名和属性名中间用“_”分割。
2、使用自定义的控件时,要么引入命名空间,要么在写控件的全限定类名,不能只写一个类名,这样无法解析,因为解析xml文档是通过反射来的,没有全限定类名是无法知道它属于谁的。

安卓系统其实提供了很强大的自定义控件功能,通过自定义控件,可以满足各种需求,制作各种符合实际场景的控件,当然这些控件不仅仅可以自定义样式,你也可以自定义事件,从而以弥补自带控件不够强的缺陷。
作者:廖启华
原文链接

你可能感兴趣的:(andriod)