自定义View是Android开发中不可避开的一个重点,也是难点。一方面自定义View涉及到的的知识点较多,从基础的坐标、Paint和Canvas的使用到Drawable、动画,更复杂的可能还会涉及Shader以及混合模式Xfermode等等(当然可能不止如此);另一方面自定义View的过程比较复杂,要了解View的测量、布局、绘制流程,重写相应的方法。自己一直有个想法,想把自己在学习或工作过程中所学到的关于自定义View的知识点梳理一下,记录下来,让自己对自定义View的完整流程有一个清晰的认识。

       就像罗马不是一天建成的一样,再复杂的UI也是由一个一个基础的方法绘制而成的,因此熟练掌握基本功是非常重要的,另外,不要急着写代码,要先学会将一个复杂的过程分解,解耦成一个个子过程,形成一个大致的思路,然后再开始敲代码。

       下面举一个例子,假设要实现下面的圆形进度条:

Android 实现自定义协议接口_自定义

        首先,学会将一个复杂的过程分解,拆分一下圆形进度条的绘制过程:

       1、先绘制一个圆圈,作为整个圆形进度条的背景(上图采用的背景颜色是透明的所以看不出);

       2、绘制外层的圆弧(白色部分),表示完整的进度;

       3、绘制外层表示当前进度的圆弧(黄色部分);

       4、绘制圆圈中间表示进度的文本。

       这样就可以将一个看起来比较复杂的过程解耦成一个个子过程,子过程实现起来就没有那么复杂了。当然,这里举的例子可能谈不上复杂,只是想说明一下一个理念,学会将一个复杂的过程分解成一个一个简单的子过程。

       在形成一个大体的思路后就可以开始动手写代码了!

    第一,确定自定义View要提供哪些自定义属性:

       通常自定义View的时候会提供一些可配置的属性让开发人员可以直接在xml布局中配置而不需要去修改View的Java类文件。以上图为例,可配置属性包括:背景色、进度条的颜色及宽度、进度条文本的字体大小及颜色、百分号的字体大小及颜色等等,完整如下:

private int mMax;//进度最大值
    private int mProgress;//当前进度
    private float mRingWidth;//圆环宽度
    private int mRingBackgroung;//圆环背景色
    private int mRingColor;//圆环颜色
    private float mRingPadding;//圆环与圆圈的边距
    private int mCircleColor;//圆圈背景色
    private float mProgressTextSize;//进度文本字体大小
    private int mProgressTextColor;//进度文本字体颜色
    private String mSuffixText;//百分号文本
    private float mSuffixTextSize;//百分号字体大小
    private int mSuffixTextColor;//百分号字体颜色
    private float mSuffixTextPadding;//百分号文本和进度文本的边距

        然后在res\values目录下新建一个attrs.xml并配置相应属性名称及类型:

<declare-styleable name="CircleProgress">
        <attr name="max" format="integer"/>
        <attr name="progress" format="integer"/>
        <attr name="ring_width" format="dimension"/>
        <attr name="ring_color" format="color"/>
        <attr name="ring_backgroung" format="color"/>
        <attr name="ring_padding" format="dimension"/>
        <attr name="circle_color" format="color"/>
        <attr name="progress_textSize" format="dimension"/>
        <attr name="progress_textColor" format="color"/>
        <attr name="suffix_text" format="string"/>
        <attr name="suffix_textSize" format="dimension"/>
        <attr name="suffix_textColor" format="color"/>
        <attr name="suffix_textPadding" format="dimension"/>
    </declare-styleable>

        这样所有要提供给外界进行配置的属性就声明完成了。

     第二,继承View并覆盖构造方法,在构造方法中获取自定义属性并预设默认值:

        View的构造函数有四个:

public View(Context context);
    public View(Context context, AttributeSet attrs);
    public View(Context context, AttributeSet attrs, int defStyleAttr);
    public View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes);

       但是一般来说,只需要覆盖前面两个构造方法就可以了,当在代码中创建View的时候使用View(Context),当从XML中引用View的时候使用View(Context,AttributeSet)。以上图为例,需要在第2个构造函数中通过Context.obtainStyledAttributes方法获取自定义属性并设置默认值,代码如下:

