本文基于Android API 28
绘制三大方法
onMeasure
onMeasure(int, int)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在onMeasure方法用来测量view和他的内容,来决定view的宽高,该方法被measure(int, int)调用,并且需要被子类重写去测量view的内容的精确、有效的宽高;
重写该方法时,必须调用setMeasureDimension(int, int)方法去保存该view测量的宽和高;如果不这样做,measure(int, int)方法会抛出IllegalStateException异常,调用父类的onMeasure(int, int)方法可以避免这样;
基类实现了测量默认的背景尺寸,除非MeasureSpec允许更大的尺寸,子类应该重写onMeasure(int, int)方法去提供更好的测量他们的内容;
如果重写该方法,子类有责任确保实测宽高至少是view的最小宽高;
该方法在View类里面直接调用setMeasureDimension方法去保存默认的view的尺寸,参数直接调用getDefaultSize方法获取默认宽高;
getDefaultSize(int, int);
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;
}
这个方法返回默认的尺寸,如果MeasureSpec没有添加约束,则使用提供的尺寸,如果MeasureSpec允许,则将尺寸放大;即当MeasureSpec的Mode为UNSPECIFIED时,没有约束,使用默认的大小尺寸,为AT_MOST和EXACTLY时,需要计算出尺寸的大小;
getSuggestedMinimumHeight() /Width()
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
这两个方法返回view建议的最小宽高以及背景的最小宽高的最大值;调用这两个方法时,需要注意返回的宽高应该在父View的要求范围内;
setMeasuredDimension(int, int)
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
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);
}
该方法必须由onMeasure(int, int)方法调用来保存实测的view宽高,否则将抛出异常;这个方法里面涉及到要判断View及其父View是否为ViewGroup及是否有光学效果,来决定是否要处理关于这些效果产生的尺寸影响;
isLayoutModeOptical(Object)
public static boolean isLayoutModeOptical(Object o) {
return o instanceof ViewGroup && ((ViewGroup) o).isLayoutModeOptical();
}
该方法返回该View是否为ViewGroup,并且是否由光效效果,例如阴影、亮光等,这些也将影响View的尺寸测量;
getOpticalInsets()
public Insets getOpticalInsets() {
if (mLayoutInsets == null) {
mLayoutInsets = computeOpticalInsets();
}
return mLayoutInsets;
}
该方法返回当前View的插图属性,如果为空,则去计算;
setOpticalInsets(Insets)
public void setOpticalInsets(Insets insets) {
mLayoutInsets = insets;
}
为该View设置Insets,该方法不请求layout,如果手动调用该方法设置了Insets,则需要调用requestLayout()方法;
computeOpticalInsets()
Insets computeOpticalInsets() {
return (mBackground == null) ? Insets.NONE : mBackground.getOpticalInsets();
}
如果当前View没有背景图片,则返回Insets.NONE,否则去计算背景图片的光学效果范围;
setMeasuredDimensionRaw(int, int)
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
该方法中最终保存View的尺寸,并设置标志位为PFLAG_MEASURED_DIMENSION_SET;
MeasureSpec
MeasureSpec类封装了从父View传递到子View的布局要求;包含size和mode;
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
三种Mode
·UNSPECIFIED(00….)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
父布局对子View没有任何限制,子View可以是任何大小;
·EXACTLY(01….)
public static final int EXACTLY = 1 << MODE_SHIFT;
父布局给了子View一个准确的大小约束,不管子View想要多大,都会受到该限制;
·AT_MOST(10….)
public static final int AT_MOST = 2 << MODE_SHIFT;
子View可是达到他想要的任何指定大小;
makeMeasureSpec(int, int)
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);
}
}
该方法提供了Size和Mode的算法;
getMode(int)、getSize(int)
这两个方法分别获取View的MeasureSpec的Mode和Size;
onLayout
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
该方法是一个空实现,需要子View自己去实现是否需要为他们的子View重新计算大小和位置;
onDraw
protected void onDraw(Canvas canvas) {
}
该方法也是一个空实现,子View自己定义绘制内容;
View创建
加载xml布局
LayoutInflater.from(context).inflate(R.layout.activity_base, null);
以上代码用来加载定义好的xml文件,加载布局;
LayoutInflater#from(context)
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
在代码中,通过Context的getSystemService方法去获取到LayoutInflater的实例;
LayoutInflater#inflate()
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
这里inflate通过内容调用到另一个重载的inflate方法,这里获取到了Resource资源,并且获取到XmlResourceParser对象实例,用来解析Xml布局,然后最终调用到了inflate(XmlPullParser, ViewGroup, boolean)方法;
LayoutInflater#inflate(XmlPullParser, ViewGroup, boolean)
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
... ...
final String name = parser.getName();
... ...
if (TAG_MERGE.equals(name)) {
... ...
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
... ....
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
... ...
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
... ...
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
... ...
return result;
}
}
这里省略掉部分代码,使用Xml解析器去解析Xml,获取Xml标签,先忽略Xml开始和结束等标签,直接看创建View的部分,这里判断是否为merge标签,进入else分支,这里的核心代码是调用createViewFromTag(root, name, inflaterContext, attrs)方法去创建View,后面是判断是否有父布局,获取父布局参数等信息,将创建好的View添加到父布局;
LayoutInflater#createViewFromTag(View, String, Context, AttributeSet, boolean)
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
... ...
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
... ...
}
在该方法中,首先先判断了三个factory,这两个factory在通过LayoutInflater.from().inflate()方法在加载布局的时候都为空,这个Factory是在创建View的时候可以去手动设置View的创建规则,实现自己定义View的创建,例如在AppComAppCompatActivity中就会初始化一个Factory对象去创建属于AppCompat风格的View,下面分析;
所以方法进入到下面if处,这里判断name中是否包含“.”,即是在判断是否为自定义View,其实两个方法最终调用的都是同一个方法,只是在系统的View时,会为其加上android.view的包名;
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
LayoutInflater#creatView(String, String, AttrbuteSet)
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
... ...
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
... ...
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
... ...
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
final View view = constructor.newInstance(args);
... ...
return view;
... ...
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
这是是通过反射拿到View类,通过反射去调用对应的View的构造方法去创建VIew,所以这里明白了通过Xml创建布局的时候系统是通过反射来创建的;走到这里就可以回到View的构造方法了;
通过setContentView创建
在Activity的onCreate()方法中,通过setContentView()方法将布局加载进去,Activity继承自AppCompatActivity;
AppCompatActivity#setContentView()
public void setContentView(@LayoutRes int layoutResID) {
this.getDelegate().setContentView(layoutResID);
}
这里调用geDelegate()的setContentView方法;
public AppCompatDelegate getDelegate() {
if (this.mDelegate == null) {
this.mDelegate = AppCompatDelegate.create(this, this);
}
return this.mDelegate;
}
AppCompatDelegate类中的getDelegate()方法返回一个AppCompatDelegate的实现类AppCompatDelegateImpl,所以方法的具体实现由AppCompatDelegateImpl类完成;
AppCompatDelegateImpl#setContentView()
public void setContentView(int resId) {
this.ensureSubDecor();
ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290);
contentParent.removeAllViews();
LayoutInflater.from(this.mContext).inflate(resId, contentParent);
this.mOriginalWindowCallback.onContentChanged();
}
到这里又看到了LayoutInflater的inflate()方法,流程又回到了步骤2.1中;不过这里需要注意的是,在Xml创建的流程中,在createViewFromTag()方法中有一个对factory的判断,在Xml创建时为空,但是在这里就不为空了,这里在AppCompatActivity中对其做了处理,在Factory中去创建自己风格的View,因为AppCompatActivity和之前的Activity的风格是略有不一样;
Activity继承自AppCompatActivity,看onCreate()方法;
AppCompatActivity#onCreate()
protected void onCreate(@Nullable Bundle savedInstanceState) {
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
... ...
super.onCreate(savedInstanceState);
}
这里初始化了一个Factory对象,具体实现还是在AppCompatDelegateImpl中;
AppCompatDelegateImpl#installViewFactory()
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i("AppCompatDelegate", "The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's");
}
}
在这里为LayoutInflater设置了一个Factory2对象;在创建的View的时候,判断factory不为空,就通过factory去创建View,最终如果是自定义View,则通过反射去创建View,如果是系统自身的View,则对应到AppCompatViewInflater的createView()方法中,匹配要创建的View,分别有每个View对应的实现类;
通过代码new
通过代码new的话,直接去调用对应的View的构造方法;
文章已同步至GitPress博客:https://gitpress.io/@yangshijie/