目录
目录.png
1. 自定义View基础

1.1 分类

自定义View的实现方式有以下几种

类型定义
自定义组合控件多个控件组合成为一个新的控件,方便多处复用
继承系统View控件继承自TextView等系统控件,在系统控件的基础功能上进行扩展
继承View不复用系统控件逻辑,继承View进行功能定义
继承系统ViewGroup继承自LinearLayout等系统控件,在系统控件的基础功能上进行扩展
继承ViewViewGroup不复用系统控件逻辑,继承ViewGroup进行功能定义

1.2 View绘制流程

View的绘制基本由measure()、layout()、draw()这个三个函数完成

函数作用相关方法
measure()测量View的宽高measure(),setMeasuredDimension(),onMeasure()
layout()计算当前View以及子View的位置layout(),onLayout(),setFrame()
draw()视图的绘制工作draw(),onDraw()

1.3 坐标系
在Android坐标系中,以屏幕左上角作为原点,这个原点向右是X轴的正轴,向下是Y轴正轴。如下所示:
Android坐标系.png
除了Android坐标系,还存在View坐标系,View坐标系内部关系如图所示。
视图坐标系.png

View获取自身高度

由上图可算出View的高度:
width = getRight() - getLeft();height = getBottom() - getTop();
View的源码当中提供了getWidth()和getHeight()方法用来获取View的宽度和高度,其内部方法和上文所示是相同的,我们可以直接调用来获取View得宽高。

View自身的坐标

通过如下方法可以获取View到其父控件的距离。
getTop();获取View到其父布局顶边的距离。getLeft();获取View到其父布局左边的距离。getBottom();获取View到其父布局顶边的距离。getRight();获取View到其父布局左边的距离。

1.4 构造函数

无论是我们继承系统View还是直接继承View,都需要对构造函数进行重写,构造函数有多个,至少要重写其中一个才行。如我们新建TestView,
public class TestView extends View { /** * 在java代码里new的时候会用到 * @param context */ public TestView(Context context) { super(context); } /** * 在xml布局文件中使用时自动调用 * @param context */ public TestView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } /** * 不会自动调用,如果有默认style时,在第二个构造函数中调用 * @param context * @param attrs * @param defStyleAttr */ public TestView(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 TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } }

1.5 自定义属性

Android系统的控件以android开头的都是系统自带的属性。为了方便配置自定义View的属性,我们也可以自定义属性值。
Android自定义属性可分为以下几步:
自定义一个View编写values/attrs.xml,在其中编写styleable和item等标签元素在布局文件中View使用自定义的属性(注意namespace)在View的构造方法中通过TypedArray获取

实例说明

自定义属性的声明文件自定义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.test); String text = ta.getString(R.styleable.test_testAttr); int textAttr = ta.getInteger(R.styleable.test_text, -1); Log.e(TAG, "text = " + text + " , textAttr = " + textAttr); ta.recycle(); } }布局文件中使用

属性值的类型format

(1). reference:参考某一资源ID
属性定义:属性使用:
(2). color:颜色值
属性定义:属性使用:
(3). boolean:布尔值
属性定义:属性使用:
(4). dimension:尺寸值
属性定义:属性使用:
(5). float:浮点值
属性定义:属性使用:
(6). integer:整型值
属性定义:属性使用:
(7). string:字符串
属性定义:属性使用:
(8). fraction:百分数
属性定义:属性使用:
(9). enum:枚举值
属性定义:属性使用:
注意:枚举类型的属性在使用的过程中只能同时使用其中一个,不能 android:orientation = “horizontal|vertical"
(10). flag:位或运算
属性定义:...属性使用:
注意:位运算类型的属性在使用的过程中可以使用多个值
(11). 混合类型:属性定义时可以指定多种类型值
属性定义:属性使用:或者:
2. View绘制流程
这一章节偏向于解释View绘制的源码实现,可以更好地帮助我们掌握整个绘制过程。
View的绘制基本由measure()、layout()、draw()这个三个函数完成

函数作用相关方法
measure()测量View的宽高measure(),setMeasuredDimension(),onMeasure()
layout()计算当前View以及子View的位置layout(),onLayout(),setFrame()
draw()视图的绘制工作draw(),onDraw()

2.1 Measure()

MeasureSpec

MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。
MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。即MeasureSpec=mode+size。
在MeasureSpec当中一共存在三种mode:UNSPECIFIED、EXACTLY和
AT_MOST。
对于View来说,MeasureSpec的mode和Size有如下意义

