这里写目录标题
前言
自定义View在实际开发中经常会用到,因此掌握自定义View是必须的。它能帮助我们更好的了解View的绘制流程,提高工作效率和代码质量。
什么是自定义View
自定义View通俗来说就是根据自己的需要实现特定化需求的View。
为什么要用自定义View
在实际项目开发中,UI小姐姐给我们设计的视图组件是各种各样的 ,android提供的基础组件是无法满足实际需要的,这种情况下我们就需要自定义View了。
1.自定义View基础
1.1自定义View的分类
自定义View分为以下几类
类型 | 定义 |
---|---|
自定义组合控件 | 多个控件组合成为一个新的控件,方便多处复用(eg:app通用标题栏) |
继承系统View控件 | 继承自TextView等系统控件,在系统控件的基础功能上进行扩展 |
直接继承View | 不复用系统控件逻辑,继承View进行功能定义 |
继承系统ViewGroup控件 | 继承自LinearLayout等系统控件,在系统控件的基础功能上进行扩展 |
直接继承ViewGroup | 不复用系统控件逻辑,继承ViewGroup进行功能定义 |
1.2.View的绘制流程
View的绘制主要由measure()、layout()、draw()这个三个函数完成的。
函数名 | 作用 | 相关函数 |
---|---|---|
measure() | 测量View的宽高 | measure(),setMeasuredDimension(),onMeasure() |
layout() | 计算当前View以及子View的位置 | ayout(),onLayout(),setFrame() |
draw() | 视图的绘制工作 | draw(),onDraw() |
1.3.坐标系
坐标系上节有详细讲过
1.4.构造函数
public class MyView extends View {
/**
* 在java代码里new的时候会用到
* @param context
*/
public MyView(Context context) {
super(context);
}
/**
* 在xml布局文件中使用时自动调用
* @param context
*/
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
/**
* 不会自动调用,如果有默认style时,在第二个构造函数中调用
* @param context
* @param attrs
* @param defStyleAttr
*/
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 只有在API版本>21时才会用到
* 不会自动调用,如果有默认style时,在第二个构造函数中调用
* @param context
* @param attrs
* @param defStyleAttr
* @param defStyleRes
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
1.5.自定义属性
Android系统的控件以android开头的都是系统自带的属性。为了方便配置自定义View的属性,我们也可以自定义属性值。
Android自定义属性可分为以下几步:
1.自定义一个View
2.编写values/attrs.xml,在其中编写styleable和item等标签元素
3.在布局文件中View使用自定义的属性(注意namespace)
5.在View的构造方法中通过TypedArray获取
实例说明
1.5.1.自定义属性的声明文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="myView">
<attr name="test" format="string" />
<attr name="testAttr" format="integer" />
</declare-styleable>
</resources>
1.5.2.自定义View类
public class MyTextView extends View {
private static final String TAG = MyTextView.class.getSimpleName();
//在View的构造方法中通过TypedArray获取
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.myView);
String test= ta.getString(R.styleable.myView_test);
int textAttr = ta.getInteger(R.styleable.myView_testAttr, -1);
Log.e(TAG, "test= " + test+ " , textAttr = " + textAttr);
ta.recycle();
}
}
1.5.3.在布局文件中使用
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res/com.example.test"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.MyView.MyTextView
android:layout_width="100dp"
android:layout_height="200dp"
app:testAttr="10"
app:text="hello android" />
</RelativeLayout>
1.5.4.属性值的类型format
(1). reference:参考某一资源ID
属性定义:
<declare-styleable name = "名称">
<attr name = "background" format = "reference" />
</declare-styleable>
属性使用:
<ImageView android:background = "@drawable/图片ID"/>
(2).color:颜色值
属性定义:
<attr name = "textColor" format = "color" />
属性使用:
<TextView android:textColor = "#00FF00" />
(3).boolean:布尔值
属性定义:
<attr name = "focusable" format = "boolean" />
属性使用:
<Button android:focusable = "true"/>
(4). dimension:尺寸值
属性定义:
<attr name = "layout_width" format = "dimension" />
属性使用:
<Button android:layout_width = "18dp"/>
(5). float:浮点值
属性定义:
<attr name = "fromAlpha" format = "float" />
属性使用:
<alpha android:fromAlpha = "1.0"/>
(6). integer:整型值
属性定义:
<attr name = "framesCount" format="integer" />
属性使用:
<animated-rotate android:framesCount = "12"/>
(7). string:字符串
属性定义:
<attr name = "text" format = "string" />
属性使用:
<TextView android:text = "我是文本"/>
(8). fraction:百分数
属性定义:
<attr name = "pivotX" format = "fraction" />
属性使用:
<rotate android:pivotX = "100%"/>
(9). enum:枚举值
属性定义:
<declare-styleable name="名称">
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
</declare-styleable>
属性使用:
<LinearLayout
android:orientation = "vertical">
</LinearLayout>
注意:枚举类型的属性在使用的过程中只能使用其中一个,不能 android:orientation = “horizontal|vertical"
(10). flag:位或运算
属性定义:
<declare-styleable name="名称">
<attr name="gravity">
<flag name="top" value="0x01" />
<flag name="bottom" value="0x02" />
<flag name="left" value="0x04" />
<flag name="right" value="0x08" />
<flag name="center_vertical" value="0x16" />
...
</attr>
</declare-styleable>
属性使用:
<TextView android:gravity="bottom|left"/>
注意:位运算类型的属性在使用的过程中可以使用多个值
(11). 混合类型:属性定义时可以指定多种类型值
属性定义:
<declare-styleable name = "名称">
<attr name = "background" format = "reference|color" />
</declare-styleable>
属性使用:
<ImageView
android:background = "@drawable/图片ID" />
或者:
<ImageView
android:background = "#00FF00" />
2.View绘制流程
View 的绘制流程分为三步:在自定义View的时候一般需要重写父类的onMeasure()、onLayout()、onDraw()三个方法来完成视图的绘制工作。一个完整的绘制流程包括measure、layout、draw三个步骤,其中:
measure:测量。系统会先根据xml布局文件和代码中对控件属性的设置,来获取或者计算出每个View和ViewGrop的尺寸,并将这些尺寸保存下来。
layout:布局。根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置。
draw:绘制。确定好位置后,就将这些控件绘制到屏幕上。
2.1 Measure()
重要的内部类MeasureSpec
MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。
MeasureSpec是一个int型变量,值保存在一个int值当中。一个int值有32位,MeasureSpec让32位中的最高2位代表mode,后30位代表size。其中mode有UNSPECIFIED(00)= 0、EXACTLY(01)=1、AT_MOST(10)=2
在MeasureSpec当中一共存在三种mode:UNSPECIFIED、EXACTLY 和AT_MOST。
对于View来说,MeasureSpec的mode和Size有如下意义
模式 | 含义 | 对应 |
---|---|---|
EXACTLY | 精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size | match_parent |
AT_MOST | 最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值 | wrap_content |
UNSPECIFIED | 无限制,View对尺寸没有任何限制,View设置为多大就应当为多大(把该View放在NestScrollView、ScrollView中) | 一般系统内部使用 |
使用方法
// 获取测量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)
// 获取测量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)
// 通过Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
在View当中,MeasureSpace的测量代码如下:
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) {
//当父View要求一个精确值时,为子View赋值
case MeasureSpec.EXACTLY:
//如果子view有自己的尺寸,则使用自己的尺寸
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//当子View是match_parent,将父View的大小赋值给子View
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
//如果子View是wrap_content,设置子View的最大尺寸为父View
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局给子View了一个最大界限
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//如果子view有自己的尺寸,则使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 父View的尺寸为子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//父View的尺寸为子View的最大尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局对子View没有做任何限制
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//如果子view有自己的尺寸,则使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//因父布局没有对子View做出限制,当子View为MATCH_PARENT时则大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这里需要注意,这段代码只是在为子View设置MeasureSpec参数而不是实际的设置子View的大小。子View的最终大小需要在View中具体设置。
从源码可以看出来,子View的测量模式是由自身LayoutParam和父View的MeasureSpec来决定的。
在测量子View大小时:
父View mode | 子View |
---|---|
UNSPECIFIED | 父布局没有做出限制,子View有自己的尺寸,则使用,如果没有则为0 |
EXACTLY | 父布局采用精准模式,有确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围 |
AT_MOST | 父布局采用最大模式,存在确切的大小,如果有大小则直接使用,如果子View没有大小,子View不得超出父view的大小范围 |
onMeasure()
整个测量过程的入口位于View的measure方法当中,该方法做了一些参数的初始化之后调用了onMeasure方法,这里我们主要分析onMeasure。
onMeasure方法的源码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
很简单这里只有一行代码,涉及到了三个方法我们挨个分析。
1.setMeasuredDimension(int measuredWidth, int measuredHeight) :该方法用来设置View的宽高,在我们自定义View时也会经常用到。
2.getDefaultSize(int size, int measureSpec):该方法用来获取View默认的宽高,结合源码来看。
/**
* 有两个参数size和measureSpec
* 1、size表示View的默认大小,它的值是通过`getSuggestedMinimumWidth()方法来获取的,之后我们再分析。
* 2、measureSpec则是我们之前分析的MeasureSpec,里面存储了View的测量值以及测量模式
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//从这里我们看出,对于AT_MOST和EXACTLY在View当中的处理是完全相同的。所以在我们自定义View时要对这两种模式做出处理。
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
getSuggestedMinimumWidth():getHeight和该方法原理是一样的,这里只分析这一个。
//当View没有设置背景时,默认大小就是mMinWidth,这个值对应Android:minWidth属性,如果没有设置时默认为0.
//如果有设置背景,则默认大小为mMinWidth和mBackground.getMinimumWidth()当中的较大值。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
ViewGroup的测量过程与View有一点点区别,其本身是继承自View,它没有对View的measure方法以及onMeasure方法进行重写。
为什么没有重写onMeasure呢?ViewGroup除了要测量自身宽高外还需要测量各个子View的大小,而不同的布局测量方式也都不同(可参考LinearLayout以及FrameLayout),所以没有办法统一设置。因此它提供了测量子View的方法measureChildren()以及measureChild()帮助我们对子View进行测量。
measureChildren()以及measureChild()的源码这里不再分析,大致流程就是遍历所有的子View,然后调用View的measure()方法,让子View测量自身大小。具体测量流程上面也以及介绍过了
measure过程会因为布局的不同或者需求的不同而呈现不同的形式,使用时还是要根据业务场景来具体分析,如果想再深入研究可以看一下LinearLayout的onMeasure方法。
2.2 Layout()
layout()过程,对于View来说用来计算View的位置参数,对于ViewGroup来说,除了要测量自身位置,还需要测量子View的位置。
layout()方法是整个Layout()流程的入口,看一下这部分源码
/**
* 这里的四个参数l、t、r、b分别代表View的左、上、右、下四个边界相对于其父View的距离。
*
*/
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// ....省略其它部分
}
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
// ....省略其它部分
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
invalidateParentCaches();
}
mPrivateFlags |= drawn;
mBackgroundSizeChanged = true;
mDefaultFocusHighlightSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}
从源码我们知道,在layout()方法中已经通过setOpticalFrame(l, t, r, b)或 setFrame(l, t, r, b)方法对View自身的位置进行了设置,所以onLayout(changed, l, t, r, b)方法主要是ViewGroup对子View的位置进行计算。
2.3 Draw()
draw流程也就是的View绘制到屏幕上的过程,整个流程的入口在View的draw()方法之中,而源码注释也写的很明白,整个过程可以分为6个步骤。
如果需要,绘制背景。
1.有过有必要,保存当前canvas。
2.绘制View的内容。
3.绘制子View。
4.如果有必要,绘制边缘、阴影等效果。
5.绘制装饰,如滚动条等等。
6.通过各个步骤的源码再做分析:
public void draw(Canvas canvas) {
int saveCount;
// 1. 如果需要,绘制背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 2. 有过有必要,保存当前canvas。
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 3. 绘制View的内容。
if (!dirtyOpaque) onDraw(canvas);
// 4. 绘制子View。
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 6. 绘制装饰,如滚动条等等。
onDrawForeground(canvas);
// we're done...
return;
}
}
/**
* 1.绘制View背景
*/
private void drawBackground(Canvas canvas) {
//获取背景
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
//获取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0,则会在平移后的canvas上面绘制背景。
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
/**
* 3.绘制View的内容,该方法是一个空的实现,在各个业务当中自行处理。
*/
protected void onDraw(Canvas canvas) {
}
/**
* 4. 绘制子View。该方法在View当中是一个空的实现,在各个业务当中自行处理。
* 在ViewGroup当中对dispatchDraw方法做了实现,主要是遍历子View,并调用子类的draw方法,一般我们不需要自己重写该方法。
*/
protected void dispatchDraw(Canvas canvas) {
}
至此View的绘制流程结束。
requestLayout重新绘制视图
子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。
invalidate在UI线程中重新绘制视图
当子View调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程(只绘制需要重绘的视图)。
postInvalidate在非UI线程中重新绘制视图
这个方法与invalidate方法的作用是一样的,都是使View树重绘,但两者的使用条件不同,postInvalidate是在非UI线程中调用,invalidate则是在UI线程中调用。
总结
本文参考了 Android自定义View全解
本文主要对常用的自定义View的方式进行了总结,并简单分析了View的绘制流程。后面会对各种自定义View做出简单的例子。