自定义View
MoMo Lv5
  1. 自定义View的流程 ⭐⭐⭐⭐⭐
  2. 自定义View需要重写哪些函数?说说你在自定义View时常常重写的一些方法? ⭐⭐⭐⭐
  3. 自定义View的种类有哪些?给我说说你之前项目中的案例。⭐⭐⭐⭐
  4. 说说自定义View中如何自定义属性?⭐⭐⭐
  5. 自定义View如何处理padding?⭐⭐
  6. 自定义View效率高于xml布局文件吗?⭐⭐
  7. 自定义View什么时候需要处理wrap_content属性?怎么处理?⭐

什么是自定义View

View的绘制有测量 -〉布局 -〉绘制,这三大步骤

自定义View和自定义ViewGroup

  • 自定义View:如果官方提供现成的View控件无法达到符合自己预期的View的样式,那就需要自己来实现,一般需要重写onDraw()方法来设置绘制的样式,这就是自定义View;
  • 自定义ViewGroup:如果希望将一个或多个现有的View按照特定的布局方式,组装形成一个新的组件,这就是自定义ViewGroup。

自定义View基础知识

坐标系

在安卓系统中,屏幕左上角为原点,往右边是X轴正向,往下边是Y轴正向。常见API函数如下:

  • 子View到父View的距离
1
2
3
4
5
6
7
8
9
getHeight()	//获取View自身高度
getWidth() //获取View自身宽度
getTop() //获取子 View 左上角到父 View 顶部的距离
getLeft() //获取子 View 左上角到父 View 左边的距离
getBottom() //获取子 View 右下角到父 View 顶部的距离
getRight() //获取子 View 右上角到父 View 左边的距离

getBottom() - getTop() = 子View 的高
getRight() - getLeft() = 子View 的宽
  • 触摸点到所在View或者屏幕坐标系
1
2
3
4
5
event.getX()	//触摸点相对于其所在 View 坐标系的坐标
event.getY()

event.getRawX() //触摸点相对于屏幕坐标系的坐标
event.getRawY()

详细可参考下图(抄录于参考文档1),其中绿色方块为子View,子View里面的蓝色小圆圈代表触摸点,子View外边依次是父View和屏幕坐标。

image

颜色

Android系统支持的颜色模式有以下三种:

颜色模式 备注
ARGB8888 四通道高精度(32位)
ARGB4444 四通道低精度(16位)
RGB565 屏幕默认模式(16位)

其中A代表透明度,RGB分别代表红绿蓝三种原色,后面的数值代表该模式用多少位二进制数来表示,比如:

1
2
3
4
#0xF00	//低精度 - 不带透明通道红色
#0xAF00 //低精度 - 带透明通道红色
#0xFF0000 //高精度 - 不带透明通道红色
#0xAAFF0000 //高精度 - 带透明通道红色

触摸事件

既然是View,那就离不开触摸事件,常见的触摸事件如下:

事件 简介
ACTION_DOWN 手指初次接触屏幕时触发
ACTION_MOVE 手指在屏幕上滑动时触发,会多次触发
ACTION_UP 手指离开屏幕时触发
ACTION_CANCEL 事件被上层拦截时触发

margin和padding

在开发中,经常可以看到这两个,在此再次介绍下:

  • margin:子控件与父控件的距离,也就是“外边距”;
  • padding:控件里内容和控件的边界之间的距离,也就是“内边距”。在使用系统自带的控件时,只要在xml布局文件设置好padding即刻生效,但在自定义View则不会生效,需要手动在onDraw()方法里处理。

自定义View效率高于xml布局文件吗?

自定义View效率高于xml定义,原因如下:

  1. 自定义View少了解析xml;
  2. 自定义View 减少了ViewGroup与View之间的测量,包括父量子,子量自身,子在父中位置摆放,当子view变化时,父的某些属性都会跟着变化。

自定义View的流程

自定义View有一个通用的流程:

image

onMeasure()

在Measure阶段,需根据需要重写onMeasure()方法,即使在xml布局文件里面设置了View的宽高。因为一个子View的宽高不止受自身参数决定,还需要受到父控件的影响。

onLayout()

确定布局可以用onLayout()方法,在自定义View中,一般不需要重写该方法。但在自定义ViewGroup中可能需要重写,一般做法是循环取出子View,并计算每个子View位置等坐标值,然后使用child.layout()方法设置子View的位置,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int left = 0;
View child;
//循环遍历各个子View
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
int width = child.getMeasuredWidth();
childWidth = width;
//设置子View位置
child.layout(left, 0, left + width, child.getMeasuredHeight());
left += width;
}
}
}

