Android自定义View的三种方式:继承布局,继承原生控件,继承View

 自定义View非常的常用,也是Android开发的一项基本技能,自定义View有三种方式:继承布局,继承原生控件,继承View。

一、继承布局

先看效果图:


代码实现:

1.在layout文件夹中创建布局title_view.xml,这一步根据自己需要写,本例中的布局如下:


布局代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="?actionBarSize"
    android:id="@+id/title_view"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <ImageView
        android:id="@+id/back"
        android:src="@drawable/ic_action_arrow_left"
        android:padding="16dp"
        android:adjustViewBounds="true"
        android:layout_width="wrap_content"
        android:layout_height="match_parent" />
    <TextView
        android:id="@+id/title"
        tools:text="Title"
        android:textColor="@android:color/black"
        android:textSize="24sp"
        android:gravity="center"
        app:layout_constraintLeft_toRightOf="@id/back"
        android:layout_marginLeft="8dp"
        android:layout_marginStart="8dp"
        android:layout_width="wrap_content"
        android:layout_height="match_parent" />
    <ImageView
        android:id="@+id/menu"
        tools:src="@mipmap/ic_launcher"
        android:padding="8dp"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="match_parent" />
</android.support.constraint.ConstraintLayout>

设置了一个返回图标,一个标题,一个菜单图标。

2.在values文件夹中新建attrs.xml,在其中声明自定义属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomTitleView">
        <attr name="backgroundColor" format="color"/>
        <attr name="title" format="string"/>
        <attr name="menuSrc" format="reference"/>
    </declare-styleable>
</resources>

本例中自定义了背景颜色,标题,菜单资源三个属性,format是指该属性的取值类型,format取值一共有string,color,demension,integer,enum,reference,float,boolean,fraction,flag这几种,其中reference是指引用资源文件。

3.新建CustomTitleView文件,并重写三个构造方法,在public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr)方法中绑定布局,并将其他两个构造方法修改成调用public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr)方法。这样就实现了每个构造方法都会绑定我们刚才写的布局。当然这里也可以在每个构造方法中都写一遍绑定布局。



4.找到控件并获取属性


public class CustomTitleView extends ConstraintLayout{
    private ConstraintLayout clTitleView;
    private ImageView ivBack;
    private TextView tvTitle;
    private ImageView ivMenu;
    //背景色
    private int backgroundColor;
    //标题
    private String title;
    //菜单图片资源
    private int menuSrc;
    public CustomTitleView(Context context) {
        this(context,null);
    }

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

    public CustomTitleView(final Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //绑定布局
        LayoutInflater.from(context).inflate(R.layout.title_view,this);
        //找到控件
        clTitleView = findViewById(R.id.title_view);
        ivBack = findViewById(R.id.back);
        tvTitle = findViewById(R.id.title);
        ivMenu = findViewById(R.id.menu);

        //获取属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomTitleView,defStyleAttr,0);
        //获取背景色属性,默认透明
        backgroundColor = typedArray.getColor(R.styleable.CustomTitleView_backgroundColor, Color.TRANSPARENT);
        //获取标题属性
        title = typedArray.getString(R.styleable.CustomTitleView_title);
        //获取菜单图片资源属性,未设置菜单图片资源则默认为-1,后面通过判断此值是否为-1决定是否设置图片
        menuSrc = typedArray.getResourceId(R.styleable.CustomTitleView_menuSrc,-1);
        //TypedArray使用完后需手动回收
        typedArray.recycle();

        //设置属性
        clTitleView.setBackgroundColor(backgroundColor);
        tvTitle.setText(title);
        if(menuSrc!=-1){
            ivMenu.setImageResource(menuSrc);
        }
        //back图标点击事件,点击关闭activity
        ivBack.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                ((Activity)getContext()).finish();
            }
        });
    }
}


使用TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomTitleView,defStyleAttr,0);获取所有属性,再使用typedArray的getColor,getString,getResourceId方法分别获取format为color,string,reference的自定义属性。这些方法中有的需要传入两个参数,第二个参数就是没有设置此属性时的默认值,在绑定布局后找到控件,然后为控件设置属性。typedArray使用完之后需要手动调用typedArray.recycle()回收掉。

本例中我给返回图标添加了一个点击事件,点击就会将这个TitleView所在的activity关闭

5.在布局中使用CustomTitleView

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.sample.studycustomview.CustomTitleView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:backgroundColor="@color/colorPrimary"
        app:title="Title"
        app:menuSrc="@mipmap/ic_launcher"/>
