如何将视图引用传递给Android自定义视图?

24

我按照以下步骤进行:

1)创建一个可样式化的

<declare-styleable name="Viewee">
    <attr name="linkedView" format="reference"/>
</declare-styleable>

2) 定义自定义视图布局

<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:background="#ffc0">
    <TextView
            android:id="@+id/custom_text"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="[text]"
            />
</LinearLayout>

3) 创建所需的类

public class Viewee extends LinearLayout
{
public Viewee(Context context, AttributeSet attributeSet)
{
    super(context, attributeSet);
    View.inflate(context, R.layout.viewee, this);
    TextView textView = (TextView) findViewById(R.id.custom_text);
    TypedArray typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.Viewee);
    int id = typedArray.getResourceId(R.styleable.Viewee_linkedView, 0);
    if (id != 0)
    {
        View view = findViewById(id);
        textView.setText(((TextView) view).getText().toString());
    }

    typedArray.recycle();
}
}

最终在以下类似的活动中

<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res/com.ns"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="vertical">
    <TextView
            android:id="@+id/tvTest"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="android"/>
    <com.ns.Viewee
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            app:linkedView="@+id/tvTest"
            />
</LinearLayout>

现在虽然我在Viewee构造函数中接收到了一个非零的id,但是findViewById(id)返回了null并且出现了NullPointerException

我错过了什么?

我按照这里所描述的方法进行操作

3个回答

21

我找到了答案!

问题出在findViewById(id)的调用位置上。根据文档findViewById只会查找子视图,而不会查找位于上层层次结构的视图。因此,我必须像这样调用getRootView().findViewById(id),但由于调用位置不正确,它也返回null

Viewee构造函数中,Viewee本身尚未附加到其根视图,因此该调用会导致NullPointerException

因此,如果我在构造后的其他地方调用getRootView().findViewById(id),它就能正常工作,并且"@+id/tvTest""@id/tvTest"都是正确的。我已经测试过了!

答案如下:

public class Viewee extends LinearLayout
{
    public Viewee(Context context, AttributeSet a)
    {
        super(context, attributeSet);
        View.inflate(context, R.layout.main6, this);
        TextView textView = (TextView) findViewById(R.id.custom_text);
        TypedArray t = context.obtainStyledAttributes(a, R.styleable.Viewee);
        int id = t.getResourceId(R.styleable.Viewee_linkedView, 0);
        if (id != 0)
        {
            _id = id;
        }

        t.recycle();
    }

    private int _id;

    public void Foo()
    {
        TextView textView = (TextView) findViewById(R.id.custom_text);
        View view = getRootView().findViewById(_id);
        textView.setText(((TextView) view).getText().toString());
    }
}

当需要通过其引用ID在活动中的其他位置处理附加视图时,会调用Foo等相关方法。

完全归功于那些在此帖子中做出贡献的人们。在提交问题之前,我还没有看到过这篇帖子。


请将以下代码放置在onAttachedToWindow中,而不是onDraw中。将其放置在onDraw中会导致该方法每秒调用X次,可能达到60次每秒。@DanielWilson,您能删除您的评论吗? - Hugo Gresse

18

我知道这是一个老问题,但是我想提供另一种方法来完成这个任务,因为我想将所有东西封装到我的自定义视图中。

而不是从外部调用,获取层次结构中较高的视图的另一种方法是,我使用了onAttachedToWindow()方法:

public class MyCustomView extends LinearLayout {
    private int siblingResourceId;
    private View siblingView;

    public MyCustomView(Context context, AttributeSet a) {
        super(context, attributeSet);
        inflate(context, R.layout.main6, this);
        TextView textView = (TextView) findViewById(R.id.custom_text);
        TypedArray t = context.obtainStyledAttributes(a, R.styleable.Viewee);
        siblingResourceId = t.getResourceId(R.styleable.MyCustomView_siblingResourceId, NO_ID);
        t.recycle();
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (siblingResourceId != NO_ID) {
            siblingView = ((View) getParent()).findViewById(siblingResourceId);
        }
    }
}

onAttachedToWindow 方法被调用得相当早,但显然足够晚以使整个视图层次结构稳定。至少对于我的需求来说它工作得很完美,而且更加可控,不需要外部交互即可工作 ;-)

EDIT: 添加了 Kotlin 代码

class MyCustomView(context: Context, attributeSet: AttributeSet) : LinearLayout(context, attributeSet) {
    private val siblingResourceId: Int
    private lateinit var siblingView: View

    // All other constructors left out for brevity.

    init {
        inflate(context, R.layout.main6, this)
        val textView = findViewById<TextView>(R.id.custom_text)
        val t = context.obtainStyledAttributes(a, R.styleable.Viewee)
        siblingResourceId = t.getResourceId(R.styleable.MyCustomView_siblingResourceId, NO_ID)
        t.recycle()
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        if (siblingResourceId != NO_ID) {
            siblingView = (parent as View).findViewById(siblingResourceId) 
        }
    }
}

注意:我们假设此自定义 Viewparent本身就是一个View


如果在上一级中有相同ID的其他视图,则将获取该视图。 - Ali mohammadi
@Alimohammadi 不确定这是否有意义 - 如何使用相同的ID构建布局?这是不可能的。 - Darwind
考虑在Activity中的Fragment。假设在activity.xml和fragment.xml文件中有相同的ID,这将返回在Activity中的视图。 - Ali mohammadi
@Alimohammadi 这与我们现在使用的自定义视图有关,而不是一个 Fragment 或者一个 Activity。这就是关于在 View 类中使用 findViewById 的文档解释: 查找具有给定ID的第一个子视图,如果ID与 {@link #getId()} 匹配,则返回该视图本身,如果ID无效(<0)或层次结构中没有匹配的视图,则返回 null。 - Darwind

2

您所描述的android:id被设置为app:linkedView="@+id/tvTest。然而,@+id/tvTest是用于创建一个名为"tvTest"的新id。您想要做的是使用app:linkedView="@id/tvTest


我尝试了你的答案,但是 View view = findViewById(id); 仍然返回 null! - anonim
为什么要使用 id 来保存 View 的 id?使用 R.id.foo 而不是创建另一个变量应该相当简单。此外,通过你的注释我无法确定是否将 <com.ns.Viewee /> 元素的 app:linkedView 属性更改为 "@id/tvTest" - Jason L
我必须保留 id,因为 Viewee 是一个自定义视图,引用另一个视图的原因。我不能像你说的那样引用链接的视图作为 R.id.foo。如果我在另一个布局中使用 Viewee,而 R.id.foo 不存在怎么办?我也按照你的建议将 "@+id/tvTest" 更改为 "@id/tvTest",但没有成功。 - anonim
@+id 而非 @id 并不重要,加号可以。 - Kamil Seweryn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接