自定义View构造函数,知多少?

4,263 阅读8分钟

构造函数

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) {

本文先以TextView为例理论讲解这四个构造函数如何使用,再用一个自定义View来进行实战。

一个参数的构造函数

一个参数的构造函数是在代码中创建的,例如把一个TextView添加到Activity布局中

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 创建TextView对象,并设置属性
        TextView textView = new TextView(this);
        textView.setText(R.string.app_name);
        textView.setTextSize(30);
        // 把TextView对象添加到布局中
        setContentView(textView);
    }
}

从这个例子中可以发现,使用一个参数的构造函数创建对象后,需要手动调用设置属性的方法。

两个参数的构造函数

现在假设在一个布局中声明了一个TextView控件,代码如下

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:textSize="24sp" />

</RelativeLayout>

系统在解析这个XML布局的时候,会使用TextView两个参数的构造函数,代码如下

    public TextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.textViewStyle);
    }

第二个参数AttributeSet attrs就代表了在XML中为TextView声明的属性集,我们可以利用它解析出声明的属性值,例如android:textandroid:textSize

三个参数的构造函数

我们知道,可以通过Theme全局控制控件的样式,其中的原理就是使用三个参数的构造函数

三个参数构造函数的使用方式有点特别,一般是二个参数的构造函数中传入一个Theme中的属性

    public TextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.textViewStyle);
    }

    public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

可以看到,TextView两个参数的构造函数调用了三个参数的构造函数,而第三个参数使用的值就是Theme中的com.android.internal.R.attr.textViewStyle属性值。

如果我们想覆盖Theme中的com.android.internal.R.attr.textViewStyle,就需要自定义这个属性的值,代码如下

    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <!--定义TextView使用的属性-->
        <item name="android:textViewStyle">@style/MyTextViewStyle</item>
    </style>
    
    <!--自定义TextView的颜色-->
    <style name="MyTextViewStyle" parent="Widget.AppCompat.TextView">
        <item name="android:textColor">@color/colorAccent</item>
    </style>

四个参数的构造函数

Theme是全局控制样式的,但是时候我们只想为某几个TextView单独定义样式,那就得使用四个参数的构造函数。

四个参数构造函数的使用方式,一般是在三个参数的构造函数中调用,并传入自定义Style

首先,在styles.xml中声明一个TextView使用的Style

    <style name="CustomTextViewStyle" parent="Widget.AppCompat.TextView" >
        <item name="android:textColor">@color/colorPrimaryDark</item>
    </style>

这个Style只是简单定义了TextView的文本颜色。

然后自定义一个继承自TextView的控件

public class MyTextView extends TextView {
    public MyTextView(Context context) {
        this(context, null);
    }

    public MyTextView(Context context, @Nullable AttributeSet attrs) {
        // 注意,这里的第三个参数为0
        this(context, attrs, 0);
    }

    public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, R.style.CustomTextViewStyle);
    }

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

在两个参数的构造函数中,调用了三个参数的构造函数,但是传入的第三个参数的值为0。至于是什么原因,后面会讲到。

在三个参数的构造函数中,调用四个参数的构造函数,第四个参数需要传入自定义的Style

属性值的覆盖规则

既然有这么多地方能控制属性值,那么就有个有限顺序。其实可以从四个参数的obtainStyledAttributes()方法中看到这个规则

    public final TypedArray obtainStyledAttributes(
            AttributeSet set, int[] attrs, int defStyleAttr, 
            int defStyleRes) {}

第一个参数AttributeSet set指的是XML中声明的属性集。

第三个参数int defStyleAttr指的是Theme中的控制控件的属性。

第四个参数int defStyleRes指的是自定义的Style

那么最简单属性值获取的优先规则就是第一个参数,第三个参数,第四个参数。

如果在XML给控件使用style属性呢?它的优先级是介于第一个参数和第三个参数之间。

那么最终的优先规则如下

  1. XML中属性
  2. XML中style属性
  3. Theme中属性
  4. 自定义Style

自定义View

现在,通过一个自定义View演示四个构造函数如何使用。

自定义View名字叫SimpleView,在这个控件中,只简单绘制一个圆,并且它有一个自定义属性,可以控制圆的颜色。

首先声明SimpleView使用的自定义属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--SimpleView的自定义属性,控制圆的颜色-->
    <declare-styleable name="SimpleView">
        <attr name="circleColor" format="color|reference" />
    </declare-styleable>
</resources>

然后,在两个参数的构造函数中解析这个属性颜色值,并使用这个颜色值绘制圆

public class SimpleView extends View {
    Paint mPaint = new Paint();
    int mColor = Color.BLACK;
    int mCenterX, mCenterY;
    int mRadius;

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

    public SimpleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        // 获取XML中声明的属性集,并解析属性
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SimpleView);
        for (int i = 0; i < a.getIndexCount(); i++) {
            int attrIndex = a.getIndex(i);
            if (attrIndex == R.styleable.SimpleView_circleColor) {
                mColor = a.getColor(attrIndex, Color.BLACK);
            }
        }
        a.recycle();
        mPaint.setColor(mColor);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        mCenterX = w / 2;
        mCenterY = h / 2;
        mRadius = w < h ? w / 4 : h / 4;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(mCenterX, mCenterY, mRadius, mPaint);
    }
}