onDraw()

Canvas(画布)

这是实际绘制的部分,使用Canvas(画布)进行绘制常见的Canvas API函数如下:

操作类型 相关 API 备注

绘制颜色 drawColor、drawRGB、drawARGB 使用单一颜色填充整个画布
绘制基本图形 drawPoint、drawPoints、drawLine、drawLines、drawRect、drawRoundRect、drawOval、drawCircle、drawArc 绘制点、线、矩形、圆角矩形、椭圆、圆、圆弧
绘制图片 drawBitmap、drawPicture 绘制位图和图片
绘制路径 drawPath 绘制路径,绘制贝塞尔曲线
画布裁剪 clipPath、clipRect 设置画布的显示区域
画布变换 translate、scale、rotate、skew 位移、缩放、旋转、错切

Paint(画笔)

Paint(画笔)在自定义View的实现也是非常常见的,所以需要了解常见的API函数,详情可见:Android开发手册-Paint

以下是Paint常用API函数:Android Paint API总结和使用方法

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
void reset();
void set(Paint src);
void setCompatibilityScaling( float factor);
void setBidiFlags( int flags);
void setFlags( int flags);
void setHinting( int mode);
//是否抗锯齿
void setAntiAlias( boolean aa);
//设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰
void setDither( boolean dither);
//设置线性文本
void setLinearText( boolean linearText);
//设置该项为true,将有助于文本在LCD屏幕上的显示效果
void setSubpixelText( boolean subpixelText);
//设置下划线
void setUnderlineText( boolean underlineText);
//设置带有删除线的效果
void setStrikeThruText( boolean strikeThruText);
//设置伪粗体文本,设置在小字体上效果会非常差
void setFakeBoldText( boolean fakeBoldText);
//如果该项设置为true,则图像在动画进行中会滤掉对Bitmap图像的优化操作
//加快显示速度,本设置项依赖于dither和xfermode的设置
void setFilterBitmap( boolean filter);
//设置画笔风格,空心或者实心 FILL,FILL_OR_STROKE,或STROKE
//Paint.Style.STROKE 表示当前只绘制图形的轮廓,而Paint.Style.FILL表示填充图形。
void setStyle(Style style);
//设置颜色值
void setColor( int color);
//设置透明图0~255,要在setColor后面设置才生效
void setAlpha( int a);
//设置RGB及透明度
void setARGB( int a, int r, int g, int b);
//当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度
void setStrokeWidth( float width);
void setStrokeMiter( float miter);
//当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷末端的图形样式
//如圆形样式Cap.ROUND,或方形样式Cap.SQUARE
void setStrokeCap(Cap cap);
//设置绘制时各图形的结合方式,如平滑效果等
void setStrokeJoin(Join join);
//设置图像效果,使用Shader可以绘制出各种渐变效果
Shader setShader(Shader shader);
//设置颜色过滤器,可以在绘制颜色时实现不用颜色的变换效果
ColorFilter setColorFilter(ColorFilter filter);
//设置图形重叠时的处理方式,如合并,取交集或并集,经常用来制作橡皮的擦除效果
Xfermode setXfermode(Xfermode xfermode);
//设置绘制路径的效果,如点画线等
PathEffect setPathEffect(PathEffect effect);
//设置MaskFilter,可以用不同的MaskFilter实现滤镜的效果,如滤化,立体等
MaskFilter setMaskFilter(MaskFilter maskfilter);
//设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等
Typeface setTypeface(Typeface typeface);
//设置光栅化
Rasterizer setRasterizer(Rasterizer rasterizer);
//在图形下面设置阴影层,产生阴影效果,radius为阴影的角度,dx和dy为阴影在x轴和y轴上的距离,color为阴影的颜色
//注意:在Android4.0以上默认开启硬件加速,有些图形的阴影无法显示。关闭View的硬件加速 view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
void setShadowLayer( float radius, float dx, float dy, int color);
//设置文本对齐
void setTextAlign(Align align);
//设置字体大小
void setTextSize( float textSize);
//设置文本缩放倍数,1.0f为原始
void setTextScaleX( float scaleX);
//设置斜体文字,skewX为倾斜弧度
void setTextSkewX( float skewX);

Path(路径)