</android.support.constraint.ConstraintLayout>

这样就完成了上面的效果图


二、继承原生控件

先看效果图


代码实现

1.在values文件夹下的attrs.xml中,声明自定义属性

<declare-styleable name="CustomProgressBar">
    <attr name="circleColor" format="color"/>
    <attr name="circleWidth" format="dimension"/>
    <attr name="startAngle" format="integer"/>
    <attr name="textSize" format="dimension"/>
    <attr name="textColor" format="color"/>
</declare-styleable>

2.新建CustomProgressbar,继承ProgressBar,重写三个构造方法,并获取自定义的属性

public class CustomProgressBar extends ProgressBar{
    private Paint mPaint;
    private int mCircleColor;//圆的颜色
    private int mCircleWidth;//圆的粗细
    private int mStartAngle;//起始角度
    private int mTextSize;//文字大小
    private int mTextColor;//文字颜色
    public CustomProgressBar(Context context) {
        this(context,null,0);
    }

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

    public CustomProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomProgressBar,defStyleAttr,0);
        //获取圆的颜色,默认黑色
        mCircleColor = typedArray.getColor(R.styleable.CustomProgressBar_circleColor,Color.BLACK);
        //获取圆的粗细,默认5dp
        mCircleWidth = (int) typedArray.getDimension(R.styleable.CustomProgressBar_circleWidth,FormatUtil.dp2px(context,5));
        //获取圆的起始角度,默认0度
        mStartAngle = typedArray.getInteger(R.styleable.CustomProgressBar_startAngle,0);
        //获取文字大小,默认18sp
        mTextSize = (int) typedArray.getDimension(R.styleable.CustomProgressBar_textSize,FormatUtil.sp2px(getContext(),18));
        //获取文字颜色,默认黑色
        mTextColor = typedArray.getColor(R.styleable.CustomProgressBar_textColor,Color.BLACK);
        typedArray.recycle();
        mPaint = new Paint();
    }
}

3.重写onMeasure,计算宽高

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);
    //如果宽高为固定dp 或 match_parent 直接使用以上获得的width和height即可,如果是wrap_content 需要单独处理
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    //默认宽度60dp,默认高度60dp
    if(widthMode == MeasureSpec.AT_MOST){
        width = getPaddingLeft() + getPaddingRight() + FormatUtil.dp2px(getContext(),60);
    }
    if(heightMode == MeasureSpec.AT_MOST){
        height = getPaddingTop() + getPaddingBottom() + FormatUtil.dp2px(getContext(),60);
    }
    setMeasuredDimension(width,height);
}

4.重写onDraw,画圆弧和文字

public class CustomProgressBar extends ProgressBar{
    private Paint mPaint;
    private int mCircleColor;//圆的颜色
    private int mCircleWidth;//圆的粗细
    private int mStartAngle;//起始角度
    private int mTextSize;//文字大小
    private int mTextColor;//文字颜色
    private RectF mRectF;//限制弧线的矩形
    private Rect mBounds;//测量文字的边缘
    public CustomProgressBar(Context context) {
        this(context,null,0);
    }

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

