如果说要按类型来划分的话,自定义View的实现方式大概可以分为三种,自绘控件、组合控件、以及继承控件。那么下面我们就来依次学习一下,每种方式分别是如何自定义View的。
一、自绘控件
自绘控件的意思就是,这个View上所展现的内容全部都是我们自己绘制出来的。绘制的代码是写在onDraw()方法中的,而这部分内容我们已经在 Android视图绘制流程完全解析,带你一步步深入了解View(二) 中学习过了。
下面我们准备来自定义一个计数器View,这个View可以响应用户的点击事件,并自动记录一共点击了多少次。新建一个CounterView继承自View,代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
public
class
CounterView
extends
View
implements
OnClickListener {
private
Paint mPaint;
private
Rect mBounds;
private
int
mCount;
public
CounterView(Context context, AttributeSet attrs) {
super
(context, attrs);
mPaint =
new
Paint(Paint.ANTI_ALIAS_FLAG);
mBounds =
new
Rect();
setOnClickListener(
this
);
}
@Override
protected
void
onDraw(Canvas canvas) {
super
.onDraw(canvas);
mPaint.setColor(Color.BLUE);
canvas.drawRect(
0
,
0
, getWidth(), getHeight(), mPaint);
mPaint.setColor(Color.YELLOW);
mPaint.setTextSize(
30
);
String text = String.valueOf(mCount);
mPaint.getTextBounds(text,
0
, text.length(), mBounds);
float
textWidth = mBounds.width();
float
textHeight = mBounds.height();
canvas.drawText(text, getWidth() /
2
- textWidth /
2
, getHeight() /
2
+ textHeight /
2
, mPaint);
}
@Override
public
void
onClick(View v) {
mCount++;
invalidate();
}
}
|
既然CounterView是一个自绘视图,那么最主要的逻辑当然就是写在onDraw()方法里的了,下面我们就来仔细看一下。这里首先是将Paint画笔设置为蓝色,然后调用Canvas的drawRect()方法绘制了一个矩形,这个矩形也就可以当作是CounterView的背景图吧。接着将画笔设置为黄色,准备在背景上面绘制当前的计数,注意这里先是调用了getTextBounds()方法来获取到文字的宽度和高度,然后调用了drawText()方法去进行绘制就可以了。
这样,一个自定义的View就已经完成了,并且目前这个CounterView是具备自动计数功能的。那么剩下的问题就是如何让这个View在界面上显示出来了,其实这也非常简单,我们只需要像使用普通的控件一样来使用CounterView就可以了。比如在布局文件中加入如下代码:
1
2
3
4
5
|
<relativelayout android:layout_height=
"match_parent"
android:layout_width=
"match_parent"
xmlns:android=
"http://schemas.android.com/apk/res/android"
>
<com.example.customview.counterview android:layout_centerinparent=
"true"
android:layout_height=
"100dp"
android:layout_width=
"100dp"
>
</com.example.customview.counterview></relativelayout>
|
好了,就是这么简单,接下来我们可以运行一下程序,并不停地点击CounterView,效果如下图所示。
怎么样?是不是感觉自定义View也并不是什么高级的技术,简单几行代码就可以实现了。当然了,这个CounterView功能非常简陋,只有一个计数功能,因此只需几行代码就足够了,当你需要绘制比较复杂的View时,还是需要很多技巧的。
二、组合控件
组合控件的意思就是,我们并不需要自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,但我们可以将几个系统原生的控件组合到一起,这样创建出的控件就被称为组合控件。
举个例子来说,标题栏就是个很常见的组合控件,很多界面的头部都会放置一个标题栏,标题栏上会有个返回按钮和标题,点击按钮后就可以返回到上一个界面。那么下面我们就来尝试去实现这样一个标题栏控件。
新建一个title.xml布局文件,代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
<!--?xml version=
1.0
encoding=utf-
8
?-->
<relativelayout android:background=
"#ffcb05"
android:layout_height=
"50dp"
android:layout_width=
"match_parent"
xmlns:android=
"http://schemas.android.com/apk/res/android"
><button android:background=
"@drawable/back_button"
android:id=
"@+id/button_left"
android:layout_centervertical=
"true"
android:layout_height=
"40dp"
android:layout_marginleft=
"5dp"
android:layout_width=
"60dp"
android:text=
"Back"
android:textcolor=
"#fff"
>
<textview android:id=
"@+id/title_text"
android:layout_centerinparent=
"true"
android:layout_height=
"wrap_content"
android:layout_width=
"wrap_content"
android:text=
"This"
android:textcolor=
"#fff"
android:textsize=
"20sp"
is=
""
title="\">
</textview></button><喎�
"/kf/ware/vc/"
target=
"_blank"
class
=
"keylink"
>vcmVsYXRpdmVsYXlvdXQ+PC9wcmU+CjxwPtTa1eK49rK8vtbOxLz+1tCjrM7Sw8fK18/ItqjS5cHL0ru49lJlbGF0aXZlTGF5b3V01/fOqrGzvrCyvL7Wo6zIu7rz1NrV4rj2sry+1sDvtqjS5cHL0ru49kJ1dHRvbrrN0ru49lRleHRWaWV3o6xCdXR0b26+zcrHserM4sC41tC1xLe1u9iwtMWlo6xUZXh0Vmlld77Nysex6sziwLjW0LXEz9TKvrXEzsTX1qGjPC9wPgo8cD6908/CwLS0tL2o0ru49lRpdGxlVmlld7zMs9DX1EZyYW1lTGF5b3V0o6y0+sLryOfPwsv5yr6jujwvcD4KPHA+Jm5ic3A7PC9wPgo8cHJlIGNsYXNzPQ==
"brush:java;"
>
public
class
TitleView
extends
FrameLayout {
private
Button leftButton;
private
TextView titleText;
public
TitleView(Context context, AttributeSet attrs) {
super
(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title,
this
);
titleText = (TextView) findViewById(R.id.title_text);
leftButton = (Button) findViewById(R.id.button_left);
leftButton.setOnClickListener(
new
OnClickListener() {
@Override
public
void
onClick(View v) {
((Activity) getContext()).finish();
}
});
}
public
void
setTitleText(String text) {
titleText.setText(text);
}
public
void
setLeftButtonText(String text) {
leftButton.setText(text);
}
public
void
setLeftButtonListener(OnClickListener l) {
leftButton.setOnClickListener(l);
}
}</relativelayout>
|
接下来调用findViewById()方法获取到了返回按钮的实例,然后在它的onClick事件中调用finish()方法来关闭当前的Activity,也就相当于实现返回功能了。
另外,为了让TitleView有更强地扩展性,我们还提供了setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法,分别用于设置标题栏上的文字、返回按钮上的文字、以及返回按钮的点击事件。
到了这里,一个自定义的标题栏就完成了,那么下面又到了如何引用这个自定义View的部分,其实方法基本都是相同的,在布局文件中添加如下代码:
1
2
3
4
5
6
|
<relativelayout android:layout_height=
"match_parent"
android:layout_width=
"match_parent"
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:tools=
"http://schemas.android.com/tools"
>
<com.example.customview.titleview android:id=
"@+id/title_view"
android:layout_height=
"wrap_content"
android:layout_width=
"match_parent"
>
</com.example.customview.titleview>
</relativelayout>
|
这样就成功将一个标题栏控件引入到布局文件中了,运行一下程序,效果如下图所示:
现在点击一下Back按钮,就可以关闭当前的Activity了。如果你想要修改标题栏上显示的内容,或者返回按钮的默认事件,只需要在Activity中通过findViewById()方法得到TitleView的实例,然后调用setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法进行设置就OK了。
三、继承控件
继承控件的意思就是,我们并不需要自己重头去实现一个控件,只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。这种自定义控件的特点就是不仅能够按照我们的需求加入相应的功能,还可以保留原生控件的所有功能,比如 Android PowerImageView实现,可以播放动画的强大ImageView 这篇文章中介绍的PowerImageView就是一个典型的继承控件。
为了能够加深大家对这种自定义View方式的理解,下面我们再来编写一个新的继承控件。ListView相信每一个Android程序员都一定使用过,这次我们准备对ListView进行扩展,加入在ListView上滑动就可以显示出一个删除按钮,点击按钮就会删除相应数据的功能。
首先需要准备一个删除按钮的布局,新建delete_button.xml文件,代码如下所示:
1
|
<!--?xml version=
1.0
encoding=utf-
8
?--><button android:background=
"@drawable/delete_button"
android:id=
"@+id/delete_button"
android:layout_height=
"wrap_content"
android:layout_width=
"wrap_content"
xmlns:android=
"http://schemas.android.com/apk/res/android"
></button>
|
接着创建MyListView继承自ListView,这就是我们自定义的View了,代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
public
class
MyListView
extends
ListView
implements
OnTouchListener,
OnGestureListener {
private
GestureDetector gestureDetector;
private
OnDeleteListener listener;
private
View deleteButton;
private
ViewGroup itemLayout;
private
int
selectedItem;
private
boolean
isDeleteShown;
public
MyListView(Context context, AttributeSet attrs) {
super
(context, attrs);
gestureDetector =
new
GestureDetector(getContext(),
this
);
setOnTouchListener(
this
);
}
public
void
setOnDeleteListener(OnDeleteListener l) {
listener = l;
}
@Override
public
boolean
onTouch(View v, MotionEvent event) {
if
(isDeleteShown) {
itemLayout.removeView(deleteButton);
deleteButton =
null
;
isDeleteShown =
false
;
return
false
;
}
else
{
return
gestureDetector.onTouchEvent(event);
}
}
@Override
public
boolean
onDown(MotionEvent e) {
if
(!isDeleteShown) {
selectedItem = pointToPosition((
int
) e.getX(), (
int
) e.getY());
}
return
false
;
}
@Override
public
boolean
onFling(MotionEvent e1, MotionEvent e2,
float
velocityX,
float
velocityY) {
if
(!isDeleteShown && Math.abs(velocityX) > Math.abs(velocityY)) {
deleteButton = LayoutInflater.from(getContext()).inflate(
R.layout.delete_button,
null
);
deleteButton.setOnClickListener(
new
OnClickListener() {
@Override
public
void
onClick(View v) {
itemLayout.removeView(deleteButton);
deleteButton =
null
;
isDeleteShown =
false
;
listener.onDelete(selectedItem);
}
});
itemLayout = (ViewGroup) getChildAt(selectedItem
- getFirstVisiblePosition());
RelativeLayout.LayoutParams params =
new
RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
params.addRule(RelativeLayout.CENTER_VERTICAL);
itemLayout.addView(deleteButton, params);
isDeleteShown =
true
;
}
return
false
;
}
@Override
public
boolean
onSingleTapUp(MotionEvent e) {
return
false
;
}
@Override
public
void
onShowPress(MotionEvent e) {
}
@Override
public
boolean
onScroll(MotionEvent e1, MotionEvent e2,
float
distanceX,
float
distanceY) {
return
false
;
}
@Override
public
void
onLongPress(MotionEvent e) {
}
public
interface
OnDeleteListener {
void
onDelete(
int
index);
}
}
|
当手指按下时,会调用OnGestureListener的onDown()方法,在这里通过pointToPosition()方法来判断出当前选中的是ListView的哪一行。当手指快速滑动时,会调用onFling()方法,在这里会去加载delete_button.xml这个布局,然后将删除按钮添加到当前选中的那一行item上。注意,我们还给删除按钮添加了一个点击事件,当点击了删除按钮时就会回调onDeleteListener的onDelete()方法,在回调方法中应该去处理具体的删除操作。
好了,自定义View的功能到此就完成了,接下来我们需要看一下如何才能使用这个自定义View。首先需要创建一个ListView子项的布局文件,新建my_list_view_item.xml,代码如下所示:
1
2
3
4
5
6
|
<!--?xml version=
1.0
encoding=utf-
8
?-->
<relativelayout android:descendantfocusability=
"blocksDescendants"
android:layout_height=
"match_parent"
android:layout_width=
"match_parent"
android:orientation=
"vertical"
xmlns:android=
"http://schemas.android.com/apk/res/android"
>
<textview android:gravity=
"left|center_vertical"
android:id=
"@+id/text_view"
android:layout_centervertical=
"true"
android:layout_height=
"50dp"
android:layout_width=
"wrap_content"
android:textcolor=
"#000"
>
</textview></relativelayout>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public
class
MyAdapter
extends
ArrayAdapter<string> {
public
MyAdapter(Context context,
int
textViewResourceId, List<string> objects) {
super
(context, textViewResourceId, objects);
}
@Override
public
View getView(
int
position, View convertView, ViewGroup parent) {
View view;
if
(convertView ==
null
) {
view = LayoutInflater.from(getContext()).inflate(R.layout.my_list_view_item,
null
);
}
else
{
view = convertView;
}
TextView textView = (TextView) view.findViewById(R.id.text_view);
textView.setText(getItem(position));
return
view;
}
}</string></string>
|
1
2
3
4
5
6
|
<relativelayout android:layout_height=
"match_parent"
android:layout_width=
"match_parent"
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:tools=
"http://schemas.android.com/tools"
>
<com.example.customview.mylistview android:id=
"@+id/my_list_view"
android:layout_height=
"wrap_content"
android:layout_width=
"match_parent"
>
</com.example.customview.mylistview>
</relativelayout>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
public
class
MainActivity
extends
Activity {
private
MyListView myListView;
private
MyAdapter adapter;
private
List<string> contentList =
new
ArrayList<string>();
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
initList();
myListView = (MyListView) findViewById(R.id.my_list_view);
myListView.setOnDeleteListener(
new
OnDeleteListener() {
@Override
public
void
onDelete(
int
index) {
contentList.remove(index);
adapter.notifyDataSetChanged();
}
});
adapter =
new
MyAdapter(
this
,
0
, contentList);
myListView.setAdapter(adapter);
}
private
void
initList() {
contentList.add(Content Item
1
);
contentList.add(Content Item
2
);
contentList.add(Content Item
3
);
contentList.add(Content Item
4
);
contentList.add(Content Item
5
);
contentList.add(Content Item
6
);
contentList.add(Content Item
7
);
contentList.add(Content Item
8
);
contentList.add(Content Item
9
);
contentList.add(Content Item
10
);
contentList.add(Content Item
11
);
contentList.add(Content Item
12
);
contentList.add(Content Item
13
);
contentList.add(Content Item
14
);
contentList.add(Content Item
15
);
contentList.add(Content Item
16
);
contentList.add(Content Item
17
);
contentList.add(Content Item
18
);
contentList.add(Content Item
19
);
contentList.add(Content Item
20
);
}
}</string></string>
|
