源码分析初级《篇一》 为何建议使用LayoutInflater.from而不用View.inflate

转载文章请注明出处:道龙的博客

我们不管是在ListView、RecyclerView、甚至自定义布局的时候,都会通过View.inflate(......);方法加载布局,其实这是偷懒的方式,有些时候,通过这种偷懒的方式反而带来意想不到的bug。比如空指针异常,非法状态异常。接下来就通过源码角度,分析为何不建议使用这种方式。

伪代码示例:

public class MyStaggedRecyclerAdapter extends RecyclerView.Adapter<MyStaggedRecyclerAdapter.MyViewHolder> {

    private List<String> list;
    private List<Integer> heights;

    public MyStaggedRecyclerAdapter(List<String> list) {
        // TODO Auto-generated constructor stub
        this.list = list;

        heights = new ArrayList<Integer>();
        for (int i = 0; i < list.size(); i++) {
            heights.add((int) (200 + Math.random() * 50));
        }

    }

    class MyViewHolder extends RecyclerView.ViewHolder {
        TextView tv;

        public MyViewHolder(View view) {
            super(view);
            tv = (TextView) view.findViewById(android.R.id.text1);

        }

    }

    @Override
    public int getItemCount() {
        // TODO Auto-generated method stub
        return list.size();
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        //绑定数据
        LayoutParams params = holder.tv.getLayoutParams();
        params.height = heights.get(position);
        holder.tv.setBackgroundColor(Color.rgb(100, (int) (Math.random() * 255), (int) (Math.random() * 255)));
        holder.tv.setLayoutParams(params);
        holder.tv.setText(list.get(position));

    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup viewGroup, int arg1) {
        // 创建ViewHolder
        MyViewHolder holder = new MyViewHolder(View.inflate(viewGroup.getContext(), android.R.layout.simle_list_item_1, null));
        //MyViewHolder holder = new MyViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(android.R.layout.simple_list_item_1, viewGroup, false));

        return holder;
    }
}

这里的代码非常简单,是为了给RecycleView设置流式布局的适配器类。流式布局需要给孩子设置宽高,这里通过动态随机给item孩子设置高度的方式,这样展示流式布局显得更加高大上一点~   首先,在onCreateViewHolder方法中,先通过以往偷懒的方式加载item孩子布局的。运行程序,我们会发现报错:

NullPointException

经过log日志,或者debug,锁定到LayoutParams为空,接下来设置宽高,也就没法执行了。看看为空情况,眼见为实:



这里就很头疼了,我按照标准写法写的,为什么为空呢?这里,其实存在着不小的坑。对于这个坑,需要通过源码角度理解。进入源码:

public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
    LayoutInflater factory = LayoutInflater.from(context);
    return factory.inflate(resource, root);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}
我们发现,通过调用View.inflate本质上调用的是LayoutInflater的inflate(resource, root, root != null);方法。由于传入的根布局root为nulll,因此这里本质上为inflate(resource, null, false);。继续跟进源码
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三个参数含义前两个很明显,第三个看参数名称也很明显,含义是:是否绑定传入的RootView?我们本质上传入的是false,所以这个值以后都为fallse了。方法里面是通过xml序列化器,对自定义传入的res布局文件预进行解析。具体的解析,继续跟进源码:
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;
    }
}
在这个方法里面,就是对传入的item布局进行全面的解析了。看一下红色位置标注处,params = root.generateLayoutParams(attrs);

哈哈,终于把你揪出来来了。我们传入的root为null,更不可能去生成该item的params了,因此,刚开始获取params为null很明显了了。

同样的我们加载的布局他压根就没有父亲,这样他的layout_width和layout_height等属性都需要有父亲才有效。没有params为null也就没有什么可以疑问的了。

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/text1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceListItemSmall"
    android:gravity="center_vertical"
    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
    android:minHeight="?android:attr/listPreferredItemHeightSmall" />

我们先不直接使用LayoutInflater.from,我们想到,既然root为空,那么我传入一个root让其不是空,不就解决了这个bug吗?做如下修改:

MyViewHolder holder = new MyViewHolder(View.inflate(viewGroup.getContext(), android.R.layout.simple_list_item_1, viewGroup));
还是报错:
java.lang.IllegalStateException:
The specified child already has a parent. You must call removeView() on the child's parent first.

错误情况也很明朗,因为,我们指定的孩子view已经有了父容器了(RecyclerView会默认成为父亲),我们传入的root不能直接成为父亲。你必须先让原来孩子的父亲把孩子去掉,才能再找一个父亲。这尼玛真实一言不合就换父亲啊~~

我们还是要看看为何报这个错误:

首先:由于我们传入了root,也就不为空,最初默认调用还是默认还是调用的:
inflate(resource, root, root != null);
inflate(resource, root, true);

接着定位最终调用源码绿色位置:

看源码就知道:多做了一个事情就是
 if (root != null && attachToRoot) {
            root.addView(temp, params);
        }

由于RecyclerView/ListView会自动将child添加到它里面去成为父亲,并最终一起添加到viewGroup(成为爷爷)。可以找出错误的原因有两个:1、给布局添加父亲,只能存在一个父亲。2、(这个解释有点牵强但是可以增加理解)孩子自己就想添加到viewGroup,想直接把爷爷当做父亲,这伦理上也说不过去啊,难免自作多情!这个时候,就会报非法异常了。

那么最后看解决办法:

解决办法1,还是使用View.inflate()

        MyViewHolder holder = new MyViewHolder(View.inflate(viewGroup.getContext(), R.layout.listview_item, null));

布局使用自己定义的布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <TextView
        android:gravity="center"
        android:textSize="20sp"
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>
这样的,我们给TextView制定了父布局,那么通过最初的方式就能完成任务。

解决方式2:

通过建议的方式,使用

        MyViewHolder holder = new MyViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.listview_item, viewGroup, false));
使用这种方式,无论加载什么布局,都能完成View的加载,当然,这也是本文所说的结论,如果想要加载布局,不想带来可能存在的“危机”,就使用这种方式加载布局吧!

其他解决方式:

可以通过ViewTreeObserver.addGlob..Listener()方法,设置页面布局绘制完毕监听器,在里面进行获取组件的宽高一定不会报错。至于使用方式,自定百度~


对于这一块的更具体、详细的源码,等笔者能力提高了,再进入两万行的代码里带大家遨游一番吧~


觉得有作用,就点个赞价加个关注呗~~






  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 这个方法是在Android开发中使用的,可以通过LayoutInflater类的from方法获取到一个LayoutInflater实例,然后调用其inflate方法来加载布局文件,将其转化为一个View对象,以供使用。 ### 回答2: layoutinflater.from(context).inflateAndroid中一个比较常见的方法,主要用于将布局文件转换成可在代码中使用View对象。以下是详细的解释: 首先,LayoutInflater是一个Android系统类,其作用是将布局文件转换成可在代码中实现的View对象。而从context.getParameter()方法中返回一个LayoutInflater示例后,需要使用inflate()方法来加载布局文件,返回一个View对象。inflate()方法有三个参数:布局文件ID、父View以及一个布尔标志。 在大多数情况下,第二个参数都为null,这意味着在加载布局文件时没有父元素。而第三个参数标志通常设置为false,这意味着在加载布局文件时不附加给指定父元素。 因此,调用LayoutInflater.from(context).inflate(R.layout.my_layout, null, false)会返回一个View对象,该对象表示my_layout.xml布局文件的内容。可以将此对象添加到任何视图层次结构中,例如: ViewGroup parent = findViewById(R.id.parent_layout); View child = LayoutInflater.from(context).inflate(R.layout.my_layout, parent, true); parent.addView(child); 在这种情况下,inflate()方法的第二个参数是父元素的引用,表示新加载的View对象将成为此父元素的一部分。第三个参数标志设置为true,这意味着从布局文件加载的视图将自动成为传递给inflate()方法的父元素的一部分。 总之,LayoutInflater.from(context).inflateAndroid开发中非常有用的一个方法,它使您可以轻松地将布局文件转换为可在代码中操作的View对象。了解它的用法可以使您更轻松地开发高质量的Android应用程序。 ### 回答3: layoutinflater.from(context).inflateAndroid 中一种常见的布局填充方法。在 Android 中,我们通常使用 XML 文件创建布局,然后使用 Java 代码调用该布局以填充视图。其中,layoutinflater.from(context) 是获取一个 LayoutInflater 对象的方法,它可以用于动态将布局文件转换为其对应的视图对象并在当前视图中添加。 在上述代码中,context 是用于创建视图的上下文对象,inflate 是用于执行布局填充的方法。该方法中需要传入一个布局文件ID,该 ID 用于确定要填充的布局文件的位置和名称。被填充的布局文件中包含了布局中的所有 View 对象及其属性,包括控件大小、边距、背景等等。填充完成后,该布局文件中的所有视图都将被转换为 Java 中的 View 类型,并作为整个填充视图的一部分添加到 ViewGroup 中。 通常情况下,我们会在 Activity 中调用该方法以填充视图对象,然后通过 findViewById 对填充好的视图对象中的控件进行定位,并对其进行任何必要的更改或操作。这样,我们就可以通过代码实现 UI 界面中各种复杂的布局效果,并实现超出 XML 文件所能实现的更高级的 UI 界面效果。 总之,layoutinflater.from(context).inflateAndroid 中一种非常常见的布局填充方法,它允许我们将布局文件转换为对应的 View 对象,并添加到当前视图中。这种方法在 Android 应用程序开发中非常有用,特别是在创建 UI 界面时。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值