您的位置:首页 > 移动开发 > Android开发

Android笔记--LayoutInflator源码和使用分析

2017-09-12 17:59 381 查看

LayoutInflator源码分析

获取LayoutInflator对象

获取LayoutInflator的方式有两种:

使用LayoutInflator.from(Context context)可以获取到LayoutInflator对象。

(LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)

第一种本质也是调用第二种方式。

从本质上看context无论是以何种形式传入,最终都是利用binder获取的远程Service的能力

进行XML文件的填装工作。

LayoutInflator方法

LyaoutInflator含有的方法如下:



其中核心功能地一个系列就是inflate方法和rInflate方法

inflate方法 | 作用是把XML文件以View形式实例化到内存中

rInflate方法 | 作用是递归调用把XML中相应的嵌套布局结构也实例化出来并添加到XML的根布局中

inflate方法

inflate系列方法最终调用的都是:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)

方法

但是实际上最常用的是:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)

因为XmlPullParser对象并不是普通的XML的来的。而是在编译器XML预编译出来的文件得到的。所以普通的XML在这里转换成

XmlPullParser对象并不允许。所以必须借助Resource得到布局id进行相关操作。

参数代表的含义:

int resource | XML布局资源的id.(e.g. R.layout.main_activity)

ViewGroup root | 父root对象,若attachTRoot==true, 则把resource代表的布局对象填充到root.

若attachToRoot == false,则resource代表的布局只是利用root的LayoutParams即布局参数,但是不往root里添加。

attachToRoot | true 则返回父布局root, false 则返回XML对应得视图对象(源码中为temp对象)

inflate方法的调用流程如下:



下面用代码做一些试验:

这个是activity_main.xml:

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/mainView"
tools:context="com.example.myapplication.MainActivity"
android:orientation="vertical">

</LinearLayout>

这个是一个textview 在layout文件夹命名为pink.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/colorAccent"
android:text="red">

</TextView>

MainActivity.java

public class MainActivity extends AppCompatActivity {

private TextView view;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

ViewGroup parent = (ViewGroup) findViewById(R.id.mainView);

// result: layout_height=wrap_content layout_width=match_parent
view = (TextView) LayoutInflater.from(this).inflate(R.layout.pink, null);

parent.addView(view);

// result: layout_height=100 layout_width=100
view = (TextView) LayoutInflater.from(this).inflate(R.layout.pink, null);

parent.addView(view, 100, 100);

// result: layout_height=25dp layout_width=25dp
// view=textView due to attachRoot=false
view = (TextView) LayoutInflater.from(this).inflate(R.layout.pink, parent, false);

parent.addView(view);

// result: layout_height=25dp layout_width=25dp
// parent.addView not necessary as this is already done by attachRoot=true
// view=root due to parent supplied as hierarchy root and attachRoot=true

ViewGroup p = LayoutInflater.from(this).inflate(R.layout.pink, parent, true);

}
}

在开发者眼里这个inflate有时候显得很诡异。下面就意义分析一下上面的结果的原因:

inflate(R.layout.pink, null) 父布局addView(view)

// result: layout_height=wrap_content layout_width=match_parent
view = (TextView) LayoutInflater.from(this).inflate(R.layout.pink, null);

parent.addView(view);

这里的结果是宽match_parent 高wrap_content

刚开始肯定心里满是WTF,因为这里根本没有给pink布局文件指定一个父root.那pink在父布局中是怎么判定自己的布局参数的呢?

其实秘密就在addView方法中

/**

*

Adds a child view. If no layout parameters are already set on the child, the

* default parameters for this ViewGroup are set on the child.

Note: do not invoke this method from

* {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},

* {@link #dispatchDraw(android.graphics.Canvas)} or any related method.

@param child the child view to add

@see #generateDefaultLayoutParams()

*/

public void addView(View child) {

addView(child, -1);

}

/**
* Adds a child view. If no layout parameters are already set on the child, the
* default parameters for this ViewGroup are set on the child.
*
* <p><strong>Note:</strong> do not invoke this method from
* {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
* {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
*
* @param child the child view to add
* @param index the position at which to add the child
*/
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();//这里是关键,不通的ViewGroup会重新此方法
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}