public CircleProgress(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initPaint();
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleProgress);
        mMax = ta.getInteger(R.styleable.CircleProgress_max, 100);
        mProgress = ta.getInteger(R.styleable.CircleProgress_progress, 0);
        setMax(mMax);
        setProgress(mProgress);
        mRingWidth = ta.getDimension(R.styleable.CircleProgress_ring_width, SizeUtil.dp2px(getResources(), 15));
        mRingColor = ta.getColor(R.styleable.CircleProgress_ring_color, Color.parseColor("#ffe401"));
        mRingBackgroung = ta.getColor(R.styleable.CircleProgress_ring_backgroung, Color.parseColor("#eeeeee"));
        mRingPadding = ta.getDimension(R.styleable.CircleProgress_ring_padding, SizeUtil.dp2px(getResources(), 0));
        mCircleColor = ta.getColor(R.styleable.CircleProgress_circle_color, Color.parseColor("#ffffff"));
        mProgressTextSize = ta.getDimension(R.styleable.CircleProgress_progress_textSize, SizeUtil.dp2px(getResources(), 25));
        mProgressTextColor = ta.getColor(R.styleable.CircleProgress_progress_textColor, Color.parseColor("#ffe401"));
        mSuffixText = ta.getString(R.styleable.CircleProgress_suffix_text);
        if (TextUtils.isEmpty(mSuffixText))
            mSuffixText = "%";
        mSuffixTextSize = ta.getDimension(R.styleable.CircleProgress_suffix_textSize, SizeUtil.dp2px(getResources(), 15));
        mSuffixTextColor = ta.getColor(R.styleable.CircleProgress_suffix_textColor, Color.parseColor("#ffe401"));
        mSuffixTextPadding = ta.getDimension(R.styleable.CircleProgress_suffix_textPadding, SizeUtil.dp2px(getResources(), 2));
    }
    第三,如果有必要,重写onMeasure方法让View支持wrap_content属性:

       View类默认的onMeasure方法只支持EXACTLY模式,所以如果自定义控件的时候不重写onMeasure()方法,就只能使精确值模式,控件可以响应具体的宽高值和match_parent属性,不支持wrap_content,如果要让自定义View支持wrap_content属性,就必须重写onMeasure()方法指定wrap_content时的大小。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }

    private int measureWidth(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = 200;//指定宽度wrap_content时为200px
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    private int measureHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = 100;//指定高度wrap_content时为100px
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }
    第四,重写onDraw方法,绘制自定义View:

       以上图为例,已经知道了怎么去绘制圆形进度条了,那么接下来要做的就是调用Canvas的相应方法去完成就可以了,如canvas.drawCircle、canvas.drawArc等等,代码如下:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setColor(mCircleColor);
        canvas.drawCircle(mWidth / 2.0f, mHeight / 2.0f, mWidth / 2.0f, mCirclePaint);
        mArcPaint.setAntiAlias(true);
        mArcPaint.setColor(mRingBackgroung);
        mArcPaint.setStyle(Paint.Style.STROKE);
        mArcPaint.setStrokeWidth(mRingWidth);
        canvas.drawArc(mRectf, 0, 360, false, mArcPaint);
        mArcPaint.setColor(mRingColor);
        mArcPaint.setStrokeCap(Paint.Cap.ROUND);
        canvas.drawArc(mRectf, -90, -(float) mProgress / getMax() * 360, false, mArcPaint);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextSize(mProgressTextSize);
        mTextPaint.setColor(mProgressTextColor);
        String value = String.valueOf(getProgress());
        mTextPaint.setColor(mProgressTextColor);
        mTextPaint.setTextSize(mProgressTextSize);
        float textHeight = mTextPaint.descent() + mTextPaint.ascent();
        float textBaseline = (getHeight() - textHeight) / 2.0f;
        canvas.drawText(value,
                (getWidth() - mTextPaint.measureText(value)) / 2.0f,
                textBaseline, mTextPaint);
        mTextPaint.setTextSize(mSuffixTextSize);
        mTextPaint.setColor(mSuffixTextColor);
        float suffixHeight = mTextPaint.descent() + mTextPaint.ascent();
        canvas.drawText(mSuffixText,
                getWidth() / 2.0f + mTextPaint.measureText(value)
                        + mSuffixTextPadding, textBaseline + textHeight
                        - suffixHeight, mTextPaint);
    }
    第五,在XML布局文件中引入自定义View:

       这里说一下,在XML中使用自定义属性时要创建一个新的命名空间,如在根布局可以声明:

xmlns:app="http://schemas.android.com/apk/res-auto"
<com.elestic.CircleProgress
        android:id="@+id/circle_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:progress="14"
        app:ring_width="20dp"
        app:ring_color="#ff9920"
        app:circle_color="@android:color/transparent"
        app:suffix_textSize="32dp"
        app:suffix_textColor="#ff9920"
        app:progress_textSize="60dp"
        app:progress_textColor="#ff9920" />
完整Demo地址最后,总结一下:

       虽然自定义View有的很复杂,有的却很简单,但是总体来说,过程大同小异,简单的基本流程可以归纳如下:

       第一、自定义布局属性,这个在res\values\attrs.xml中配置属性名称和类型;

       第二、继承View,覆盖构造方法,获取并设置自定义布局属性的默认值;

       第三、如果有必要,重写onMeasure方法,指定wrap_content时的大小;

       第四、重写onDraw方法,绘制控件;

       第五、在XML布局文件中引用自定义View,在代码中实现View的动画过程或属性的动态设置等。

在后面的系列中,我会记录下自己学习自定义View的体会,积水成河。

       Android自定义View(一)坐标系解读

       Android自定义View(二)屏幕尺寸信息和单位转换

       Android自定义View(三)自定义属性AttributeSet

       Android自定义View(四)Paint的常用方法

       Android自定义View(五)Canvas的常用方法

       Android自定义View(六)插值器和估值值

       Android自定义View(七)属性动画的使用

      。。。。。。