一、自定义View步骤

  • 在res/values/目录下新建attrs.xml文件,在该文件中定义和我们自定义View相关的属性。
  • 在构造函数中获取自定义的属性
  • 重写onMeasure()方法获测量自定义View的大小
  • 重写onDraw()方法绘制自定义View显示在界面上

二、下面一步一步的演示上面的步骤

先上效果图:


             

1、自定义属性
  • 自定义属性时,declare-styleable 后的name可以任意定义,只是为了区分,默认情况下该name为自定义View的类名,当然,这里也最好使用类名,这样容易辨别。
<resources>
            <declare-styleable name="CustomProgressBar">
                <!--圆圈内的文本-->
                <attr name="progressText" format="string"/>
                <!--圆圈内文本大小-->
                <attr name="progressTextSize" format="dimension"/>
                <!--圆圈内文本颜色-->
                <attr name="progressTextColor" format="color"/>
                <!--圆圈底层颜色-->
                <attr name="firstColor" format="color"/>
                <!--圆圈第二层颜色-->
                <attr name="secondColor" format="color"/>
                <!--圆圈宽度-->
                <attr name="progressWidth" format="float"/>
                <!--圆圈的内径-->
                <attr name="progressInnerRadius" format="float"/>
                <!--最大进度-->
                <attr name="max" format="integer"/>
                <!--当前进度-->
                <attr name="progress" format="integer"/>
            </declare-styleable>
        </resources>
2、获取自定义属性
  • 在自定义View的构造函数中获取自定义属性,一定要有AttributeSet attrs的构造函数,因为获取自定义属性全靠它了,如果仅仅是一个只有context的构造函数是不能获取得到的,所以这里一定要注意了。
public CustomProgressBar(Context context) {
            this(context, null);
        }

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

        public CustomProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            initAttributes(context, attrs, defStyleAttr);
            mPaint = new Paint();
        }
        /**
         * 初始化自定义属性
         *
         * @param context      上下文
         * @param attrs        属性集合
         * @param defStyleAttr 自定义属性样式
         */
        private void initAttributes(Context context, AttributeSet attrs, int defStyleAttr) {
            if (attrs != null) {
                TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomProgressBar, defStyleAttr, 0);
                try {
                    //第一层颜色
                    mFirstColor = a.getColor(R.styleable.CustomProgressBar_firstColor, mDefaultFirstColor);
                    //第二层颜色
                    mSecondColor = a.getColor(R.styleable.CustomProgressBar_secondColor, mDefaultSecondColor);
                    //圆圈内部显示的文本
                    mProgressText = a.getString(R.styleable.CustomProgressBar_progressText);
                    //圆圈宽度
                    mProgressWidth = a.getFloat(R.styleable.CustomProgressBar_progressWidth, mDefaultProgressWidth);
                    //圆圈内径
                    mProgressInnerRadius = a.getFloat(R.styleable.CustomProgressBar_progressInnerRadius, mDefaultInnerRadius);
                    //圆圈内文本颜色
                    mProgressTextColor = a.getColor(R.styleable.CustomProgressBar_progressTextColor, mDefaultProgressTextColor);
                    //圆圈内文本字体大小
                    mProgressTextSize = a.getDimension(R.styleable.CustomProgressBar_progressTextSize, mDefaultProgressTextSize);
                    //最大进度
                    max = a.getInt(R.styleable.CustomProgressBar_max, 0);
                    //当前进度
                    progress = a.getInt(R.styleable.CustomProgressBar_progress, 0);
                } finally {
                    a.recycle();
                }
            }
        }
3、重写onMeasure()方法
  • 其实这一步不是必须的,因为如果在xml文件中给定的layout_width和layout_height为确定的值的话,如固定的dp或者match_parent,那么onMeasure是不需要重写的,如果是wrap_content的话,默认为父容器剩余空间的最大值,除非是当没有给该View一个确定的大小的时候,重写该方法是有意义的,当用户使用该View时是用的是wrap_content时可以计算一个默认的大小出来。建议还是重写该方法,将多种情况都考虑进来。