@Override//此处我们用的LinearLayout 这里是他的重新实现
protected LayoutParams generateDefaultLayoutParams() {
if (mOrientation == HORIZONTAL) {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
} else if (mOrientation == VERTICAL) {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
return null;
}

通过源码可以看出addView对于要添加的View的默认布局参数是依据不同的ViewGroup来的。ViewGroup默认宽高都是wrap_content

。而LinearLayout对子View要求的默认布局参数是根据布局方向定的:

水平布局 | 宽高均为wrap_content

垂直布局 | 宽是match_parent 高是warp_content

所以就可以解释为什么在指定root为null的时候addView得到的是宽是match_parent 高是warp_content了。因为我么用了垂直布局

inflate(R.layout.pink, null) 父布局parent.addView(view, 100, 100)

这里依旧没有为pink指定其在父布局中的布局参数。但是父布局调用了parent.addView(view, 100, 100)。

/**
* Adds a child view with this ViewGroup's default layout parameters and the
* specified width and height.
*
* <p><strong>Note:</strong> do not invoke this method from
* {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)},
* {@link #dispatchDraw(android.graphics.Canvas)} or any related method.</p>
*
* @param child the child view to add
*/
public void addView(View child, int width, int height) {
final LayoutParams params = generateDefaultLayoutParams();
params.width = width;
params.height = height;
addView(child, -1, params);
}

源码也很明确,虽然布局参数依旧是父布局默认的。但是为布局参数重新指定了宽高,覆盖了默认值。所以这样指定可以改掉

pink在父布局中的布局参数。

inflate(R.layout.pink, parent, false)

这里为pink指定了其在父布局中应当使用的布局参数是parent的布局参数。但是并不把pink布局加载到parent视图中。

返回的依旧是pink对应的布局。

这个过程其实就是上面的流程图中的过程。

这里不再分析。

inflate(R.layout.pink, parent, true)

这里同样是上述的流程图过程。pink的布局参数用parent的布局参数。并且把pink布局视图放到parent中。返回的是

parent。

这里需要注意,因为这里是传入的parent是mian_view对应的视图,所以返回的也是parent.但是如果这里传入的是

另外的ViewGroup,那么返回的就是那个ViewGroup了。

最具有迷惑性的属性是root 和attachToRoot

root相对来好解释一点:

root就仅仅是为xml文件提供一个布局参数LayoutParams。用于限定大小和位置。不论attachToRoot true或者false

这个布局参数都会实实在在的应用在xml对应的视图对象上。


attachToRoot | true xml布局对应的对象利用root的布局参数限定来填充到root对象中。返回的是root对象。

attachToRoot | false xml布局对应的对象利用root的布局参数但是返回的是xml对应对象的根布局对象。要想把返回的View

加到父布局中就要用其他的办法。

但是什么时候用true 什么时候用false呢?

用true的场景:

假如父布局是一个LinearLayout,在父布局上加一个button,那么直接用true即可

下面这两种方式是等价的

inflater.inflate(R.layout.custom_button, mLinearLayout, true);

inflater.inflate(R.layout.custom_button, mLinearLayout);

在root不空时,attachToRoot是true

自定义View

自定义View初始化的时候,目的就是为了给自定义的View加一个自己的布局上去。

private void init() {

LayoutInflater inflater = LayoutInflater.from(getContext());

inflater.inflate(R.layout.view_with_merge_tag, this);

}

总结: 总结一下就是,在我们只是需要往root上加布局对象。而不需要的到返回的XML布局对象时。使用true即可

需要false的场景:

比如有个button的布局文件。想在加到父布局之前做一些定制。那么这是肯定需要先拿到符合父布局布局参数的button

的。做完定制以后再添加到父布局中

Button button = (Button) inflater.inflate(R.layout.custom_button, mLinearLayout, false);

。。。。。

mLinearLayout.addView(button);

RecyclerView 子类中onCreateViewHolder方法中需要使用false

public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

LayoutInflater inflater = LayoutInflater.from(getActivity());

View view = inflater.inflate(android.R.layout.list_item_recyclerView, parent, false);

return new ViewHolder(view);

}

因为RecyclerView负责呈现和填充XML布局的时机

Fragment 中onCreateView方法要使用false

FragmentManager fragmentManager = getSupportFragmentManager();

Fragment fragment = fragmentManager.findFragmentById(R.id.root_viewGroup);

if (fragment == null) {

fragment = new MainFragment();

fragmentManager.beginTransaction()

.add(R.id.root_viewGroup, fragment)

.commit();

}

//这里的root_viewGroup就是fragment中onCreateView方法第二个参数的视图对象。

//而fragment_layout填充附加到parentViewGroup的过程是FragmentManager 来做的

//所以你一定不能为同一个parentViewGroup添加两次,所以下面要传false

public View onCreateView(LayoutInflater inflater, ViewGroup parentViewGroup, Bundle savedInstanceState) {

View view = inflater.inflate(R.layout.fragment_layout, parentViewGroup, false);



return view;

}

当然root参数也可以传null。但是可能会带来不符合预期的结果。因为root为null的情况要有ViewGroup及其子类自己决定。

而你在XML布局中指定的参数就会失效。所以为了保证XML中的布局参数就是我们想要XML展示的。那就需要传入root.

例外情况:AlertDialog。inflater.inflate(R.layout.custom_alert_dialog, null);这是合理的。

因为AlertDialog会为所有的布局都采取match_parent

总结

为了保证结果是符合预期的。需要尽量按照下面的方案来操作:

只要有父布局,就一定穿进去父布局当root.

避免给root传null因为不同的viewGroup实现的generateDefaultLayoutParams不同

只要我们不负责把xml附加到root上,attachToRoot 参数传false.

同一个XML不要为同一个ViewGroup传两次true,否则報异常

自定义View attachToRoot传true

参考:

https://www.bignerdranch.com/blog/understanding-androids-layoutinflater-inflate/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: