My Blog | My Blog |
---|---|
Github | Github |
CSDN | CSDN |
zhihu | zhihu |
Android 自定义 View 学习
Android 自定义 View 之 onMeasure() 源码分析
Android 自定义 View 之 onLayout() 源码分析
Android 自定义 View 之对 TouchEvent 的处理
如果觉得我的文章还行的话,也可以关注我的公众号,里面也会第一时间更新,并且会有更多的关于技术的最新资讯和一些个人感想。
扫码关注
之前在学习自定义 View 时就想到通过写一个综合性高的作品来实战,所以开始写酷云,果然在这过程中发现了自身很多的不足,在之前的 Android 实战之酷云–>仿网易云音乐开发(一) 和 Android 实战之酷云–>仿网易云音乐开发(二) 两篇文章都讲到了一些问题,当然,稍后还有更多文章,当是总结。最近在利用 LayoutInflater 实现动态加载布局踩了一些坑,因为也是 View 方面的知识,便写进自定义 View 系列的文章吧!
LayoutInflater
获取 LayoutInflater 实例
说到加载布局,或许大家都会 Activity 中的 setContentView() 方法,因为一般加载布局的工作都是由这个方法来完成的,但或许大家不知道的是 setContentView() 方法加载布局的原理其实就是 LayoutInflater 来实现的,今天我们就来从原理上分析 LayoutInflater,将 LayoutInflater 看懂。
首先还是先看一看 Google 官方文档是怎么描述 LayoutInflater 的吧:
Instantiates a layout XML file into its corresponding View objects. It is never used directly. Instead, use getLayoutInflater() or getSystemService(Class) to retrieve a standard LayoutInflater instance that is already hooked up to the current context and correctly configured for the device you are running on.
从中可以看出,LayoutInflater 的作用是将 XML 布局文件实例化到其对应的View对象中。但是不能直接实例化,需要通过 getLayoutInflater() 方法或者 getSystemService(Class)方法来获取 LayoutInflater 实例。
继续深入看文档的方法,我们会发现有一个 from() 方法,文档对它的描述是:
Obtains the LayoutInflater from the given context.
也就是说可以通过此方法获取 LayoutInflater 实例,先不看这个方法,我们先看一下 getLayoutInflater() 方法。
Activity 的 getLayoutInflater() 方法是调用 PhoneWindow 的 getLayoutInflater() 方法来获取实例的,来看一下该方法的源代码:接着我们来看一下 getLayoutInflater() 方法的源码:
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
可以看到 getLayoutInflater() 方法间接调用了 from() 方法。那么接下来让我们看一下 from() 方法的源码:
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;
}
其实,from() 方法是 getSystemService(Class) 的简化版,Google 为我们进行了封装,方便我们的使用。
因此,以上三个方法的本质其实都是通过 getSystemService() 方法来获取实例,可以通过以上三个方法来获取 LayoutInflater 的实例。
下面是通过 getSystemService(Class) 方法获取 LayoutInflater 实例的示例代码:
LayoutInflater inflater = (LayoutInflater)context.getSystemService
(Context.LAYOUT_INFLATER_SERVICE);
使用 LayoutInflater.inflate() 方法加载布局
获取到 LayoutInflater 的实例后就可以调用 inflate() 方法来加载布局了, inflate(int resource, ViewGroup root) 方法接受两个参数,第一个参数是要加载布局的id,第二个参数是指给该布局的外部再嵌套一层父布局,如果不需要直接传null。这样就成功成功创建了一个布局的实例,之后再将它添加到指定的位置就可以显示出来了。
LayoutInflater 提供了四个 inflate() 方法,分别如下:
inflate(int resource, ViewGroup root)
inflate(XmlPullParser parser, ViewGroup root)
inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)
inflate(int resource, ViewGroup root, boolean attachToRoot)
所以,让我们来看一下 LayoutInflater.inflate(int resource, ViewGroup root) 的源码:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
注意 : 如果 root 不为 null,attachToRoot 为 true,否则 attachToRoot 为 false
我们发现 inflate(int resource, ViewGroup root) 方法里只有一行代码,是对 inflate(int resource, ViewGroup root, boolean attachToRoot) 方法的调用。所以我们继续深入分析,接着看 inflate(int resource, ViewGroup root, boolean attachToRoot) 的源码:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(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(int resource, ViewGroup root, boolean attachToRoot) 方法也并没有其他实现逻辑,而是获取 XmlResourceParser 实例后调用了 inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) 方法。看到这里你可能有点懵,没关系,下面这张图会有助于你的理解:
也就是说,真正处理加载逻辑的是 inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) 方法。
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 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("**************************");
}
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
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
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) {
// 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");
}
// 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.
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;
}
}
} 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;
}
}
对上面的代码简单的分析一下:
- 加载设置的 resource,临时标记为 temp
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
- 获取 ViewGroup.LayoutParams 的实例并获取其值
ViewGroup.LayoutParams params = null;
params = root.generateLayoutParams(attrs);
- 当root不为空,attachToRoot 为 false 时,为 temp 设置 layout 属性,当该 view 以后被添加到父 view 当中时,这些layout属性会自动生效
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) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
总结一下,当 root 和 attachToRoot 分别传入不同的值时会出现的情况:
如果 root 为 null,attachToRoot 将失去作用,设置任何值都没有意义。
如果 root 不为 null,attachToRoot 设为true,则会给加载的布局文件的指定一个父布局,即 root。
如果 root 不为 null,attachToRoot 设为 false,则会将布局文件最外层的所有 layout 属性进行设置,当该 view 被添加到父 view 当中时,这些 layout 属性会自动生效。
在不设置 attachToRoot 参数的情况下,如果 root 不为 null,attachToRoo t参数默认为 true。
调用 createViewFromTag() 方法,并把节点名和参数传入。从方法名可以看出,它是根据节点名来创建 View 对象的,在 createViewFromTag() 方法的内部会调用 createView() 方法,然后使用反射的方式创建出 View 的实例并返回。
然后在第 39 行调用了 rInflate() 方法来循环遍历这个根布局下的子元素,源码如下:
private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs)
throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
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)) {
parseRequestFocus(parser, parent);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(name, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs);
viewGroup.addView(view, params);
}
}
parent.onFinishInflate();
}
我们发现在第 21 行依旧是利用 createViewFromTag() 方法来创建 View 实例,然后在第 24 行递归调用 rInflate() 方法来遍历这个 View 下的子元素,每次递归完成后则将这个 View 添加到父布局当中。
Demo 演示
编写 MainActivity 的布局界面 activity_main.xml,只添加一个空 LinearLayout 布局,不添加任何控件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
</LinearLayout>
然后编写一个加载布局,命名为 add_view.xml ,只添加一个 TextView:
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/text">
</Button>
最后编写 MainActivity.java,利用 LayoutInflater 的 inflate() 方法将 add_view.xml 布局添加到 activity_main.xml 布局中:
public class MainActivity extends Activity {
private LinearLayout mainLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mainLayout = (LinearLayout) findViewById(R.id.main_layout);
LayoutInflater layoutInflater = LayoutInflater.from(this);
View textviewLayout = layoutInflater.inflate(R.layout.add_view, null);
mainLayout.addView(textviewLayout);
}
}
运行结果如下:
然后我们试着更改控件的大小,看一下有什么效果:
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="200dp"
android:layout_height="80dp"
android:text="@string/text">
</Button>
运行后发现大小居然没变,这是为什么呢?
这是因为 layout_width 和 layout_height 并不是用于设置 View 的大小的,而是用于设置 View 在布局中的大小的。也就是说 View 必须存在于一个布局中,此时设置的 layout_width 和 layout_height 才能起作用。因此,我们更改 add_view.xml 的布局代码,在 Button 外再嵌套一层布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="200dp"
android:layout_height="80dp"
android:text="@string/text" />
</LinearLayout>
运行效果如下:
好了,关于 LayoutInflater 的讲解就到这里吧,欢迎小伙伴们提出你宝贵的意见。