Path类封装了由直线段,二次曲线和三次曲线组成的复合(多轮廓)几何路径。 它可以用canvas.drawPath(path,paint)绘制,可以是填充或描边(基于绘制的样式),也可以用于剪裁或在路径上绘制文本。 详情见:Android开发手册-Path

常见的自定义View类型

如1.1小节说的,自定义View主要可以分为自定义View和自定义ViewGroup两种,常见的自定义View分为以下4种类型:

  • 继承系统提供的现有控件的自定义View;
  • 继承View类的自定义View;
  • 将多个单一的View合成复杂的自定义组合View;
  • 继承ViewGroup类的自定义View(引导);

接下来根据每种类型依次介绍。

继承系统提供的现有控件的自定义View

继承系统控件,一般是为了在系统控件上增加新的特性,可以在onDraw()方法里进行处理即可。系统控件TextView可以正常设置背景颜色和文本内容,但如果需要增加背景线条等特殊操作,正常的TextView的API函数是无法做到的。这时候就可以通过自定义View来实现了:

image

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
public class MyTextView extends TextView { //继承TextView
private Paint mPaint = new Paint(Paint.DITHER_FLAG); //绘制时启用抗锯齿功能的绘制标志

public MyTextView(Context context) {
super(context);
initDraw();
}

public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initDraw();
}

public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initDraw();
}

private void initDraw() {
mPaint.setColor(Color.BLUE); //设置画笔颜色
mPaint.setStrokeWidth((float) 1.5); //设置画笔宽度,也就是字体的宽度
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
canvas.drawLine(0, 0, width, height, mPaint); //绘制线条
canvas.drawLine(0, height, width, 0, mPaint); //绘制线条
}
}

通过以上的代码,就自定义了一个继承TextView的自定义View,这时候只要在xml文件里直接引用该控件即可:

1
2
3
4
5
6
7
8
9
<com.example.android.MyTextView 
android:id="@+id/iv_text"
android:layout_width="250dp"
android:layout_height="150dp"
android:textSize="15sp"
android:background="@android:color/blue"
android:layout_centerHorizontal="true"
android:text="自定义TextView"
/>

继承View类的自定义View

注意事项

上面继承系统控件相对简单,如果是继承View类的自定义View,就不仅要重写onDraw()方法,还要考虑以下几点:

  • padding属性处理;
  • wrap_content属性处理;
  • 提供自定义属性,方便自定义View的属性配置;
  • 如果涉及触摸操作,还需要重写onTouchEvent()方法来处理触摸事件;

既然是继承View类来创造一个新的View控件,我们画一个贝塞尔曲线,就是仿手机边缘滑动时的曲线图:

image

直接上代码(为方便展示,以下是最终版本的代码):

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
public class BezierView extends View {
private Path bezierPath; //贝塞尔曲线路径
private Paint paint; //画笔
private int mColor=Color.BLACK;

public BezierView(Context context) {
super(context);
initDraw();
}

public BezierView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray mTypedArray=context.obtainStyledAttributes(attrs,R.styleable.BezierView);
//提取BezierView属性集合的bezier_color属性,如果没设置默认值为Color.BLACK
mColor=mTypedArray.getColor(R.styleable.bezier_color,Color.BLACK);
//获取资源后要及时回收
mTypedArray.recycle();
initDraw();
}

public BezierView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initDraw();
}

// 初始化路径和画笔
private void initDraw() {
bezierPath = new Path();
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.RED);
paint.setStrokeWidth((float) 1.5);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //1
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int defaultValue = 700;
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);

//AT_MOST对应的是wrap_content的宽高
if(widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(defaultValue,defaultValue);
}else if(widthSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(defaultValue,heightSpecSize);
}else if(heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,defaultValue);
}
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 半弧的宽度是可以根据手指位置而变化的,这里简化写死为200
float currentWidth = 200;
float height = getHeight();
int maxWidth = getWidth();
float centerY = height / 2;
float progress = currentWidth / maxWidth;
if (progress == 0) {
return;
}

//开始画半弧图形
/*
ps: 形状如下,小点为起始点和结束点,星号为控制点
·
|
*
*
|
·
|
*
*
|
·
*/
//设置画笔颜色,现在设置为黑色
paint.setColor(mColor);
//半弧颜色的深度是可以根据手指位置而变化的,这里简化写死
paint.setAlpha((int) (500 * progress));

float bezierWidth = currentWidth / 2;
float coordinateX = 0; //2