    public CustomProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomProgressBar,defStyleAttr,0);
        //获取圆的颜色,默认黑色
        mCircleColor = typedArray.getColor(R.styleable.CustomProgressBar_circleColor,Color.BLACK);
        //获取圆的粗细,默认5dp
        mCircleWidth = (int) typedArray.getDimension(R.styleable.CustomProgressBar_circleWidth,FormatUtil.dp2px(context,5));
        //获取圆的起始角度,默认0度
        mStartAngle = typedArray.getInteger(R.styleable.CustomProgressBar_startAngle,0);
        //获取文字大小,默认18sp
        mTextSize = (int) typedArray.getDimension(R.styleable.CustomProgressBar_textSize,FormatUtil.sp2px(getContext(),18));
        //获取文字颜色,默认黑色
        mTextColor = typedArray.getColor(R.styleable.CustomProgressBar_textColor,Color.BLACK);
        typedArray.recycle();
        mRectF = new RectF();
        mBounds = new Rect();
        mPaint = new Paint();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        //如果宽高为固定dp 或 match_parent 直接使用以上获得的width和height即可,如果是wrap_content 需要单独处理
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //默认宽度60dp,默认高度60dp
        if(widthMode == MeasureSpec.AT_MOST){
            width = getPaddingLeft() + getPaddingRight() + FormatUtil.dp2px(getContext(),60);
        }
        if(heightMode == MeasureSpec.AT_MOST){
            height = getPaddingTop() + getPaddingBottom() + FormatUtil.dp2px(getContext(),60);
        }
        setMeasuredDimension(width,height);
    }

    @Override
    protected synchronized void onDraw(Canvas canvas) {
        //1.画圆弧
        mPaint.setAntiAlias(true);
        //设置只画边框模式
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(mCircleColor);
        mPaint.setStrokeWidth(mCircleWidth);
        //限制圆弧的左、上、右、下坐标
        mRectF.set(getPaddingLeft(),getPaddingTop(),getWidth() - getPaddingRight(),getHeight() - getPaddingBottom());
        //画圆弧,传入RectF,开始角度,扫过角度,是否连接中心,画笔
        canvas.drawArc(mRectF,mStartAngle,getProgress()*1.0f/getMax()*360,false,mPaint);
        //2.画文字
        String strProgress = getProgress()+"%";
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        mPaint.setStrokeWidth(FormatUtil.dp2px(getContext(),1));
        //设置填充模式
        mPaint.setStyle(Paint.Style.FILL);
        //获取文字边缘
        mPaint.getTextBounds(strProgress,0,strProgress.length(),mBounds);
        //画文字,传入文字内容,文字左下角坐标,画笔
        canvas.drawText(strProgress
                ,(getWidth() - getPaddingLeft() - getPaddingRight() - mBounds.width())/2+getPaddingLeft()
                ,(getHeight() - getPaddingTop() - getPaddingBottom() - mBounds.height())/2+getPaddingTop()+mBounds.height(),mPaint);
    }
}

5.在布局中使用CustomProgressBar

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.sample.studycustomview.CustomProgressBar
        android:progress="60"
        android:max="100"
        android:padding="16dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:circleColor="@color/colorPrimary"
        app:circleWidth="3dp"
        app:startAngle="90"
        app:textSize="15sp"
        app:textColor="@color/colorPrimary"/>
</android.support.constraint.ConstraintLayout>

附上FormatUtil工具类,主要是为了dp、sp、px互相转换

public class FormatUtil {
    private FormatUtil()
    {
        /* cannot be instantiated */
        throw new UnsupportedOperationException("cannot be instantiated");
    }

    /**
     * Value of dp to value of px.
     *
     * @param dpValue The value of dp.
     * @return value of px
     */
    public static int dp2px(Context context,final float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    /**
     * Value of px to value of dp.
     *
     * @param pxValue The value of px.
     * @return value of dp
     */
    public static int px2dp(Context context,final float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }

    /**
     * Value of sp to value of px.
     *
     * @param spValue The value of sp.
     * @return value of px
     */
    public static int sp2px(Context context,final float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }

    /**
     * Value of px to value of sp.
     *
     * @param pxValue The value of px.
     * @return value of sp
     */
    public static int px2sp(Context context,final float pxValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValue / fontScale + 0.5f);
    }
}

这样就完成了上面的效果图

三、继承View

先看效果图:


代码实现:

1.在values文件夹下的attrs.xml中,声明自定义属性

<declare-styleable name="CustomAnimNumberView">
    <attr name="number" format="string"/>
    <attr name="numberColor" format="color"/>
    <attr name="numberSize" format="dimension"/>
    <attr name="animDuration" format="integer"/>
</declare-styleable>

2.新建CustomAnimNumberView,继承View,重写三个构造方法,并获取自定义的属性,其中用到的FormatUtil和上例中一样:

public class CustomAnimNumberView extends View {
    private Paint paint;
    private int number;//
    private int numberColor;//文字颜色
    private int numberSize;//文字大小
    private int animDuration;//动画时长
    
    public CustomAnimNumberView(Context context) {
        this(context,null);
    }

    public CustomAnimNumberView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CustomAnimNumberView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomAnimNumberView,defStyleAttr,0);
        number = typedArray.getInt(R.styleable.CustomAnimNumberView_number,0);
        numberColor = typedArray.getColor(R.styleable.CustomAnimNumberView_numberColor, Color.BLACK);
        numberSize = typedArray.getDimensionPixelSize(R.styleable.CustomAnimNumberView_numberSize,FormatUtil.sp2px(context,18));
        animDuration = typedArray.getInt(R.styleable.CustomAnimNumberView_animDuration,1000);
        typedArray.recycle();
        paint = new Paint();
    }
}

