参考:
自定义视图(View
)是 Android
开发的一个进阶内容。随着开发的深入,肯定会出现系统提供的基础控件不符合需求的情况。一方面通过组合基础控件以形成新的布局,另一方面可以通过自定义控件的方式来更加灵活的实现需求
自定义视图涉及到 Android
系统许多方面的内容,下面根据自己的理解顺序来讲一讲如何自定义视图
视图创建
可以通过代码(from code
)或者布局文件(from XML layout file
)增加视图,根据不同的载入方式,将调用不同的构造函数
构造函数
View
类提供了 4
种构造函数,其分别有 1-4
个参数:
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
单个参数:
/** * Simple constructor to use when creating a view from code. * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. */ public View(Context context) { ... }
当视图在代码中创建时,调用此构造函数
两个参数:
/** * Constructor that is called when inflating a view from XML. This is called * when a view is being constructed from an XML file, supplying attributes * that were specified in the XML file. This version uses a default style of * 0, so the only attribute values applied are those in the Context's Theme * and the given AttributeSet. * * <p> * The method onFinishInflate() will be called after all children have been * added. * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. * @see #View(Context, AttributeSet, int) */ public View(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); }
当视图在
xml
布局文件中定义时,调用此构造函数三个参数:
/** * Perform inflation from XML and apply a class-specific base style from a * theme attribute. This constructor of View allows subclasses to use their * own base style when they are inflating. For example, a Button class's * constructor would call this version of the super class constructor and * supply <code>R.attr.buttonStyle</code> for <var>defStyleAttr</var>; this * allows the theme's button style to modify all of the base view attributes * (in particular its background) as well as the Button class's attributes. * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. * @param defStyleAttr An attribute in the current theme that contains a * reference to a style resource that supplies default values for * the view. Can be 0 to not look for defaults. * @see #View(Context, AttributeSet) */ public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); }
该构造函数同样表示当自定义视图在布局文件中定义时使用,不过它和上一个构造函数的区别是多了一个
defStyleAttr
属性。当自定义视图继承自View
子类,比如按钮(button
),调用此构造函数,属性defStyleAttr
就表示按钮的基本样式属性四个参数:
/** * Perform inflation from XML and apply a class-specific base style from a * theme attribute or style resource. This constructor of View allows * subclasses to use their own base style when they are inflating. * <p> * When determining the final value of a particular attribute, there are * four inputs that come into play: * <ol> * <li>Any attribute values in the given AttributeSet. * <li>The style resource specified in the AttributeSet (named "style"). * <li>The default style specified by <var>defStyleAttr</var>. * <li>The default style specified by <var>defStyleRes</var>. * <li>The base values in this theme. * </ol> * <p> * Each of these inputs is considered in-order, with the first listed taking * precedence over the following ones. In other words, if in the * AttributeSet you have supplied <code><Button * textColor="#ff000000"></code> * , then the button's text will <em>always</em> be black, regardless of * what is specified in any of the styles. * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. * @param defStyleAttr An attribute in the current theme that contains a * reference to a style resource that supplies default values for * the view. Can be 0 to not look for defaults. * @param defStyleRes A resource identifier of a style resource that * supplies default values for the view, used only if * defStyleAttr is 0 or can not be found in the theme. Can be 0 * to not look for defaults. * @see #View(Context, AttributeSet, int) */ public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { ... }
最后一个构造函数是从
Android 5.0(API 21)
开始加入的,如果minSdkVersion
小于21
的话,自定义视图不需要重载它
通常的使用方式是在自定义视图中重载前 3
个构造函数:
public class MyView extends View {
public MyView(Context context) {
// super(context);
this(context, null);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
// super(context, attrs);
this(context, attrs, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
构造结束判断
当自定义视图从布局文件中载入结束后,会调用方法 onFinishInflate
:
/**
* Finalize inflating a view from XML. This is called as the last phase
* of inflation, after all child views have been added.
*
* <p>Even if the subclass overrides onFinishInflate, they should always be
* sure to call the super method, so that we get called.
*/
@CallSuper
protected void onFinishInflate() {
}
Note:需要调用超类方法(super method
)
测试代码如下:
public class MyView extends View {
private static final String TAG = "MyView";
public MyView(Context context) {
// super(context);
this(context, null);
Log.e(TAG, "MyView: one");
}
public MyView(Context context, @Nullable AttributeSet attrs) {
// super(context, attrs);
this(context, attrs, 0);
Log.e(TAG, "MyView: two");
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.e(TAG, "MyView: three");
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
Log.e(TAG, "onFinishInflate: ");
}
}
布局文件定义:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.zj.viewtest.MainActivity">
<com.zj.viewtest.MyView
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@android:color/black" />
</LinearLayout>
运行,日志如下:
由图可知,自定义视图调用顺序如下:
public MyView(Context context, @Nullable AttributeSet attrs)
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
protected void onFinishInflate()
自定义属性
当自定义视图在布局文件中定义时,可以根据需要自定义属性
自定义属性分为 4
个基本步骤:
- 使用
<declare-styleable>
资源元素定义自定义属性(Define custom attributes for your view in a <declare-styleable> resource element
) - 在布局文件中使用自定义属性(
Specify values for the attributes in your XML layout
) - 从构造函数中解析自定义属性(
Retrieve attribute values at runtime
) - 在绘图中使用自定义属性(
Apply the retrieved attribute values to your view
)
定义
自定义属性使用资源元素(resource element
)<declare-styleable>
,通常将其放置在 res/values/attrs.xml
文件中(工程如果没有此资源文件,新建一个):
元素 <declare-styleable>
作为 <resource>
元素的子元素,包含属性 name
,指明此自定义属性集:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomView" ></declare-styleable>
</resources>
Note:通常将属性 name
赋值为使用的自定义视图名
在元素 <declare-styleable>
下声明子元素 <attr>
,每个子元素表示一个自定义属性
元素 <attr>
需要声明两个属性:name
和 format
属性 name
表示该属性名称,属性 format
表示该属性格式,共有 10
种格式:
enum
boolean
color
dimension
flag
float
fraction
integer
reference
string
定义 3
个自定义属性,分别表示文本内容,文本大小和文本颜色,示例如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyView">
<attr name="text" format="string" />
<attr name="textSize" format="dimension" />
<attr name="textColor" format="color" />
</declare-styleable>
</resources>
也可以在根元素 <resource>
下先定义好 <attr>
元素,然后在元素 <declare-stylebale>
中使用:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="text" format="string" />
<attr name="textSize" format="dimension" />
<attr name="textColor" format="color" />
<declare-styleable name="MyView">
<attr name="text" />
<attr name="textSize" />
<attr name="textColor" />
</declare-styleable>
</resources>
赋值
定义完成后,就可以在布局文件的自定义组件中使用这些属性
首先需要在根元素中定义自定义属性的命名空间(namespace
),使用指令 xmlns:name=value
,其中 name
为命名空间名称(自定义),value
为命名空间,值为 http://schemas.android.com/apk/res/[your package name]
,比如:
xmlns:custom="http://schemas.android.com/apk/com.zj.viewtest"
也可使用系统默认的
xmlns:app="http://schemas.android.com/apk/res-auto"
在布局文件中使用自定义视图时,需要完全限定名(the fully qualified name
),即完整的名称,比如 com.example.MyView
;如果自定义视图是内部类,也需要完整名称,比如文件 MyView.java
中的内部类 InnerView
,其名为 com.example.MyView&InnerView
自定义属性的使用方式和原生属性无异,唯一有的区别可能就是没有自动提示了:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:custom="http://schemas.android.com/apk/res/com.zj.viewtest"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.zj.viewtest.MainActivity">
<com.zj.viewtest.MyView
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@color/colorAccent"
custom:text="Hello World"
custom:textColor="@android:color/holo_blue_bright"
custom:textSize="16sp" />
</LinearLayout>
获取
当自定义视图在布局文件中定义时,构造对象时使用的构造函数为:
public View(Context context, @Nullable AttributeSet attrs)
布局文件中使用的属性均放置在参数 attrs
中,Android
推荐不直接从 AttributeSet
对象中取出属性值,它列出两点原因:
- Resource references within attribute values are not resolved
- Styles are not applied
将参数 attrs
输入函数 obtainStyledAttributes
,然后从这里获取属性值
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.e(TAG, "MyView: three");
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0);
try {
text = a.getString(R.styleable.MyView_text);
textSize = a.getDimension(R.styleable.MyView_textSize, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
textColor = a.getColor(R.styleable.MyView_textColor, Color.BLACK);
} finally {
a.recycle();
}
Log.e(TAG, "MyView: text = " + text);
Log.e(TAG, "MyView: textSize = " + textSize);
Log.e(TAG, "MyView: textColor = " + textColor);
}
Note 1:属性值的写法 - R.styleable.自定义属性_属性名
,比如 R.styleable.MyView_text
Note 2: 在检索完属性值后,需要将 TypedArray
回收
从
Android 自定义View (一)
中看到另一种检索属性值的方式,参考一下:
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.e(TAG, "MyView: three");
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0);
try {
int count = a.getIndexCount();
for (int i = 0; i < count; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.MyView_text:
text = a.getString(attr);
break;
case R.styleable.MyView_textSize:
textSize = a.getDimension(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
case R.styleable.MyView_textColor:
textColor = a.getColor(attr, Color.BLACK);
break;
}
}
} finally {
a.recycle();
}
Log.e(TAG, "MyView: text = " + text);
Log.e(TAG, "MyView: textSize = " + textSize);
Log.e(TAG, "MyView: textColor = " + textColor);
}
获取的属性值如下:
使用
获取自定义的属性值后,就可以在绘图操作中使用
绘图操作最重要的部分就是重载 onDraw
方法
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
参数 canvas
是画布,另外还需要定义画笔 Paint
实现如下:
public class MyView extends View {
private static final String TAG = "MyView";
private String text;
private float textSize;
private int textColor;
private Paint paint;
private Rect textBound;
...
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.e(TAG, "MyView: three");
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0);
try {
int count = a.getIndexCount();
for (int i = 0; i < count; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.MyView_text:
text = a.getString(attr);
break;
case R.styleable.MyView_textSize:
textSize = a.getDimension(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
case R.styleable.MyView_textColor:
textColor = a.getColor(attr, Color.BLACK);
break;
}
}
} finally {
a.recycle();
}
Log.e(TAG, "MyView: text = " + text);
Log.e(TAG, "MyView: textSize = " + textSize);
Log.e(TAG, "MyView: textColor = " + textColor);
paint = new Paint();
textBound = new Rect();
}
...
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
paint.setColor(textColor);
paint.setTextSize(textSize);
paint.getTextBounds(text, 0, text.length(), textBound);
canvas.drawText(text, width / 2 - textBound.width() / 2, height / 2 + textBound.height() / 2, paint);
}
}
结果:
属性封装
在应用运行过程中,可能会需要改变自定义视图的属性,可以通过 setter getter
方法,封装自定义属性
MyView.java
中增加如下代码:
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
invalidate();
requestLayout();
}
public float getTextSize() {
return textSize;
}
public void setTextSize(float textSize) {
this.textSize = textSize;
invalidate();
requestLayout();
}
public int getTextColor() {
return textColor;
}
public void setTextColor(int textColor) {
this.textColor = textColor;
invalidate();
requestLayout();
}
Note:在 setter
方法中除了改变属性值外,还需要调用方法 invalidate
和 requestLayout
,以确保通知系统重绘该视图
完整代码
attrs.xml
:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="text" format="string" />
<attr name="textSize" format="dimension" />
<attr name="textColor" format="color" />
<declare-styleable name="MyView">
<attr name="text" />
<attr name="textSize" />
<attr name="textColor" />
</declare-styleable>
</resources>
MyView.java
:
public class MyView extends View {
private static final String TAG = "MyView";
private String text;
private float textSize;
private int textColor;
private Paint paint;
private Rect textBound;
public MyView(Context context) {
// super(context);
this(context, null);
Log.e(TAG, "MyView: one");
}
public MyView(Context context, @Nullable AttributeSet attrs) {
// super(context, attrs);
this(context, attrs, 0);
Log.e(TAG, "MyView: two");
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.e(TAG, "MyView: three");
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, 0);
try {
int count = a.getIndexCount();
for (int i = 0; i < count; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.MyView_text:
text = a.getString(attr);
break;
case R.styleable.MyView_textSize:
textSize = a.getDimension(attr, (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
case R.styleable.MyView_textColor:
textColor = a.getColor(attr, Color.BLACK);
break;
}
}
} finally {
a.recycle();
}
Log.e(TAG, "MyView: text = " + text);
Log.e(TAG, "MyView: textSize = " + textSize);
Log.e(TAG, "MyView: textColor = " + textColor);
paint = new Paint();
textBound = new Rect();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
Log.e(TAG, "onFinishInflate: ");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
paint.setColor(textColor);
paint.setTextSize(textSize);
paint.getTextBounds(text, 0, text.length(), textBound);
canvas.drawText(text, width / 2 - textBound.width() / 2, height / 2 + textBound.height() / 2, paint);
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
invalidate();
requestLayout();
}
public float getTextSize() {
return textSize;
}
public void setTextSize(float textSize) {
this.textSize = textSize;
invalidate();
requestLayout();
}
public int getTextColor() {
return textColor;
}
public void setTextColor(int textColor) {
this.textColor = textColor;
invalidate();
requestLayout();
}
}
activity_main.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:custom="http://schemas.android.com/apk/res/com.zj.viewtest"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.zj.viewtest.MainActivity">
<com.zj.viewtest.MyView
android:id="@+id/my_view"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@color/colorAccent"
custom:text="Hello World"
custom:textColor="@android:color/holo_blue_bright"
custom:textSize="16sp" />
<Button
android:id="@+id/btn_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="TEXT" />
<Button
android:id="@+id/btn_color"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="COLOR" />
</LinearLayout>
MainActivity.java
:
public class MainActivity extends AppCompatActivity {
private MyView myView;
private Button btnText;
private Button btnColor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myView = (MyView) findViewById(R.id.my_view);
btnText = (Button) findViewById(R.id.btn_text);
btnColor = (Button) findViewById(R.id.btn_color);
final String[] randomText = getResources().getStringArray(android.R.array.phoneTypes);
btnText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Random random = new Random();
myView.setText(randomText[random.nextInt(randomText.length)]);
}
});
final int[] randomColor = new int[]{Color.GREEN, Color.GRAY, Color.WHITE, Color.BLUE, Color.BLACK};
btnColor.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Random random = new Random();
myView.setTextColor(randomColor[random.nextInt(randomColor.length)]);
}
});
}
}