自己绘制控件
自己绘制控件
自定义View
的步骤
- 布局
- 自定义
View
的属性 - XML或Java代码控制布局
- 测量——
onMeasure(int,int)
(该函数可重写可不重写,具体看需求) - 布局——
onLayout(boolean,int,int,int,int)
- 绘制——
onDraw(Canvas canvas)
- 测量——
- 自定义
- 处理逻辑
- 构造函数(获得
View
属性值,初始化等) - 自定义处理逻辑
- 事件响应(
onClick
,onScroll
等)
- 构造函数(获得
- 提供数据(数据和处理逻辑分开,Adapter)
- 使用
- layout
- Activity
其他自定义控件的介绍可参见
1. 布局
1.1 自定义View
的属性
1.1.1 定义公共属性
在res/values
下面新建attrs.xml
属性文件1
2
3
4
5
6
7
8
9
<resources>
<!--name 是自定义属性名,一般采用驼峰命名,可以随意。 format 是属性的单位-->
<attr name="titleSize" format="dimension"></attr>
<attr name="titleText" format="string"></attr>
<attr name="titleColor" format="color"></attr>
<attr name="titleBackgroundColor" format="color"></attr>
...
</resources>
第一部分是公共的属性format
字段后面的属性单位AS
开发的话IDE
会自动有提示,基本包括如下:
- dimension(字体大小)
- string(字符串)
- color(颜色)
- boolean(布尔类型)
- float(浮点型)
- integer(整型)
- enmu(枚举)
- fraction(百分比)等
1.1.2 定义控件的主题样式
attrs.xml
属性文件1
2
3
4
5
6
7
8
9
10<resources>
...
<!--name 是自定义控件的类名-->
<declare-styleable name="MyCustomView">
<attr name="titleSize"></attr>
<attr name="titleText"></attr>
<attr name="titleColor"></attr>
<attr name="titleBackgroundColor"></attr>
</declare-styleable>
</resources>
第二部分是自定义控件MyCustomView
的主题样式。公共属性可以被多个自定义控件主题样式使用。
1.1.3 获得View
的属性值
一般在构造函数中去获得View
的属性值
1 | public class MyCustomView extends View { |
- 第一步通过
theme.obtainStyledAttributes()
方法获得自定义控件的主题样式数组。 - 第二步就是遍历每个属性来获得对应属性的值,也就是我们在
xml
布局文件中写的属性值。注意:在分支
case
里R.styleable.
后面的属性名称有一个规则:控件的样式主题名
+_
+属性名
。 - 循环结束之后记得调用
a.recycle()
回收资源。 - 至此就获得了自定义控件的属性值,这些属性值可以在
onDraw
函数中用来绘制View
。
1.2 XML或Java代码控制布局
XML布局1
2android:layout_width="match_parent"
android:background="#CCCCCCCC"
或Java代码控制布局1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18private LinearLayout linearLayout;
private LinearLayout.LayoutParams linearLayoutParams;
private TextView textV;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
linearLayout = new LinearLayout(this);
linearLayout.setOrientation(LinearLayout.VERTICAL);
setContentView(linearLayout);
linearLayoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
textV = new TextView(this);
textV.setTextSize(30.0f);
textV.setTextColor(Color.BLACK);
linearLayout.addView(textV, linearLayoutParams);
...
}
真正起效通过以下三个函数实现
- 测量——
onMeasure(int,int)
(该函数可重写可不重写,具体看需求) - 布局——
onLayout(boolean,int,int,int,int)
- 绘制——
onDraw(Canvas canvas)
自定义控件可以直接通过重写这三个函数实现,下面将详细分析这三个函数
1.2.1 测量——onMeasure()
- 简介
onMeasure
函数(default):
View.java
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
- 该函数比
onDraw
先执行,由子控件重写,决定子控件View的大小(宽高值)。它在调用Measure()
时被调用。 父控件将自己的大小和mode,通过
MeasureSpec
传给子控件。MeasureSpec
由大小和模式组成,封装了父布局传递给子布局的布局要求- 每个
MeasureSpec
代表了一组宽度和高度的要求 MeasureSpec
的三种模式:UNSPECIFIED
(未指定),父元素部队自元素施加任何束缚,子元素可以得到任意想要的大小;EXACTLY
(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;AT_MOST
(至多),子元素至多达到指定大小的值。
MeasureSpec
定义于View.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
...
}
onMeasure
函数常用的三个函数:static int getMode(int measureSpec)
:根据提供的测量值(格式)提取模式(上述三个模式之一)static int getSize(int measureSpec)
:根据提供的测量值(格式)提取大小值(这个大小也就是我们通常所说的大小)static int makeMeasureSpec(int size,int mode)
:根据提供的大小值和模式创建一个测量值(格式)
- 子控件根据
layout
文件中父控件,子控件的"MATCH_PARENT"
,"WRAP_CONTENT"
,指定值
,padding内边距
和margin外边距
等等对于View
大小的约束,计算宽高值(未必是最终大小)。 - 计算出实际的高和宽通过
setMeasuredDimension()
保存,如果所测的视图是ViewGroup
通过measureChild
方法递归的计算其中的每一个子View
。
setMeasuredDimension
函数:
setMeasuredDimension
方法必须由onMeasure(int, int)
来调用,来存储测量的宽,高值,定义如下:1
2
3
4
5
6protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
- 多个
View
或者ViewGroup
嵌套:
子控件如果有多个View
或者ViewGroup
嵌套,需要循环遍历视图中所有的View
。
1 |
|
measureChildren
函数:ViewGroup.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
* 遍历所有的子view去测量自己(跳过GONE类型View)
* @param widthMeasureSpec 父视图的宽详细测量值
* @param heightMeasureSpec 父视图的高详细测量值
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}measureChild
函数:ViewGroup.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 取得子视图的布局参数
final LayoutParams lp = child.getLayoutParams();
// 通过getChildMeasureSpec获取最终的宽高详细测量值
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 将计算好的宽高详细测量值传入measure方法,完成最后的测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
1.2.2 布局——onLayout()
View.java
1
2protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
- 被
public void layout(int l, int t, int r, int b)
(View.java
)调用 - 递归计算整个
View
树实际的位置,来实现该控件内部子控件的布局情况 - 为抽象方法,所以在继承
View
时必须要重写该方法(onMeasure
不需要),并调用其所有子View
的layout
函数 - 参数介绍
changed
当前View的大小和位置改变了left
左部位置(相对于父视图)top
顶部位置(相对于父视图)right
右部位置(相对于父视图)bottom
底部位置(相对于父视图)
×分析至layout函数×public void layout(int l, int t, int r, int b)
用于当前ViewGroup
中的子控件的布局
1.2.3 绘制——onDraw()
如何绘制这个View。
自绘控件的内容都是自己绘制出来的,在View的onDraw
方法中完成绘制1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setTextSize(titleSize);
/**
* 得到自定义View的titleText内容的宽和高
*/
mBound = new Rect();
mPaint.getTextBounds(titleText, 0, titleText.length(), mBound);
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(titleBackgroundColor);
canvas.drawCircle(getWidth() / 2f, getWidth() / 2f, getWidth() / 2f, mPaint);
mPaint.setColor(titleColor);
canvas.drawText(titleText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
}
2. 处理逻辑
2.1 构造函数
自定义View
一般需要实现以下几个构造函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public MyCustomView(Context context) {
this(context, null);
}
public MyCustomView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
(Build.VERSION_CODES.LOLLIPOP)
public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
2.2 自定义处理逻辑
2.3 事件响应(onClick
, onScroll
等)
3. 提供数据(数据和处理逻辑分开,Adapter)
4. 使用自定义View
4.1 layout
activity_main.xml
布局中使用自定义View
(自定义的属性)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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=".MainActivity">
<com.example.customview.MyCustomView
android:layout_width="wrap_content"
android:layout_height="match_parent"
custom:titleColor="@android:color/black"
custom:titleSize="25sp"
custom:titleBackgroundColor="#ff0000"
custom:titleText="自定义的View" />
</LinearLayout>
4.2 Activity
5. 参考文献
Android自定义控件的三种实现方式
Android自定义控件View(一)
自定义控件详解(五):onMeasure()、onLayout()
Android 自定义 view(四)—— onMeasure 方法理解
ANDROID自定义视图——onMeasure,MeasureSpec源码 流程 思路详解
ANDROID自定义视图——onLayout源码 流程 思路详解