//正式绘制贝塞尔曲线,使用cubicTo()方法
bezierPath.reset();
bezierPath.moveTo(coordinateX, 0);
bezierPath.cubicTo(coordinateX, height / 4f, bezierWidth, height * 3f / 8, bezierWidth, centerY);
bezierPath.cubicTo(bezierWidth, height * 5f / 8, coordinateX, height * 3f / 4, coordinateX, height);
canvas.drawPath(bezierPath, paint);
}
}

上述代码都做了详细的注释,先是初始化画笔,接着在onDraw()方法按照我们自己的思路,绘制贝塞尔曲线,接着只要在xml布局文件引用该自定义View即可(这几行代码下文多次讲到,记得回来这里看):

1
2
3
4
5
6
7
8
9
10
<com.example.android.BezierView 
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/rv_bezier"
android:layout_width="match_parent" <!-- 3 -->
android:layout_height="200dp"
android:layout_below="@id/iv_text"
android:layout_centerHorizontal="true"
android:layout_marginTop="50dp"
android:padding="10dp" <!-- 4 -->
app:bezier_color="@android:color/black" /> <!-- 5 -->

处理padding

上面[注释4]指定的padding(内边距),会发现无论修改到任何数值,绘制出来的View都不受影响,这是因为我们需要在onDraw()方法里做特殊处理才能显示出效果。至于系统控件设置padding数值可以生效,正是系统帮我们处理好了。

因此,在BezierView的onDraw()中的[注释2]需要做如下修改:

1
2
3
4
5
6
//原来的代码:
float coordinateX = 0;

//修改为:
int paddingLeft = getPaddingLeft();
float coordinateX = paddingLeft;

其中coordinateX是贝塞尔曲线横坐标的开始点,我们需要手动获取xml布局文件设置的偏差值,手动的修改coordinateX,如此就可以使padding值生效。效果图如下,可以发现图像确实往中间偏移了一些:

image

wrap_content属性处理

在xml布局文件修改android:layout_width属性为match_parent或者wrap_content,最终发现效果都是一样的。导致这个原因,在参考文档3的2.3小节有介绍,我们需要在onMeasure()方法里给wrap_content属性设置默认宽高值,代码已经在上面BezierView类写清楚了,即[注释1]。接着,看看android:layout_width属性设置为match_parent或者wrap_content的区别(为了更直观看出差别,直接给BezierView整个控件设置了背景颜色):

  • android:layout_width=match_parent或者android:layout_width=wrap_content但没有重写onMeasure()

image

  • android:layout_width=wrap_content并且重写onMeasure()

image

添加自定义属性

我们看5.1小节最后的代码,以android:开头的都是系统自带的属性,而[注释5]:app:bezier_color,是自定义的属性。只要在values目录下创建attrs.xml文件:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BezierView">
<attr name="bezier_color" format="color" />
</declare-styleable>
</resources>

在这个文件我们设置了一个名为BezierView的自定义属性组合,里面可以有多个属性,目前只需要一个颜色格式的属性:bezier_color。可以根据需要添加多个属性。创建好后,我们看看如何使用,有两个地方需要修改:

  • Step1:xml布局文件修改

自定义属性需要添加:

1
2
xmlns:app="http://schemas.android.com/apk/res-auto" <!-- 6 -->
app:bezier_color="@android:color/black" /> <!-- 7 -->

使用自定义属性,都要添加[注释6]。其中app是自定义的名字,可以改为其他的。最后在[注释7]配置为黑色。

  • Step2:代码修改 在上面BezierView的代码里有如下构造函数:
1
2
3
4
5
6
7
8
9
10
public BezierView(Context context, AttributeSet attrs) {
super(context, attrs);
//获取自定义属性组合
TypedArray mTypedArray=context.obtainStyledAttributes(attrs,R.styleable.BezierView);
//提取BezierView属性集合的bezier_color属性,如果没设置默认值为Color.BLACK
mColor=mTypedArray.getColor(R.styleable.bezier_color,Color.BLACK);
//获取资源后要及时回收
mTypedArray.recycle();
initDraw();
}

将多个单一的View合成复杂的自定义组合View

Android系统自带的AlertDialog比较丑,因此想要自定义一个MyDialogView,如下图:

image

可以简单的看出,这个自定义组合View包含至少2个TextView、2个Button、1个ImageView。还是先上最终完整代码,再进行解析:

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
public class MyDialogView extends Dialog {

public MyDialogView(Context context) {
super(context);
}

public MyDialogView(Context context, int theme) {
super(context, theme);
}

public static int px2dip(Context context, float pxValue) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}