3.重写onMeasure,计算宽高

public class CustomAnimNumberView extends View {
    private Paint paint;
    private int number;//
    private int numberColor;//文字颜色
    private int numberSize;//文字大小
    private int animDuration;//动画时长
    private Rect bounds;//文字边缘
    public CustomAnimNumberView(Context context) {
        this(context,null);
    }

    public CustomAnimNumberView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CustomAnimNumberView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomAnimNumberView,defStyleAttr,0);
        number = typedArray.getInt(R.styleable.CustomAnimNumberView_number,0);
        numberColor = typedArray.getColor(R.styleable.CustomAnimNumberView_numberColor, Color.BLACK);
        numberSize = typedArray.getDimensionPixelSize(R.styleable.CustomAnimNumberView_numberSize,FormatUtil.sp2px(context,18));
        animDuration = typedArray.getInt(R.styleable.CustomAnimNumberView_animDuration,1000);
        typedArray.recycle();
        paint = new Paint();
        paint.setTextSize(numberSize);
        paint.setColor(numberColor);
        bounds = new Rect();
        paint.getTextBounds(String.valueOf(number),0,String.valueOf(number).length(),bounds);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        //如果宽高为固定dp 或 match_parent 直接使用以上获得的width和height即可,如果是wrap_content 需要单独处理
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if(widthMode == MeasureSpec.AT_MOST){
            width = getPaddingLeft() + getPaddingRight() + bounds.width();
        }
        if(heightMode == MeasureSpec.AT_MOST){
            height = getPaddingTop() + getPaddingBottom() + bounds.height();
        }
        setMeasuredDimension(width,height);
    }
}

4.重写onDraw,画动画的数字

public class CustomAnimNumberView extends View {
    private Paint paint;
    private int number;//
    private int numberColor;//文字颜色
    private int numberSize;//文字大小
    private int animDuration;//动画时长
    private ValueAnimator animation;//动画
    private Rect bounds;//文字边缘
    public CustomAnimNumberView(Context context) {
        this(context,null);
    }

    public CustomAnimNumberView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CustomAnimNumberView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomAnimNumberView,defStyleAttr,0);
        number = typedArray.getInt(R.styleable.CustomAnimNumberView_number,0);
        numberColor = typedArray.getColor(R.styleable.CustomAnimNumberView_numberColor, Color.BLACK);
        numberSize = typedArray.getDimensionPixelSize(R.styleable.CustomAnimNumberView_numberSize,FormatUtil.sp2px(context,18));
        animDuration = typedArray.getInt(R.styleable.CustomAnimNumberView_animDuration,1000);
        typedArray.recycle();
        paint = new Paint();
        paint.setTextSize(numberSize);
        paint.setColor(numberColor);
        bounds = new Rect();
        paint.getTextBounds(String.valueOf(number),0,String.valueOf(number).length(),bounds);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        //如果宽高为固定dp 或 match_parent 直接使用以上获得的width和height即可,如果是wrap_content 需要单独处理
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if(widthMode == MeasureSpec.AT_MOST){
            width = getPaddingLeft() + getPaddingRight() + bounds.width();
        }
        if(heightMode == MeasureSpec.AT_MOST){
            height = getPaddingTop() + getPaddingBottom() + bounds.height();
        }
        setMeasuredDimension(width,height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //传入左下角
        paint.getTextBounds(String.valueOf(number),0,String.valueOf(number).length(),bounds);
        canvas.drawText(String.valueOf(number),getPaddingLeft(),getPaddingTop()+bounds.height(),paint);
        if(animation == null){
            animation = ValueAnimator.ofInt(0,number);
            animation.setDuration(animDuration);
            animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    number = (int) animation.getAnimatedValue();
                    postInvalidate();
                }
            });
            animation.start();
        }
    }
}

5.在布局中使用CustomAnimNumberView

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.sample.studycustomview.CustomAnimNumberView
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        app:number="100"
        app:numberColor="@color/colorPrimary"
        app:numberSize="22sp"
        app:animDuration="2000"/>
</android.support.constraint.ConstraintLayout>

这样就完成了上面的效果图。

以上,就是自定义View的三种方式。

源码已上传:

https://github.com/wkxjc/StudyCustomView


猜你喜欢

转载自blog.csdn.net/alpinistwang/article/details/80414722