模式意义对应
EXACTLY精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Sizematch_parent
AT_MOST最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值wrap_content
UNSPECIFIED无限制,View对尺寸没有任何限制,View设置为多大就应当为多大一般系统内部使用

使用方式
// 获取测量模式(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)); }
很简单这里只有一行代码,涉及到了三个方法我们挨个分析。
setMeasuredDimension(int measuredWidth, int measuredHeight):该方法用来设置View的宽高,在我们自定义View时也会经常用到。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()

要计算位置首先要对Android坐标系有所了解,前面的内容我们也有介绍过。
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; //这里通过setFrame或setOpticalFrame方法确定View在父容器当中的位置。 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); //调用onLayout方法。onLayout方法是一个空实现,不同的布局会有不同的实现。 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); } }
从源码我们知道,在layout()方法中已经通过setOpticalFrame(l, t, r, b)或setFrame(l, t, r, b)方法对View自身的位置进行了设置,所以onLayout(changed, l, t, r, b)方法主要是ViewGroup对子View的位置进行计算。
有兴趣的可以看一下LinearLayout的onLayout源码,可以帮助加深理解。

2.3 Draw()

draw流程也就是的View绘制到屏幕上的过程,整个流程的入口在View的draw()方法之中,而源码注释也写的很明白,整个过程可以分为6个步骤。
如果需要,绘制背景。有过有必要,保存当前canvas。绘制View的内容。绘制子View。如果有必要,绘制边缘、阴影等效果。绘制装饰,如滚动条等等。
通过各个步骤的源码再做分析:
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) { }
3. 自定义组合控件
自定义组合控件就是将多个控件组合成为一个新的控件,主要解决多次重复使用同一类型的布局。如我们顶部的HeaderView以及dailog等,我们都可以把他们组合成一个新的控件。
我们通过一个自定义HeaderView实例来了解自定义组合控件的用法。
1. 编写布局文件
布局很简单,中间是title的文字,左边是返回按钮,右边是一个添加按钮。
2. 实现构造方法
//因为我们的布局采用RelativeLayout,所以这里继承RelativeLayout。 //关于各个构造方法的介绍可以参考前面的内容 public class YFHeaderView extends RelativeLayout { public YFHeaderView(Context context) { super(context); } public YFHeaderView(Context context, AttributeSet attrs) { super(context, attrs); } public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } }
3. 初始化UI
//初始化UI,可根据业务需求设置默认值。 private void initView(Context context) { LayoutInflater.from(context).inflate(R.layout.view_header, this, true); img_left = (ImageView) findViewById(R.id.header_left_img); img_right = (ImageView) findViewById(R.id.header_right_img); text_center = (TextView) findViewById(R.id.header_center_text); layout_root = (RelativeLayout) findViewById(R.id.header_root_layout); layout_root.setBackgroundColor(Color.BLACK); text_center.setTextColor(Color.WHITE); }
4. 提供对外的方法
可以根据业务需求对外暴露一些方法。
//设置标题文字的方法 private void setTitle(String title) { if (!TextUtils.isEmpty(title)) { text_center.setText(title); } } //对左边按钮设置事件的方法 private void setLeftListener(OnClickListener onClickListener) { img_left.setOnClickListener(onClickListener); } //对右边按钮设置事件的方法 private void setRightListener(OnClickListener onClickListener) { img_right.setOnClickListener(onClickListener); }
5. 在布局当中引用该控件
到这里基本的功能已经有了。除了这些基础功能外,我们还可以做一些功能扩展,比如可以在布局时设置我的View显示的元素,因为可能有些需求并不需要右边的按钮。这时候就需要用到自定义属性来解决了。
前面已经简单介绍过自定义属性的相关知识,我们之间看代码
1.首先在values目录下创建attrs.xml
内容如下:
这里我们定义了三个属性,文字内容、颜色以及要显示的元素。
2.在java代码中进行设置
private void initAttrs(Context context, AttributeSet attrs) { TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeaderBar); //获取title_text属性 String title = mTypedArray.getString(R.styleable.HeaderBar_title_text); if (!TextUtils.isEmpty(title)) { text_center.setText(title); } //获取show_views属性,如果没有设置时默认为0x26 showView = mTypedArray.getInt(R.styleable.HeaderBar_show_views, 0x26); text_center.setTextColor(mTypedArray.getColor(R.styleable.HeaderBar_title_text_clolor, Color.WHITE)); mTypedArray.recycle(); showView(showView); } private void showView(int showView) { //将showView转换为二进制数,根据不同位置上的值设置对应View的显示或者隐藏。 Long data = Long.valueOf(Integer.toBinaryString(showView)); element = String.format("%06d", data); for (int i = 0; i