Android学习系列
Android之Room学习
Android之自定义View学习(一)
Android之自定义View学习(二)
目录
Android学习系列
Android之Room学习
Android之自定义View学习(一)
Android之自定义View学习(二)
Android之自定义View学习(二)
前言
2. 自定义View初体验
2.1 View类简介
2.2 自定义View构造函数
2.3 绘制自定义View
2.3.1 测量View
2.3.1.1 MeasureSpec
2.3.1.2 DecorView
2.3.1.3 onMeasure
2.3.2 布局View
2.3.3 绘制View
2.3.4 绘制自定义View总结
其他学习分享系列
数据结构与算法系列
数据结构与算法之哈希表
数据结构与算法之跳跃表
数据结构与算法之字典树
数据结构与算法之2-3树
数据结构与算法之平衡二叉树
数据结构与算法之十大经典排序
数据结构与算法之二分查找三模板
Android之自定义View学习(二)
前言
在上一节当中,博主介绍了布局加载的流程以及布局加载的源码,今天主要介绍一下View的工作原理和源码。
2. 自定义View初体验
2.1 View类简介
- 直观上:视图上的各种控件包括布局都是一种View
- 代码上:View类是Android所有组件控件间接或者直接的父类
借用红黑联盟网站上的一张图,下图为View的继承关系图,红色控件为常用控件
自定义View,顾名思义,就是写自己所需要的控件。下面开始View第一步。
2.2 自定义View构造函数
观看控件的源码,控件都是或直接或间接的继承了View类进行操作。因此第一步继承View类,并继承四种构造函数如下。
public class MyTestView extends View {
public static String TAG = "View";
//第一类,MyTestView在代码中创建
public MyTestView(Context context) {
super(context);
}
//第二类,MyTestView在.xml的布局文件中创建
//自定义属性从AttributeSet传入
public MyTestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
//第三类,MyTestView有自定义style属性时调用
public MyTestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//第四类,MyTestView设置自定义style resource文件时调用
public MyTestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
2.3 绘制自定义View
绘制自定义View主要用到以下函数
public class MyTestView extends View {
......
@Override
//用于被内部调用测量视图大小
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
//用于被内部调用安排视图位置
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
@Override
//用于被内部调用绘制视图样式
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
2.3.1 测量View
View系统的绘制流程会从ViewRoot
(源码位置是ViewRootImpl.java)的performTraversals()
方法中开始调用performMeasure()
,进而调用View的measure()
方法。
然后onMeasure
方法在源码中被measure
方法调用,用来测量自定义View的大小,并且在measure
方法注解中提及,想要自定义View必须要重写onMeasure
方法。
同时,onMeasure
方法的参数widthMeasureSpec
、heightMeasureSpec
也有着重要意义,这两个参数是从MeasureSpec
得到。
2.3.1.1 MeasureSpec
MeasureSpec
封装了父类给子类的布局要求,widthMeasureSpec
、heightMeasureSpec
即对宽度、高度的要求。
MeasureSpec
实质上是一个32位int值,由测量模式SpecMode
和测量模式下的大小值SpecSize
组成,高两位是测量模式,有三种模式,低30位是测量模式下的大小值。
UNSPECIFIED
父类不指定尺寸也不限制尺寸,随便继承的子类如何定义尺寸。
EXACTLY
父类指定子类具体尺寸
AT_MOST
父类指定子类的最大尺寸
定义如下:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
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;
......
}
2.3.1.2 DecorView
正常情况下,我们直接按照MeasureSpec来进行指定即可。对于一般View,确实好像没有问题,但是仔细思考一下,那么XML中指定属性match_parent
、wrap_content
从哪儿得到的?LinearLayout
是个ViewGroup
,ViewGroup
继承于View
,当Activity
启动时,最开始的根视图是谁呢,这个根视图又是如何获得宽高的呢?
这时候就要引入LayoutParams
和DecorView
的概念。
首先LayoutParams比较简单,就是布局所需要的宽高设置。
ViewGroup.java
public static class LayoutParams {
......
@SuppressWarnings({"UnusedDeclaration"})
@Deprecated
public static final int FILL_PARENT = -1;
public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;
......
}
因此对于一般View,它的宽高是由父容器的MeasureSpec
和自身 LayoutParams
一起决定的。关于XML中标签指定宽高的问题已经解决了。
那么进入第二个问题,根视图是谁的呢?它是如何获取宽高呢?
最顶层也就是最外层的根视图我们称之为DecorView.
如下图,Activity
是一个PhoneWindow实例,其布局形式即为DecorView
,DecorView
是一个FrameLayout
布局,有标题栏(ActionBar)和内容视图(ContentView, 也就是每个活动我们所调用的函数SetContentView
)
那么现在来看,DecorView
又是如何获得宽高的呢?
在ViewRootImpl
的measureHierarchy
中有着如下一段代码,就是DecorView
的MeasureSpec
的赋值。
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
再来看下getRootMeasureSpec
的实现:
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;
}
可以看出,三种模式,且都和windowSize
有关,即可以理解为根视图/最外层视图的宽高是由窗口尺寸和自身LayoutParams决定的。
我们可以得出结论:
- 对于一般视图View,它的宽高是由父容器的
MeasureSpec
和自身LayoutParams
一起决定的。 - 对于根视图DecorView,它的宽高是由窗口尺寸和自身
LayoutParams
一起决定的。
2.3.1.3 onMeasure
根据getDefaultSize
函数来给定高度或者宽度的大小,然后使用setMeasuredDimension
函数来指定自定义View的(尺寸)高度、宽度
getSuggestedMinimumWidth
用来获取内容或者背景尺寸二者中的较大值。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
//父类子类不同种View 则调整宽高
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
//宽高是否发生了变化
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
//是否是EXACT模式
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
//匹配
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
//如果当前View与父类View不是同种View
if (optical != isLayoutModeOptical(mParent)) {
//不同就要调整测量值大小
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
//直接赋值
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
//打上标识,已经测量该View的大小
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
//获取默认大小
//根据三种模式调整再次调整大小
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());
}
上述过程就是一个单一View的测量过程,当然对于ViewGroup来说其包含多个子View,因此在ViewGroup的源码中有measureChildren
来测量子View的尺寸。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
//子View的测量过程
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
//getChildMeasureSpec方法,大致过程也是设置size和mode
//其中与LayoutParams.MATCH_PARENT以及 LayoutParams.WRAP_CONTENT进行匹配判断
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
2.3.2 布局View
测量好View的大小之后,performTraversals
继续会调用performLayout
方法,进而调用View
的layout
方法,然后onLayout
方法在源码中被layout
方法调用,用来在视图中给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) {
//如果发生了变化,确定View在布局中的位置
onLayout(changed, l, t, r, b);
......
}
......
}
setOptionalFrame
和setFrame
函数的主要目的就是给View确定位置并判断位置是否变化,setOptionalFrame
内核也是调用setFrame
函数,因此直接分析setFrame
源码。
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (DBG) {
Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}
//判断位置是否变化,变化则需要重新绘制
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
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 our old position
invalidate(sizeChanged);
//存储新的位置并设置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
......
}
return changed;
}
对于我们来说,我们仅需在代码中重写onLayout()函数,如下。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
接着,我们追溯到View的onLayout源码,发现…
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
???小朋友你是否有很多问号,突然想了想,好像确实应该为空,因为自定义View的位置本该就由其父布局决定,即父布局(一般继承ViewGroup)决定其子View布局(一般继承View类)。那就到ViewGroup里一探究竟吧。
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
然而貌似问号越来越多,ViewGroup
的layout最关键的部分还是调用其父类View
的layout
函数(是的,ViewGroup
的父类也是View
)来确定自身位置。而ViewGroup
的onLayout
函数却是一个抽象方法??
仔细考虑一下,ViewGroup
的onLayout()
函数是抽象方法是正确的,因为每个ViewGroup
都有着自己的独特布局,如LinearLayout
、RelativeLayout
等等对于子View
的布局规则是不同的,所以写成抽象方法后方便后来继承者自定义自己的布局规则。
接下来看看LinearLayout
的onLayout
实现
LinearLayout
有两种布局形式,以其中一种作为例子分析。
为了好理解布局中一些属性(如margin、padding等),从浏览器中抠出一张图仅供参考。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);//纵向线性布局
} else {
layoutHorizontal(l, t, r, b);//横向线性布局
}
}
/**
* Position the children during a layout pass if the orientation of this
* LinearLayout is set to {@link #VERTICAL}.
*
* @see #getOrientation()
* @see #setOrientation(int)
* @see #onLayout(boolean, int, int, int, int)
* @param left
* @param top
* @param right
* @param bottom
*/
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;//左内填充
int childTop;//上位置
int childLeft;//左位置
// Where right end of child should go
final int width = right - left;//宽度
int childRight = width - mPaddingRight;//去掉内填充的右位置
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;//内容部分要用宽度去除内填充的长度
//获得子View数量 getVirtualChildCount()调用的就是getChildCount()
final int count = getVirtualChildCount();
//对齐方式,通过改变子ViewTop值
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
//循环遍历子View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);//子View的Top是基于上一个的,nullChild的值为0
} else if (child.getVisibility() != GONE) {//这就解释了为什么GONE状态不会占用布局内容
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
//确定子View位置,并判断是否变化
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);//下一个子View的Top位置
i += getChildrenSkipCount(child, i);
}
}
}
最后做一个小小的实践。
布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<com.example.myviewlearning.TestLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"/>
</com.example.myviewlearning.TestLayout>
TestLayout.java如下:
public class TestLayout extends ViewGroup {
public TestLayout(Context context) {
super(context);
}
public TestLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TestLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public TestLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
//上述四类和继承View的自定义View相同
//注意一个顺序,在Measure测量以前,getMeasureWidth和getMeasureHeight两个函数返回值为0
//这里只给了一个子View
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
//循环遍历子View
for(int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);//测量子View
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth() + 100, childView.getMeasuredHeight() + 100);//给子View安排位置
Log.d("View Size", "Width: "+ Integer.toString(childView.getWidth()) + " height:" + Integer.toString(childView.getHeight()));
Log.d("View MeasuredSize", "MeasureWidth: "+ Integer.toString(childView.getMeasuredWidth()) + " height:" + Integer.toString(childView.getMeasuredHeight()));
}
}
}
当onLayout
运行结束后,我们可以通过getWidth
、getHeight
方法来获取子View的高和宽。
注意:
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());//给子View安排位置
一般来说childView.getWidth()
与 childView.getMeasuredWidth()
会相同,原因是上述给子View布局的这句代码。
getWidth() = childView.getMeasuredWidth() - 0(就是 right - left);
但是实际上两者意义是不同的。
childView.getWidth()
主要是用来表示当前childView
在该布局中的宽度,只有在layout
过程过后才有值。
childView.getMeasuredWidth()
主要是用来测量视图本身的大小,在measure
之后即可获取到值。
2.3.3 绘制View
测量好View(measure),给View布局好位置(layout),ViewRoot
中会继续执行调用performDraw
。
private void performDraw() {
......
try {
boolean canUseAsync = draw(fullRedrawNeeded);
......
}
......
}
performDraw
调用自身的draw
方法,在drawSoftware
中创建出一个Canvas
对象进行一些基本绘制(如背景颜色)并且真正的调用View类中的draw
方法,传入创建的Canvas
对象。
ViewRootImpl.java
private boolean draw(boolean fullRedrawNeeded) {
.......
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
}
......
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
// Draw with software renderer.
final Canvas canvas;
......
try {
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(mTag, "Surface " + surface + " drawing to bitmap w="
+ canvas.getWidth() + ", h=" + canvas.getHeight());
//canvas.drawARGB(255, 255, 0, 0);
}
if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
......
try {
canvas.translate(-xoff, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
attachInfo.mSetIgnoreDirtyState = false;
mView.draw(canvas);
drawAccessibilityFocusedDrawableIfNeeded(canvas);
} finally {
if (!attachInfo.mSetIgnoreDirtyState) {
// Only clear the flag if it was not set during the mView.draw() call
attachInfo.mIgnoreDirtyState = false;
}
}
} finally {
......
}
return true;
}
然后调用View的draw()
方法来执行具体的开始绘制(draw)View,最后onDraw
方法被View类中的draw
方法调用进行内容绘制,内容绘制也是最关键的一步。
绘制过程主要分六步骤,其中第二、第五步骤相对使用较少。
View.java
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
* 1. 绘制背景
* 2. 保存当前canvas,非必须
* 3. 绘制View的内容
* 4. 绘制子View
* 5. 绘制边缘、阴影等效果,非必须
* 6. 绘制装饰,如滚动条等
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
.......
// we're done...
return;
}
......
}
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
......
}
void setBackgroundBounds() {
if (mBackgroundSizeChanged && mBackground != null) {
mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
}
从代码中看出,第一步,背景的绘制实际上调用了一个Drawable对象mBackground
进行背景绘制,然后根据layout过程确定的视图位置(mLeft mRight mTop mBottom
)来设置背景的绘制区域,之后再调用onDraw
方法来完成背景的绘制工作。
而这个mBackground
对象其实就是在XML中通过android:background
属性设置的图片或颜色。当然也可以在代码中通过setBackgroundColor()
、setBackgroundResource()
等方法进行赋值。
跳过第二步,来到第三步骤,调用onDraw
方法对View内容进行绘制,但是有了onLayout
经验,这里onDraw
方法同样需要被重写。
第四步,进行子View绘制处理,当然对于View来说,dispatchDraw
是空方法,因为没有子View,但是对于ViewGroup来说,dispatchDraw
还是比较复杂的。
跳过第五步,来到最后一步,对视图的滚动条进行装饰,从这里其实就可以看出,其实所有的控件都是有着自己的滚动条的,只不过被隐藏了起来。
最后同样来个小实践。
布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/main_layout"
tools:context=".MainActivity">
<com.example.myviewlearning.MyTestView
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="#000000"/>
</LinearLayout>
onDraw()
重写如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width, height)/2;
//画圆
canvas.drawCircle(paddingLeft + width/2, paddingTop + height/2, radius, mPaint);
//设置text和textColor
mPaint.setTextSize(88);
String text = "Hi, my view";
mPaint.setColor(Color.WHITE);
canvas.drawText(text,width/3, height/2, mPaint);
}
2.3.4 绘制自定义View总结
到此为止,我们就明白了,要想能够自定义一个View,少不了这三个步骤。
测量------>布局------>绘制
其他学习分享系列
数据结构与算法系列
数据结构与算法之哈希表
数据结构与算法之跳跃表
数据结构与算法之字典树
数据结构与算法之2-3树
数据结构与算法之平衡二叉树
数据结构与算法之十大经典排序
数据结构与算法之二分查找三模板
如有兴趣可以关注我的微信公众号,每周带你学一点算法与数据结构。