Android自定义View和Drawable总结

目录

  1. Android自定义View简述
    1. 自定义属性
    2. onMeasure()
    3. onLayout()
    4. onDraw()
    5. 一些Tips:
  2. Drawable总结
    1. BitmapDrawable
    2. NinePatchDrawable
    3. LayerDrawable
    4. StateListDrawable
    5. TransitionDrawable
    6. InsertDrawable
    7. ClipDrawable
  3. 参考

Android自定义View简述

Android自定义View方式有许多,本文对直接继承View的方式进行总结,建议深入阅读源码了解View是如何进行绘制的,可以从 ViewRootImpl#performTraversals() 方法开始,并且建议同时理解View的事件传递机制和动画机制,这样才能实现非常炫丽的View;我们主要讨论onMeasure()、onLayout()和onDraw()进行override的注意事项,不会讨论增加交互和动画等事宜。

自定义属性

1. 建立res/values/attrs.xml,demo如下:

1
2
3
4
5
6
7
8
9
<resources>
<declare-styleable name="PieChart">
<attr name="showText" format="boolean" />
<attr name="labelPosition" format="enum">
<enum name="left" value="0"/>
<enum name="right" value="1"/>
</attr>
</declare-styleable>
</resources>

2. 布局文件中使用自定义属性时,Gradle建议命名空间统一使用"http://schemas.android.com/apk/res-auto"
3. 官方建议通过TypedArray在程序中读取定义的属性,demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public PieChart(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.PieChart,
0, 0);

try {
mShowText = a.getBoolean(R.styleable.PieChart_showText, false);
mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0);
} finally {
a.recycle();
}
}

onMeasure()

系统提供的LinearLayout等ViewGroup组件会继承并重写 View#onMeasure() 方法,主要作用是遍历每个子View并调用子View的 View#onMeasure() 方法。总之,该回调函数的作用就是得到View测量过的width和height,所以override时一定要调用 View#setMeasuredDimension(int measuredWidth, int measuredHeight) 方法设置宽高,View#getMeasuredWidth()View#getMeasuredHeight() 就是取得上述函数设置的宽度和高度。View#onMeasure() 函数原型以及默认实现如下:

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

其参数含义参见View.MeasureSpec类,其高2位表示SpecMode,低30位表示SpecSize,前者表示测量模式,后者表示其规格大小。其中SpecMode定义三种取值:

Mode Description
AT_MOST 父容器指定可用的最大值,具体View对其实现不同,通常对应LayoutParams中的wrap_content
EXACTLY 父容器指定该View测量的精确值,就是SpecSize,通常对应LayoutParams中的match_parent和具体数值
UNSPECIFIED 父容器没有限制,通常系统内部使用

整个measure过程从顶层View(DecorView)开始,其长度和高度的MeasureSpec生成参考 ViewRootImp#getRootMeasureSpec()

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
/**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize
* The available width or height of the window
*
* @param rootDimension
* The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {

case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}

然后进行DecorView的measure,DecorView本质是FrameLayout,所以需要关注 ViewGroup#onMeasure()。不同的Layout对onMeasure()实现不同,但都会遍历其子View调用measure()方法,最终执行至 View#onMeasure();以FrameLayout为例,看 View#onMeasure() 的参数是如何生成的。其关键代码:

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
class FrameLayout{
...

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}

final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
...
}
}

class ViewGroup {
...
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
...
}

通过上述代码得出结论 View#onMeasure() 参数与 父容器的MeasureSpec自身的LayoutParams 都有关系。最后看一下 View#onMeasure() 的默认实现,以width为例:

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
class View{
...
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
...
}

Drawable:

public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

其中 mMinWidth 对应android:miandroid:minWidth,有些Drawable(比如BitmapDrawable)是有原始长度的,有些(比如ShapeDrawable)没有。View#onMeasure() 默认实现并没有区分LayoutParams中match_parent和wrap_content,自定义View时建议实现。为完整实现View的测量过程,务必 View#onMeasure() 中一定要执行 View#setMeasuredDimension(int measuredWidth, int measuredHeight) 方法。ViewGroup#onMeasure() 实现时需要先对子View遍历进行测量,最后设置ViewGroup的measuredWidth和measuredHeight,建议阅读FrameLayout源码进行理解。

onLayout()

View完成测量后,根据 View#getMeasuredWidth()View#getMeasuredHeight() 得到View测量的高度和宽度,然后确定View布局在Window中的位置,即确定mLeft,mRight,mTop和mBottom值。函数原型:

1
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}

通常View不需要额外实现,ViewGroup会实现该方法,根据布局特性计算每个子View上述四个值,调用 View#setFrame(int left, int top, int right, int bottom) 进行设置。View布局完成后才可以调用 View#getWidth()View#getHeight() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Return the width of the your view.
*
* @return The width of your view, in pixels.
*/
@ViewDebug.ExportedProperty(category = "layout")
public final int getWidth() {
return mRight - mLeft;
}