SimpleView一个参数的构造函数调用了两个参数的构造函数,在两个参数的构造函数中解析了自定义的属性circleColor

那么现在,在XML中使用SimpleView,然后设置circleColor属性,就可以绘制你想要的颜色的圆

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <com.umx.viewconstructortest.SimpleView
        app:circleColor="@color/colorAccent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

通过Theme控制样式

现在,我们想通过Theme的属性控制SimpleView样式。按照之前所说,那就需要一个属性并在Theme中声明,然后通过三个参数的构造函数来完成Theme的控制。

首先定义Theme使用的属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--SimpleView的自定义属性-->
    <declare-styleable name="SimpleView">
        <attr name="circleColor" format="color|reference" />
    </declare-styleable>

    <!--在Theme中控制SimpleView样式的属性-->
    <attr name="SimpleViewStyle" format="reference" />
</resources>

SimpleViewStyle就是SimpleView要使用的Theme的属性。

现在,我们在Theme中加入这个属性

<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <!--SimpleViewStyle控制SimpleView样式-->
        <item name="SimpleViewStyle">@style/MySimpleViewStyle</item>
    </style>

    <style name="MySimpleViewStyle">
        <!--统一控制SimpleView使用的颜色为黑色-->
        <item name="circleColor">#000000</item>
    </style>
</resources>

这一切准备就绪后,就来实现三个参数的构造函数

public class SimpleView extends View {

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

    public SimpleView(Context context, @Nullable AttributeSet attrs) {
        // 调用三个参数的构造函数,传入的第三个参数为Theme中声明的SimpleViewStyle属性
        this(context, attrs, R.attr.SimpleViewStyle);
    }

    public SimpleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 注意,这里使用的obtainStyledAttributes方法有四个参数
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SimpleView, defStyleAttr, 0);
        for (int i = 0; i < a.getIndexCount(); i++) {
            int attrIndex = a.getIndex(i);
            if (attrIndex == R.styleable.SimpleView_circleColor) {
                mColor = a.getColor(attrIndex, Color.BLACK);
            }
        }
        a.recycle();
        mPaint.setColor(mColor);
    }
}

在两个参数的构造函数中调用了三个参数的构造函数,并且第三个参数传入的就是刚才自定义且在Theme中声明的属性。然后在三个参数的构造函数中,为了使用刚刚传入的Theme属性,必须使用有四个参数的obtainStyledAttributes()方法,这里一定要注意。

然后在三个参数的构造函数中完成了自定义属性的解析,取代两个参数的构造函数的工作。

自定义Style控制样式

如果你想自定义一个拥有特殊样式的SimpleView,按照前面的分析,你需要使用四个参数的构造函数,并且需要继承SimpleView

首先,需要在SimpleView中加入四个参数的构造函数

public class SimpleView extends View {
    public SimpleView(Context context) {
        this(context, null);
    }

    public SimpleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, R.attr.SimpleViewStyle);
    }

    public SimpleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public SimpleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        // 注意,这里也是使用四个参数的obtainStyledAttributes()方法
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SimpleView, defStyleAttr, defStyleRes);
        for (int i = 0; i < a.getIndexCount(); i++) {
            int attrIndex = a.getIndex(i);
            if (attrIndex == R.styleable.SimpleView_circleColor) {
                mColor = a.getColor(attrIndex, Color.BLACK);
            }
        }
        a.recycle();
        mPaint.setColor(mColor);
    }
}    

实现四个参数的构造函数非常简单,只需要在三个参数的构造函数中调用四个参数的构造函数,然后把第四个参数传入0即可。同时我把属性的解析移到了四个参数的构造函数中。

现在,定义一个继承子SimpleView的类CustomSimpleView,并且传入自定义的样式

public class CustomSimpleView extends SimpleView {
    
    public CustomSimpleView(Context context) {
        this(context, null);
    }

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

    public CustomSimpleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, R.style.CustomSimpleViewStyle);
    }

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

CustomSimpleView的两个参数的构造函数中,调用了三个参数的构造函数,然而第三个参数传入的0,也就是说CustomSimpleView不想使用Theme控制它的样式。

CustomSimpleView的三个参数的构造函数中,调用了四个参数的构造函数,并且第四个参数传入了自定义的样式R.style.CustomSimpleViewStyle

CustomSimpleViewStyle自定义样式如下

<resources>
    <style name="CustomSimpleViewStyle">
        <item name="circleColor">#ff0000</item>
    </style>
</resources>

如此一来,CusstomSimpleView就不能通过Theme控制样式,而使用的是自定义的Style控制样式。

结束

平常我们可能用不到这些知识,但是在写系统控件的时候,这个就不能忽视,尤其在我们自定义系统的Theme的时候,就显得尤为重要。