/**
         * 测量组件的大小,设置的组件大小为wrapcontent的时候,组件的大小由半径决定
         * @param widthMeasureSpec
         * @param heightMeasureSpec
         */
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
            //测量组件的宽度
            if (widthMode == MeasureSpec.EXACTLY) {
                //如果是精准测量,即在使用的时候给的大小为match_parent或者是一个固定大小
                mWidth = widthSize;
            } else {
                int desire = (int) (mProgressInnerRadius * 2 //圆圈的内部直径
                        + mProgressWidth * 2); // 圆圈的左边宽度和右边的宽度的和
                if (widthMode == MeasureSpec.AT_MOST) { //wrap_content
                    mWidth = Math.min(desire, widthSize);
                }
            }

            //测量组件的高度
            if (heightMode == MeasureSpec.EXACTLY) {
                mHeight = heightSize;
            } else {
                int desire = (int) ( mProgressInnerRadius * 2 //圆圈的内部直径
                        + mProgressWidth * 2); // 圆圈的左边宽度和右边的宽度的和
                if (heightMode == MeasureSpec.AT_MOST) { //wrap_content
                    mHeight = Math.min(desire, heightSize);
                }
            }

            //保存测量的大小
            setMeasuredDimension(mWidth, mHeight);
        }
4、重写ondraw()方法
  • 该方法是用来绘制View在界面上显示的,绘图当然和Canvas这个类密不可分了。后面会单独出一篇文章来写canvas这个类的用法。
@Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            //画第一层圆
            int length = Math.min(mWidth,mHeight);
            mPaint.setColor(mFirstColor);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(mProgressWidth);
            mPaint.setAntiAlias(true);
            mPaint.setFilterBitmap(true);
            canvas.drawCircle(mWidth / 2, mHeight / 2, length / 2 - mProgressWidth / 2, mPaint);

            //画第二层圆弧
            mPaint.setColor(mSecondColor);
            RectF oval = new RectF();
            oval.left = mWidth/2 - length/2 + mProgressWidth/2;
            oval.top = mHeight/2 - length/2 + mProgressWidth/2;
            oval.right = mWidth/2 + length/2 - mProgressWidth/2;
            oval.bottom = mHeight/2 + length/2 - mProgressWidth/2;
            int angle;
            if (max <= 0 || progress < 0) {
                angle = 0;
            }else {
                angle = (int) (progress / max * 360);
            }
            canvas.drawArc(oval, -90, angle, false, mPaint);

           //画圆圈内的文本
            mPaint.setTextAlign(Paint.Align.CENTER);
            mPaint.setColor(mProgressTextColor);
            mPaint.setTextSize(mProgressTextSize);
            mPaint.setStyle(Paint.Style.FILL);
            if (!TextUtils.isEmpty(mProgressText)) {
                //如果有文本
                canvas.drawText(mProgressText, mWidth / 2, mHeight / 2 + mTextRect.height() / 2, mPaint);
            }
        }

  • 在画圆弧的时候使用到类RectF这个类,该类表示一个方形,可以确定一个图形的范围,在android所有的图形都是在一个方形中确定的。

通过以上4个步骤,我们的这个自定义View就定义完成了,下面来演示如何使用。


三、使用自定义View

  • 使用自定义View时和正常使用android内置View一样,但是值得注意的是这里需要写类的全名,即需要加上包名才可以,否则会报错。如果需要使用到自定义属性,那么就需要定义一个命名空间,不能使用android这个命名空间,因为这个是android自己定义的命名空间,我们自定义的属性没有在该命名空间中,需要重新定义一个命名空间。如何定义一个命名空间呢?复制”xmlns:android=”http://schemas.android.com/apk/res/android”这一个话,将xmln:android中的”android”修改为任意一个名称,不能是android,此处我修改为custom,这里custom就是我自定义的一个命名空间,当然还需要将末尾的res/android修改为res-auto。