/**
* Return the height of your view.
*
* @return The height of your view, in pixels.
*/
@ViewDebug.ExportedProperty(category = "layout")
public final int getHeight() {
return mBottom - mTop;
}

Tips:
Activity在onResume()方法执行后才会执行 ViewRootImpl#performTraversals() ,所以获得View的长度和宽度建议使用 View#post(),相比ViewTreeObserver简单性能好。demo如下:

1
2
3
4
5
6
7
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth()
int height = view.getMeasuredHeight();
}
});

onDraw()

View#draw() 方法明确了View绘制的流程,如下:

1
2
3
4
5
6
1. Draw the background
2. If necessary, save the canvas' layers to prepare for fading
3. Draw view's content
4. Draw children
5. If necessary, draw the fading edges and restore layers
6. Draw decorations (scrollbars for instance)

View#onDraw() 属于第三步,注意padding属性实现,其函数原型:

1
2
3
4
5
6
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {}

一些Tips:

  1. View#post() 方法可以替代Handler
  2. View#onAttachedToWindow()View#onDetachedFromWindow() 注意资源的回收,比如动画或线程。
  3. View#onDraw() 中不要有对象的分配等容易引起垃圾回收的操作,注意invalidate()和requestLayout()调用次数。

Drawable总结

Drawable表示一种可以画在屏幕上的抽象概念,一般通过XML进行定义,也可以通过代码创建具体的Drawable对象;Drawable是抽象类,每个具体的Drawable都是其子类;有些Drawable会有内部宽/高(Drawable#getIntrinsicWidth()Drawable#getIntrinsicHeight()),比如一张图片。以下对常见Drawable进行介绍:

BitmapDrawable

位图支持三种格式:.png,.jpg,.gif,推荐.png格式。存在res/drawable/路径下,APK构建期间会对该路径下的位图进行压缩,所以如果需要读位图的bit stream时,需要把位图存于res/raw/路径下。

1
2
3
4
5
文件路径:
res/drawable/filename.png (.png, .jpg, or .gif)
资源引用:
In Java: R.drawable.filename
In XML: @[package:]drawable/filename

同样可以使用XML进行定义,好处是可以指定额外的属性,语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<bitmap
xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@[package:]drawable/drawable_resource"
android:antialias=["true"|"false"]
android:dither=["true"|"false"]
android:filter=["true"|"false"]
android:gravity=["top"|"bottom"|"left"|"right"|"center_vertical"|
"fill_vertical"|"center_horizontal"|"fill_horizontal"|
"center"|"fill"|"clip_vertical"|"clip_horizontal"]
android:mipMap=["true"|"false"]
android:tileMode=["disabled"|"clamp"|"repeat"|"mirror"] />

NinePatchDrawable

.9图对应的Drawable子类,文件位于res/drawable/下,如下:

1
2
3
4
5
文件路径:
res/drawable/filename.9.png
资源使用:
In Java: R.drawable.filename
In XML: @[package:]drawable/filename

同样可以使用XML进行定义,语法如下:

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<nine-patch
xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@[package:]drawable/drawable_resource"
android:dither=["true"|"false"] />

LayerDrawable

表示一种层次化的Drawable集合,下层Item会覆盖上层Item,使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
文件路径:
res/drawable/filename.xml
资源引用:
In Java: R.drawable.filename
In XML: @[package:]drawable/filename
语法:
<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:drawable="@[package:]drawable/drawable_resource"
android:id="@[+][package:]id/resource_name"
android:top="dimension"
android:right="dimension"
android:bottom="dimension"
android:left="dimension" />
</layer-list>

为了防止Item默认scale至父容器,通常<Item>中嵌套<bitmap>标签达到想要的层次效果,demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:src="@drawable/android_red"
android:gravity="center" />
</item>
<item android:top="10dp" android:left="10dp">
<bitmap android:src="@drawable/android_green"
android:gravity="center" />
</item>
<item android:top="20dp" android:left="20dp">
<bitmap android:src="@drawable/android_blue"
android:gravity="center" />
</item>
</layer-list>

StateListDrawable

对应<selector>标签,表示View各种状态的Drawable集合,使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
文件路径:
res/drawable/filename.xml
资源引用:
In Java: R.drawable.filename
In XML: @[package:]drawable/filename
语法:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:constantSize=["true"|"false"]
android:dither=["true"|"false"]
android:variablePadding=["true"|"false"] >
<item
android:drawable="@[package:]drawable/drawable_resource"
android:state_pressed=["true"|"false"]
android:state_focused=["true"|"false"]
android:state_hovered=["true"|"false"]
android:state_selected=["true"|"false"]
android:state_checkable=["true"|"false"]
android:state_checked=["true"|"false"]
android:state_enabled=["true"|"false"]
android:state_activated=["true"|"false"]
android:state_window_focused=["true"|"false"] />
</selector>

其中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

### LevelListDrawable
Drawable集合中每个Drawable都有Level,Level取值从0到10000,可以通过Drawable#setLevel()和ImageView#setImageLevel()改变Level,从而改变对应的Drawable,匹配规则是从上到下寻找第一个使得Level处于minLevel和maxLevel之中的Item。
``` xml
文件路径:
res/drawable/filename.xml
资源引用:
In Java: R.drawable.filename
In XML: @[package:]drawable/filename
语法:
<?xml version="1.0" encoding="utf-8"?>
<level-list
xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:drawable="@drawable/drawable_resource"
android:maxLevel="integer"
android:minLevel="integer" />
</level-list>

TransitionDrawable

实现两个Drawable之间的淡入淡出效果,向前启动时调用 Drawable#startTransition(),向后调用 Drawable#reverseTransition()。使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
文件路径:
res/drawable/filename.xml
资源引用:
In Java: R.drawable.filename
In XML: @[package:]drawable/filename
语法:
<?xml version="1.0" encoding="utf-8"?>
<transition
xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:drawable="@[package:]drawable/drawable_resource"
android:id="@[+][package:]id/resource_name"
android:top="dimension"
android:right="dimension"
android:bottom="dimension"
android:left="dimension" />
</transition>

InsertDrawable

用于嵌入其他Drawable并能留出一定间距的场景。使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
文件路径:
res/drawable/filename.xml
资源引用:
In Java: R.drawable.filename
In XML: @[package:]drawable/filename
语法:
<?xml version="1.0" encoding="utf-8"?>
<inset
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/drawable_resource"
android:insetTop="dimension"
android:insetRight="dimension"
android:insetBottom="dimension"
android:insetLeft="dimension" />

ClipDrawable

定义一个根据Level裁剪Drawable而生成的Drawable,通常用于进度条的实现。使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
文件路径:
res/drawable/filename.xml
资源引用:
In Java: R.drawable.filename
In XML: @[package:]drawable/filename
语法:
<?xml version="1.0" encoding="utf-8"?>
<clip
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/drawable_resource"
android:clipOrientation=["horizontal"|"vertical"]
android:gravity=["top"|"bottom"|"left"|"right"|"center_vertical"|
"fill_vertical"|"center_horizontal"|"fill_horizontal"|
"center"|"fill"|"clip_vertical"|"clip_horizontal"] />

默认Level为0,表示全部裁剪;level最大值为10000,表示不裁剪。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

### ScaleDrawable
定义一个根据Level缩放Drawable而生成的Drawable,使用如下:
``` xml
文件路径:
res/drawable/filename.xml
资源引用:
In Java: R.drawable.filename
In XML: @[package:]drawable/filename
语法:
<?xml version="1.0" encoding="utf-8"?>
<scale
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/drawable_resource"
android:scaleGravity=["top"|"bottom"|"left"|"right"|"center_vertical"|
"fill_vertical"|"center_horizontal"|"fill_horizontal"|
"center"|"fill"|"clip_vertical"|"clip_horizontal"]
android:scaleHeight="percentage"
android:scaleWidth="percentage" />

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

### GradientDrawable
使用XML定义的通用图形,使用如下:
``` xml
文件路径:
res/drawable/filename.xml
资源引用:
In Java: R.drawable.filename
In XML: @[package:]drawable/filename
语法:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape=["rectangle"|"oval"|"line"|"ring"] >
<corners
android:radius="integer"
android:topLeftRadius="integer"
android:topRightRadius="integer"
android:bottomLeftRadius="integer"
android:bottomRightRadius="integer" />
<gradient
android:angle="integer"
android:centerX="float"
android:centerY="float"
android:centerColor="integer"
android:endColor="color"
android:gradientRadius="integer"
android:startColor="color"
android:type=["linear"|"radial"|"sweep"]
android:useLevel=["true"|"false"] />
<padding
android:left="integer"
android:top="integer"
android:right="integer"
android:bottom="integer" />
<size
android:width="integer"
android:height="integer" />
<solid
android:color="color" />
<stroke
android:width="integer"
android:color="color"
android:dashWidth="integer"
android:dashGap="integer" />
</shape>

Tips:

1.

1
2
3
4
5
6
7
8
9
10
11
12

**2.** 以下属性仅对```android:shape="ring"```有效:

|Attribute|Description|
|:-|:-|
|```android:innerRadius```|内半径|
|```android:innerRadiusRatio```|内半径比率,最终值为环宽度/n,默认n=9;可被innerRadius覆盖|
|```android:thickness```|环的厚度|
|```android:thicknessRatio```|环厚度比率,最终值为环宽度/n,默认n=3;可被thickness覆盖|
|```android:useLevel```|true表示用于LevelListDrawable,通常应该是false|

**3.** 渐变色```<gradient>

Attribute Description
android:angle 渐变角度,0表示从左到右,90表示从下往上,必须是45倍数,默认为0
android:centerX 渐变中心X坐标,取值[0-1],默认0.5
android:centerY 渐变中心Y坐标,取值[0-1],默认0.5
android:startColor 渐变开始颜色
android:endColor 渐变结束颜色
android:centerColor 渐变中间颜色,可选
android:type 类型:linear,radial,sweep
android:gradientRadius 仅对type=radial有效,渐变半径
android:useLevel true表示用于LevelListDrawable

4. <padding>不建议使用,测试的时候不生效,可以通过InsertDrawable或者LayerDrawable嵌套实现对应效果。

5. <size>Drawable#getIntrinsicWidth()Drawable#getIntrinsicHeight() 对应;Drawable实际大小通过 Drawable#getBounds() 获得

6. <solid> 表示填充纯色

7. <stroke>

Attribute Description
android:width 线宽度
android:color 线颜色
android:dashGap 线间隔长度
android:dashWidth 如果存在间隔,间隔线的宽度

参考

[1] https://developer.android.com/training/custom-views/create-view.html
[2] https://developer.android.com/guide/topics/resources/drawable-resource.html
[3] 《Android开发艺术探索》 任玉刚 著