@Override
protected void onStart() {
super.onStart();
//8:设置窗口背景为透明
getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
getWindow().setLayout(px2dip(getContext(), 2010), LinearLayout.LayoutParams.WRAP_CONTENT);
}

public static class Builder {
private final Context context;
private String title;
private String message;
private String positiveButtonText;
private String negativeButtonText;
private DialogInterface.OnClickListener positiveButtonClickListener;
private DialogInterface.OnClickListener negativeButtonClickListener;
private Drawable mIcon;

public Builder(Context context) {
this.context = context;
}

//设置图标
public void setIcon(Drawable icon) {
mIcon = icon;
}

//设置消息内容
public Builder setMessage(String message) {
this.message = message;
return this;
}

//设置消息内容
public Builder setMessage(int message) {
this.message = (String) context.getText(message);
return this;
}

//设置标题内容
public Builder setTitle(int title) {
this.title = (String) context.getText(title);
return this;
}

//设置标题内容
public Builder setTitle(String title) {
this.title = title;
return this;
}

//设置确认按钮回调
public Builder setPositiveButton(int positiveButtonText,
DialogInterface.OnClickListener listener) {
this.positiveButtonText = (String) context
.getText(positiveButtonText);
this.positiveButtonClickListener = listener;
return this;
}

//设置确认按钮回调
public Builder setPositiveButton(String positiveButtonText,
DialogInterface.OnClickListener listener) {
this.positiveButtonText = positiveButtonText;
this.positiveButtonClickListener = listener;
return this;
}

//设置取消按钮回调
public Builder setNegativeButton(int negativeButtonText,
DialogInterface.OnClickListener listener) {
this.negativeButtonText = (String) context
.getText(negativeButtonText);
this.negativeButtonClickListener = listener;
return this;
}

//设置取消按钮回调
public Builder setNegativeButton(String negativeButtonText,
DialogInterface.OnClickListener listener) {
this.negativeButtonText = negativeButtonText;
this.negativeButtonClickListener = listener;
return this;
}

public MyDialogView create() {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// 9:Step 1: 初始化
final MyDialogView dialog = new MyDialogView(context);
View layout = inflater.inflate(R.layout.my_alert_dialog, null);
dialog.addContentView(layout, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));

// Step 2:设置图标,如果不设置则隐藏图标ImageView控件
if (mIcon != null) {
layout.findViewById(R.id.my_alert_dialog_icon).setVisibility(View.VISIBLE);
((ImageView) layout.findViewById(R.id.my_alert_dialog_icon)).setImageDrawable(mIcon);
} else {
layout.findViewById(R.id.my_alert_dialog_icon).setVisibility(View.GONE);
}

// Step 3:设置标题,如果不设置则隐藏标题TextView控件
if (title != null) {
((TextView) layout.findViewById(R.id.my_alert_dialog_title)).setText(title);
((TextView) layout.findViewById(R.id.my_alert_dialog_title)).setVisibility(View.VISIBLE);
} else {
layout.findViewById(R.id.my_alert_dialog_title).setVisibility(View.GONE);
}

// Step 4:设置确认按钮,如果不设置则隐藏确认控件
if (positiveButtonText != null) {
((TextView) layout.findViewById(R.id.my_alert_dialog_button_positive))
.setVisibility(View.VISIBLE);
((TextView) layout.findViewById(R.id.my_alert_dialog_button_positive))
.setText(positiveButtonText);
if (positiveButtonClickListener != null) {
layout.findViewById(R.id.my_alert_dialog_button_positive)
.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
positiveButtonClickListener.onClick(dialog,
DialogInterface.BUTTON_POSITIVE);
}
});
}
} else {
layout.findViewById(R.id.my_alert_dialog_button_negative).setVisibility(
View.GONE);
}

// Step 5:设置取消按钮,如果不设置则隐藏取消控件
if (negativeButtonText != null) {
((TextView) layout.findViewById(R.id.my_alert_dialog_button_negative))
.setVisibility(View.VISIBLE);
((TextView) layout.findViewById(R.id.my_alert_dialog_button_negative))
.setText(negativeButtonText);
if (negativeButtonClickListener != null) {
layout.findViewById(R.id.my_alert_dialog_button_negative)
.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
negativeButtonClickListener.onClick(dialog,
DialogInterface.BUTTON_NEGATIVE);
}
});
}
} else {
layout.findViewById(R.id.my_alert_dialog_button_negative).setVisibility(
View.GONE);
}

