在自定义复合视图中使用 <include> 会导致 onFinishInflate() 中的 NullPointerException

Using <include> in custom compound view results in NullPointerException in onFinishInflate()

我正在尝试实现一个简单的自定义拨号盘视图。一切正常,直到我尝试使用 <include> 为我的按钮包含一个 "template",而不是在 XML 文件中明确定义每个按钮。因为我将有 12 个几乎相同的按钮,所以我想通过这样做可以让我的 XML 更简洁一些。

问题是在 onFinishInflate() 我的自定义视图中,当我调用 findViewById(R.id.dialpad_view_btn1) 时它 returns 为空。我当然将 ID dialpad_view_btn1 分配给我的 <include> 之一。如果我只是明确定义按钮而不是从我的 "template" 中包含它,它就可以正常工作。对此有任何解决方法,还是我应该接受 findViewById()<include> 不能一起玩?

DialPadView.java

public class DialPadView extends android.support.v7.widget.GridLayout {


    public DialPadView(Context context) {
        super(context);
        initializeViews(context);
    }

    public DialPadView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initializeViews(context);
    }

    public DialPadView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initializeViews(context);
    }

    private void initializeViews(Context context) {
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.dialpad_view, this);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        // This call to findViewById() results in a NullPointerException
        ((ImageButton) findViewById(R.id.dialpad_view_btn1)).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // I'm just a dummy
            }
        });
    }

}

dialpad_view.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.GridLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.me.myproject.DialPadView"
    xmlns:grid="http://schemas.android.com/apk/res-auto"
    android:id="@+id/dialpad_grid_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_centerHorizontal="true"
    grid:alignmentMode="alignMargins"
    grid:columnCount="3"
    grid:rowCount="4">

    <include
        android:id="@+id/dialpad_view_btn1"
        layout="@layout/dialpad_button" />

    <include
        android:id="@+id/dialpad_view_btn2"
        layout="@layout/dialpad_button" />


</android.support.v7.widget.GridLayout>

dialpad_button.xml

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:grid="http://schemas.android.com/apk/res-auto">

    <Button
        android:layout_width="0dp"
        android:layout_height="0dp"
        grid:layout_columnWeight="1"
        grid:layout_rowWeight="1"
        grid:layout_gravity="fill"
        android:layout_margin="5dp"
        android:scaleType="centerInside"
        android:text="Test" />

</merge>

ID 问题:

您遇到的问题是由于混用了<include><merge>标签造成的。 请注意 <merge> 有不同的用途,在这种情况下不应使用它,因此请仅使用 <incluce> 并使 <Button> 成为 dialpad_button.xml 文件中的根目录。那么所有的id都应该正确分配。

<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:grid="http://schemas.android.com/apk/res-auto">
    android:layout_width="0dp"
    android:layout_height="0dp"
    grid:layout_columnWeight="1"
    grid:layout_rowWeight="1"
    grid:layout_gravity="fill"
    android:layout_margin="5dp"
    android:scaleType="centerInside"
    android:text="Test" />

详情:

如果你对这个问题感到好奇,你可以查看 LayoutInflater 源文件中的 parseInclude() 方法。您可以在那里找到处理 <merge> inflation 的代码。可以看到,<include> 的子级传递 layout 和 id 属性的代码没有为 <merge> 调用,因为 <merge> 应该用于不同的场景。那里:

if (TAG_MERGE.equals(childName)) {
    // Inflate all children.
    rInflate(childParser, parent, childAttrs, false);
} else {
    [...]

    // We try to load the layout params set in the <include /> tag. If
    // they don't exist, we will rely on the layout params set in the
    // included XML file.

    [...]

    // Attempt to override the included layout's android:id with the
    // one set on the <include /> tag itself.
    TypedArray a = mContext.obtainStyledAttributes(attrs,
        com.android.internal.R.styleable.View, 0, 0);
    int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID);
    // While we're at it, let's try to override android:visibility.

    [...]
}

其他问题:

这与您缺少 ID 的问题没有严格的关系,但正如 pskink 提到的那样,您在自定义 DialPadView 中错误地膨胀了 <android.support.v7.widget.GridLayout>,因此您最终会得到这样的层次结构:

<DialPadView>
    <android.support.v7.widget.GridLayout>
        <Button/>
        <Button/>
    </android.support.v7.widget.GridLayout>
</DialPadView>

但是 <DialPadView> 已经是 android.support.v7.widget.GridLayout 的一个实例,所以基本上你最终会在 GridLayout 中得到 GridLayout。除了不是最优的之外,它可能导致布局看起来与您想象的外观和行为不同。