<?xml version="1.0" encoding="utf-8"?>
        <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:custom="http://schemas.android.com/apk/res-auto"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

            <com.taiyang.commonui.view.CustomProgressBar
                android:id="@+id/progressbar"
                android:layout_width="200dp"
                android:layout_height="300dp"
                android:layout_centerInParent="true"
                custom:firstColor="#000088"
                custom:max="100"
                custom:progressTextColor="#567890"
                custom:progressTextSize="30sp"
                custom:progressWidth="40"
                custom:secondColor="#ff0000"/>
        </RelativeLayout>
  • 以上使用我们自定义的View就完成了,后面也会逐步深入的讲解如何自定义View和自定义ViewGroup
  • 下面将会附上该自定义View的完整源码
package com.taiyang.commonui.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

import com.taiyang.commonui.R;

/**
 * Created by taiyang5946 on 2016/1/10.
 * 这是一个圆形的进度条
 */
public class CustomProgressBar extends View {

    private final String TAG = "CustomProgressBar";

    /**圆圈第一层默认颜色*/
    private int mDefaultFirstColor = Color.BLACK;
    /**圆圈第二层默认颜色*/
    private int mDefaultSecondColor = Color.CYAN;
    /**圆圈内径默认半径为30,单位为:dp*/
    private float mDefaultInnerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30,
            getResources().getDisplayMetrics());
    /** 圆圈默认宽度为4,单位为:dp*/
    private float mDefaultProgressWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4,
            getResources().getDisplayMetrics());
    /**圆圈内文本默认颜色*/
    private int mDefaultProgressTextColor = Color.BLUE;
    /** 圆圈内文本默认字体大小*/
    private float mDefaultProgressTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14,
            getResources().getDisplayMetrics());
    /**圆圈第一层颜色*/
    private int mFirstColor;
    /**圆圈第二层颜色*/
    private int mSecondColor;
    /**圆圈内部显示文字*/
    private String mProgressText;
    /**圆圈宽度*/
    private float mProgressWidth;
    /**圆圈内径*/
    private float mProgressInnerRadius;
    /**圆圈内文本的颜色*/
    private int mProgressTextColor;
    /**圆圈内文本的字体大小*/
    private float mProgressTextSize;
    /**该组件的宽度 */
    private int mWidth;
    /**该组件的高度*/
    private int mHeight;
    /**画笔*/
    private Paint mPaint;
    /**圆圈内的文本约束*/
    private Rect mTextRect;
    /**最大进度*/
    private float max;
    /**当前进度 */
    private int progress;


    public CustomProgressBar(Context context) {
        this(context, null);
    }

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

    public CustomProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttributes(context, attrs, defStyleAttr);
        mPaint = new Paint();
    }

    /**
     * 测量圆圈内文本的约束,即宽和高
     */
    private void initProgressTextSize() {
        //如果有文本就进行测量
        if (!TextUtils.isEmpty(mProgressText)) {
            mPaint.setTextSize(mProgressTextSize);
            mTextRect = new Rect();
            //文本的宽度约束就在textRect中
            mPaint.getTextBounds(mProgressText, 0, mProgressText.length(), mTextRect);
        }
    }

    /**
     * 初始化自定义属性
     *
     * @param context      上下文
     * @param attrs        属性集合
     * @param defStyleAttr 自定义属性样式
     */
    private void initAttributes(Context context, AttributeSet attrs, int defStyleAttr) {
        if (attrs != null) {
            TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomProgressBar, defStyleAttr, 0);
            try {
                //第一层颜色
                mFirstColor = a.getColor(R.styleable.CustomProgressBar_firstColor, mDefaultFirstColor);
                //第二层颜色
                mSecondColor = a.getColor(R.styleable.CustomProgressBar_secondColor, mDefaultSecondColor);
                //圆圈内部显示的文本
                mProgressText = a.getString(R.styleable.CustomProgressBar_progressText);
                //圆圈宽度
                mProgressWidth = a.getFloat(R.styleable.CustomProgressBar_progressWidth, mDefaultProgressWidth);
                //圆圈内径
                mProgressInnerRadius = a.getFloat(R.styleable.CustomProgressBar_progressInnerRadius, mDefaultInnerRadius);
                //圆圈内文本颜色
                mProgressTextColor = a.getColor(R.styleable.CustomProgressBar_progressTextColor, mDefaultProgressTextColor);
                //圆圈内文本字体大小
                mProgressTextSize = a.getDimension(R.styleable.CustomProgressBar_progressTextSize, mDefaultProgressTextSize);
                //最大进度
                max = a.getInt(R.styleable.CustomProgressBar_max, 0);
                //当前进度
                progress = a.getInt(R.styleable.CustomProgressBar_progress, 0);
            } finally {
                a.recycle();
            }
        }
    }

    /**
     * 测量组件的大小,设置的组件大小为wrapcontent的时候,组件的大小由半径决定
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //测量组件的宽度
        if (widthMode == MeasureSpec.EXACTLY) {
            //如果是精准测量,即在使用的时候给的大小为match_parent或者是一个固定大小
            mWidth = widthSize;
        } else {
            int desire = (int) (mProgressInnerRadius * 2 //圆圈的内部直径
                    + mProgressWidth * 2); // 圆圈的左边宽度和右边的宽度的和
            if (widthMode == MeasureSpec.AT_MOST) { //wrap_content
                mWidth = Math.min(desire, widthSize);
            }
        }

        //测量组件的高度
        if (heightMode == MeasureSpec.EXACTLY) {
            mHeight = heightSize;
        } else {
            int desire = (int) ( mProgressInnerRadius * 2 //圆圈的内部直径
                    + mProgressWidth * 2); // 圆圈的左边宽度和右边的宽度的和
            if (heightMode == MeasureSpec.AT_MOST) { //wrap_content
                mHeight = Math.min(desire, heightSize);
            }
        }

        //保存测量的大小
        setMeasuredDimension(mWidth, mHeight);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画第一层圆
        int length = Math.min(mWidth,mHeight);
        mPaint.setColor(mFirstColor);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(mProgressWidth);
        mPaint.setAntiAlias(true);
        mPaint.setFilterBitmap(true);
        canvas.drawCircle(mWidth / 2, mHeight / 2, length / 2 - mProgressWidth / 2, mPaint);

        //画第二层圆弧
        mPaint.setColor(mSecondColor);
        RectF oval = new RectF();
        oval.left = mWidth/2 - length/2 + mProgressWidth/2;
        oval.top = mHeight/2 - length/2 + mProgressWidth/2;
        oval.right = mWidth/2 + length/2 - mProgressWidth/2;
        oval.bottom = mHeight/2 + length/2 - mProgressWidth/2;
        int angle;
        if (max <= 0 || progress < 0) {
            angle = 0;
        }else {
            angle = (int) (progress / max * 360);
        }
        canvas.drawArc(oval, -90, angle, false, mPaint);

       //画圆圈内的文本
        mPaint.setTextAlign(Paint.Align.CENTER);
        mPaint.setColor(mProgressTextColor);
        mPaint.setTextSize(mProgressTextSize);
        mPaint.setStyle(Paint.Style.FILL);
        if (!TextUtils.isEmpty(mProgressText)) {
            //如果有文本
            canvas.drawText(mProgressText, mWidth / 2, mHeight / 2 + mTextRect.height() / 2, mPaint);
           /* if ((mTextRect.width() * mTextRect.width() + mTextRect.height() * mTextRect.height())
                    <  //勾股定理
                    length * length) {
                //如果文本被约在圆圈内
                canvas.drawText(mProgressText, mWidth / 2, mHeight / 2, mPaint);
            } else {
                TextPaint textPaint = new TextPaint();
                String text = TextUtils.ellipsize(mProgressText,
                        textPaint,
                        mProgressInnerRadius * 2,
                        TextUtils.TruncateAt.MARQUEE).toString();
                canvas.drawText(text, mWidth / 2, mHeight / 2, mPaint);
            }*/
        }   
    }

    public String getProgressText() {
        return mProgressText;
    }

    /**
     * 设置圆圈内的文本
     * @param progressText
     */
    public void setProgressText(String progressText) {
        mProgressText = progressText;
        //测量圆圈内文本的大小,即宽和高
        initProgressTextSize();
        postInvalidate();
    }

    public void setMax(int max) {
        this.max = max;
        postInvalidate();
    }

    public void setProgress(int progress) {
        this.progress = progress;
        postInvalidate();
    }
}