// Step 6:设置消息内容,如果不设置则隐藏消息TextView控件
if (message != null) {
((TextView) layout.findViewById(R.id.my_alert_dialog_message)).setVisibility(View.VISIBLE);
((TextView) layout.findViewById(R.id.my_alert_dialog_message)).setText(message);
} else {
((TextView) layout.findViewById(R.id.my_alert_dialog_message)).setVisibility(View.GONE);
}

dialog.setContentView(layout);
return dialog;
}
}
}

在MainActivity里面可以这么使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//使用建造者设计模式进行建造
MyDialogView.Builder builder = new MyDialogView.Builder(MainActivity.this);
builder.setIcon(getDrawable(R.drawable.mydialog));
builder.setMessage("这是消息内容");
builder.setTitle("这是一个标题");
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builder.setNegativeButton("取消",
new android.content.DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builder.create().show();
}

上面的代码基本每个函数都做了注释。自定义组合View的关键是先写好xml布局文件,然后在代码里去动态的操作xml布局文件里的各种控件。先附上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
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
<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/install_RelativeLayout"
android:layout_width="760dp"
android:layout_height="wrap_content"
android:background="@drawable/my_alertdialog_blackground">

<ImageView
android:id="@+id/my_alert_dialog_icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_centerHorizontal="true"
android:layout_marginTop="32dp"
android:scaleType="fitCenter" />

<TextView
android:id="@+id/my_alert_dialog_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/my_alert_dialog_icon"
android:layout_gravity="center"
android:lineSpacingMultiplier="1.2"
android:layout_marginTop="8dp"
android:gravity="center"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:textColor="#CC000000"
android:textSize="20sp" />

<TextView
android:id="@+id/my_alert_dialog_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/my_alert_dialog_title"
android:lineSpacingMultiplier="1.2"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginTop="8dp"
android:gravity="center"
android:layout_marginEnd="32dp"
android:textColor="#60000000"
android:textSize="16sp" />

<View
android:id="@+id/divider1"
android:layout_marginTop="32dp"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/my_alert_dialog_message"
android:background="#20000000" />

<LinearLayout
android:id="@+id/test_info_bottom_button"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_below="@id/divider1"

android:orientation="horizontal">

<Button
android:id="@+id/my_alert_dialog_button_negative"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:textColor="#FF000000"
android:textSize="16sp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:background="?android:attr/selectableItemBackground"/>

<View
android:id="@+id/divider2"
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="#20000000" />

<Button
android:id="@+id/my_alert_dialog_button_positive"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:textColor="#FF000000"
android:textSize="16sp" />
</LinearLayout>
</RelativeLayout>

上面的xml布局文件就确定了自定义组合View的样式和里面包含什么View控件,接着在MyDialogView类里的create()方法的Step 1,即[注释9],通过 View layout = inflater.inflate(R.layout.my_alert_dialog, null);动态的加载布局文件,并在代码里逐一的操作各个控件,对应代码里Step2-Step6。

代码虽然比较长,但是逻辑都很简单,花几分钟相信大家就可以看懂,不过其中[注释8]

1
2
3
4
5
6
7
@Override
protected void onStart() {
super.onStart();
//8:设置窗口背景为透明
getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
getWindow().setLayout(px2dip(getContext(), 2010), LinearLayout.LayoutParams.WRAP_CONTENT);
}

这个MyDialogView继承了Dialog类,而Dialog类有自己的 Window,我们自定义的MyDialogView的四个角是圆形角,因此需要把Window设置为透明背景,否则会变成下图这样:

image

继承ViewGroup类的自定义View

自定义ViewGroup

自定义View优化

为了使自定义View运行更加流畅,有以下几点需要注意:

  • onDraw()尽量不分配内存:onDraw()被调用频率很高,如果在此进行内存分配可能会导致GC,从而导致卡顿;
  • 使用含有参数的invalidate():不带参数的invalidate()会强制重绘整个View,所以如果可能的话,尽量调用含有4个参数的invalidate()方法;
  • 减少requestLayout()调用:requestLayout()会使系统遍历整个View树来计算每个View的大小,是费时操作;

参考文档

  1. Android View体系(一)视图坐标系
  2. [自定义 View](https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/自定义 View.md)
  3. View绘制流程全解析
  4. Android Paint API总结和使用方法
Powered by Hexo & Theme Keep
Unique Visitor Page View