文章目录
Android学习系列
Android之Room学习
Android之自定义View学习(一)
Android之自定义View学习(二)
Android之自定义View学习(一)
前言
本文主要用于学习自定义View的相关使用和源码解析。
LayoutInflater解析
在学习如何实现自定义View之前,我们需要了解控件是布局如何实现,View加载到布局中以及布局视图本身的加载,这是了解自定义View第一步。
那么关于视图加载的一大利器,我们最不陌生的就是LayoutInflater
,下面就将介绍LayoutInflater
。
1. 声明定义视图加载器LayoutInflater
LayoutInflater layoutInflater = LayoutInflater.from(this);
以下语句是from
方法源码中声明定义LayoutIInflater
的方法,同样可以用来声明定义LayoutInflater
.
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Context.LAYOUT_INFLATER_SERVICE
是Context
的一种服务之一,用于检索当前上下文的视图加载器.
/**
* Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.LayoutInflater} for inflating layout resources in this
* context.
*
* @see #getSystemService(String)
* @see android.view.LayoutInflater
*/
public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater";
2. LayoutIInflater
加载布局文件
View view = layoutInflater.inflate(R.layout.XXX);
在源码中,inflate
方法被重载了多次,但是其他几类均会调用以下Inflate
方法.
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot);
2.1 XmlPullParser
第一个参数XmlPullParser
是要解析的xml布局文件
-
XML结构
主要是由 头部声明 + 诸多元素(Element)组成
元素可分为标签(tag , 分为头标签与尾标签)+内容/子元素
<?xml version="XXX" encoding="utf-8"?> <tag1> content1 </tag1> <tag2> <tag2.1> content2.1 </tag2.1> <tag2.2> content2.2 </tag2.2> <tag2.3> content2.3 </tag2.3> </tag2>
-
XML解析
一个元素(包括子元素)就是一个事件,遇到头标签开始解析事件
XMLPullPareser中有着以下几个事件标识
START DOCUMENT 文档开始解析 START_TAG 一个元素标签的开始事件,这时解析该元素的属性值 TEXT 解析该元素的内容事件,这时解析该元素的内容值 END_TAG 一个元素标签的结束事件 END_DOCUMENT 文档解析结束
函数 用法 next 解析下一个标签 nextTag 下一个标签 getEventType 获得事件类型 nextText 获得节点内具体内容 getName 获取节点名 -
Merge标签
2.2 root和attachToRoot
后两个参数,root为根容器组件, attachToRoot表示是否添加到root中.
3. 源码解析
3.1 inflate
源码
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
//Context
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
//保存根容器组件
View result = root;
try {
// Look for the root node.
//这一部分就是找寻第一个标签
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
//如果找到文档解析结束也没找到一个头标签,那么就直接抛出异常
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
//找到标签,获取第一个标签的名字
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
//是merge标签,则调用rInflate函数,将所有的merge标签下的元素加入到父根容器组件之下
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
//如若不是merge标签,则根据标签解析布局中"name"标签并返回一个相应的View变量
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;//ViewGroup的布局参数
//如果给定父布局,那么调用root的generateLayoutParams()并传入xml的解析属性, 生成布局参数
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
//并且attachToRoot为false意味着不添加到根容器组件,那么直接为View添加布局参数
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
//开始加载temp下所有子View
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
//根容器组件不为空,则直接调用根容器组件的addView()
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.
//不加载到根容器组件,则返回的结果为原先根据tag生成的View
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
流程如下:
- 先判断是否是merge标签,如果是,则直接函数
rInflate
将所有子View加载到root中 - 如若不是merge标签,则根据parser得到的"name"标签创建一个View变量(
createViewFromTag
) - 调用
rInflateChildren
将生成的View作为根容器,把新生成的View的子View加载到新生成的View中 - 如果给定root,那么
attachToRoot
默认为true,讲新创建的View变量加载到root中 attachToRoot
为false或者无root,则返回的结果为原先根据tag生成的View变量
inflate
方法主要涉及到的比较重要的标识TAG_MERGE
与函数rInflate
与函数createViewFromTag
.
createVIewFromTag
: 每一个我们所学习的控件(xml文件中的每个标签)都是一个View,该函数又有一个createView
函数返回一个View实例.
Merge标签
一般Merge标签是同include
标签一起使用.
如下是一个标题栏的布局"title"文件
<?xml version="1.0" encoding="utf-8"?>
<merge>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
android:background="@color/colorAccent"
android:orientation="horizontal">
<ImageButton
android:id="@+id/title_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@drawable/turn_left"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/title_go"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@drawable/turn_right"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="Title Text"
android:textAllCaps="false"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/title_go"
app:layout_constraintStart_toEndOf="@+id/title_back"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</merge>
在activity_main中引入include
标签如下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/title"/>
<Button
android:id="@+id/diy_dialog_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="DIY dialog"
android:textAllCaps="false"
android:textSize="20sp"
android:layout_marginTop="10dp"
android:background="@drawable/button_click"/>
</LinearLayout>
Merge标签能够减少层级,优化布局使用,通常与include
标签配合使用.
根据源码分析,也就知道优化布局的原因,直接将所有的merge标签下的元素加入到父根容器组件之下,不用自己产生View再进行诸多比较等.
3.2 rInflate源码
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
//获得parser的深度,为后续DFS做准备
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
//对每一个元素进行解析,最外层深度为0(如最初的头尾标签均是0)
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
//首先判断解析属性中foucusable为true的元素,获取View的焦点
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
//解析View的tag
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {//同时include标签不可以是根元素
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {//如果存在merge,必须是根元素,而merge标签在最初已经进行了处理
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);//根据Tag产生View
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//递归调用,解析view当中的子view
rInflateChildren(parser, view, attrs, true);
//解析后的view加入到父View中去
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
//rInfalteChildren内部也是递归调用rInflate,然后把子View加载到view中,即父容器组件就是view本身
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
对于XmlPullParser的高度:
- 最外层高度为0
- 遇到本层END_TAG之前遇到START_TAG则高度+1
- 遇到本层END_TAG之后,说明从深层跳到该层,高度-1
/**
* <pre>
* <!-- outside --> 0
* <root> 1
* sometext 1
* <foobar> 2
* </foobar> 2
* </root> 1
* <!-- outside --> 0
* </pre>
*/
源码对于getDepth()
的注解例子如上.
流程如下:
- 对每一个元素进行解析
- 如若有需要聚焦属性的元素,那么获取View的焦点
- 如若不是,排除
include&&merge
标签 - 2、3排除后则正常解析View的tag
- 然后使用
createViewFromTag
来产生View,并调用rInflate
函数进行子View加载到View中 - 最后结束递归,调用 终止加载
parent.onFinishInflate()
;
3.3 createViewFromTag
源码
以上流程将布局所有的View加载完成,而负责一个控件View生成的函数是createViewFromTag
.
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
//如果标签与主题相关,则需要将context与themeResId包裹成ContextThemeWrapper。
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
//BlinkLayout,闪烁布局,被包含的View,会有闪烁效果,如QQ消息
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
//以上部分与生成view不直接关联
//用户可以设置LayoutInflater的Factory来进行View的解析,但是默认情况下Factory均为空
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 {
//使用自定义View是需要在xml指定全路径的,而自定义View的包路径中一定带有着".",com.XXXX.XXXX.XXXX
//即可判断内置View还是自定义View
if (-1 == name.indexOf('.')) {
//内置View控件
view = onCreateView(parent, name, attrs);
} else {
//自定义View控件
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
单个View的解析过程如下:
- 判断是否是"view"标签,是的话获取其"class"
- 标签的属性是否是Theme Wrapper有关的,是的话,需要将此时的上下文与相应的主题打包成
ContextThemeWrapper
- LayoutInflator有着多种工厂可以在初始化的时候进行设置,但是默认状态时没有设置的.
- 不设置则开始判断是内置View还是自定义View,如果是内置View则调用
onCreateView
; 自定义View则调用createView
. - 最后返回View
view标签
<view
class="LinearLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
view标签需要指定class,用以表明是哪一种控件.
BlinkLayout
闪烁布局,与其他布局一样使用.
<blink
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="闪烁的按钮"/>
</blink>
3.4 createView
源码
onCreateView
和createView
的区别主要在内置View会加上"android.view."前缀
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
onCreateView
的实质还是调用了createView
方法,因此来看下createView
源码
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
//从cache中获得构造函数
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(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);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
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);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
Object lastContext = mConstructorArgs[0];
if (mConstructorArgs[0] == null) {
// Fill in the context if not already within inflation.
mConstructorArgs[0] = mContext;
}
Object[] args = mConstructorArgs;
args[1] = attrs;
//根据构造函数,构建View实例
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;
} catch (NoSuchMethodException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassCastException e) {
// If loaded class is not a View subclass
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
final InflateException ie = new InflateException(
attrs.getPositionDescription() + ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}