在ConstraintLayout中使用组来监听多个视图的点击事件

50

基本上我想在ConstraintLayout中将单个OnClickListener附加到多个视图上。

在迁移到ConstraintLayout之前,这些视图位于一个布局内,我可以在其中添加侦听器。现在它们与其他视图位于同一层,在ConstraintLayout下方。

我尝试将这些视图添加到android.support.constraint.Group中,并以编程方式添加OnClickListener。

group.setOnClickListener {
    Log.d("OnClick", "groupClickListener triggered")
}

然而,自约束布局版本 1.1.0-beta2 以来,这似乎不起作用。

我做错了什么吗?有没有一种方法实现这种行为,或者我需要将监听器附加到每个单独的视图上?

8个回答

75

ConstraintLayout中的Group只是一组松散关联的视图,据我所知,并不是一个ViewGroup,因此您将无法像在ViewGroup中使用单个点击侦听器那样使用它。

作为替代方案,您可以在代码中获取作为成员的Group的ID列表,并显式地设置点击侦听器。有关此功能的文档,请参见getReferencedIds此处

Java:

    Group group = findViewById(R.id.group);
    int refIds[] = group.getReferencedIds();
    for (int id : refIds) {
        findViewById(id).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // your code here.
            }
        });
    }

在 Kotlin 中,你可以为此构建一个扩展函数。

Kotlin:

    fun Group.setAllOnClickListener(listener: View.OnClickListener?) {
        referencedIds.forEach { id ->
            rootView.findViewById<View>(id).setOnClickListener(listener)
        }
    }

然后在该组上调用此函数:

    group.setAllOnClickListener(View.OnClickListener {
        // code to perform on click event
    })

更新

虽然在 2.0.0-beta1 及之前版本中能够立即使用参考的 ids,但是它们在 2.0.0-beta2 中不可用。请按照上面的代码进行布局后再获取参考 ids。类似下面这样的代码可以正常工作。

class MainActivity : AppCompatActivity() {
    fun Group.setAllOnClickListener(listener: View.OnClickListener?) {
        referencedIds.forEach { id ->
            rootView.findViewById<View>(id).setOnClickListener(listener)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Referenced ids are not available here but become available post-layout.
        layout.post {
            group.setAllOnClickListener(object : View.OnClickListener {
                override fun onClick(v: View) {
                    val text = (v as Button).text
                    Toast.makeText(this@MainActivity, text, Toast.LENGTH_SHORT).show()
                }
            })
        }
    }
}

这适用于2.0.0-beta2 之前的版本,因此您只需执行此操作,而不必进行任何版本检查。


4
我喜欢扩展函数! - Ivan Wooll
小心操作 - ConstraintLayout Group 旨在管理 _可见性_。我刚刚被这个问题困扰了 - 我们试图使用一个组来设置一个公共的点击监听器,就像这个答案一样,但是我们还想设置组外面一个视图的可见性,结果失败了。 - Jeff Barger
2
为了让代码更符合 Kotlin 风格,我做了一个小改动。fun Group.setAllOnClickListener(listener: (View) -> Unit) { referencedIds.forEach { id -> rootView.findViewById(id).setOnClickListener(listener) } } - lucas_sales
2
使用这种方法的一个问题是,我们最终会在组元素之间得到不可点击的空白空间,这可能会导致用户体验不佳。 - AntekM
Group 的扩展功能是否已经实现,以便我可以像对待 ViewGroup 一样,仅为该 Group 设置一个点击监听器? - Richard
显示剩余7条评论

20

更好的监听多个视图的点击事件的方法是在所有需要执行点击操作的视图上方添加一个透明视图作为容器。这个视图必须位于所有需要执行点击操作的视图的末尾(即最上面)。

示例容器视图:

<View
   android:id="@+id/view_container"
   android:layout_width="0dp"
   android:layout_height="0dp"
   app:layout_constraintBottom_toBottomOf="@+id/view_bottom"
   app:layout_constraintEnd_toEndOf="@+id/end_view_guideline"
   app:layout_constraintStart_toStartOf="@+id/start_view_guideline"
   app:layout_constraintTop_toTopOf="parent"/>

以上示例包含其中的所有四个约束边界,我们可以添加视图以一起侦听,由于它是一个视图,因此我们可以随心所欲地进行操作,例如涟漪效果。


1
或者您可以将其放在底部,如果您想更改背景。 - Michael Vescovo
1
这个很好用。你不需要将你的视图分组在一起。你甚至可以添加一个选择器到视图的背景中来指示用户按下了它。 - hopia
你有什么建议可以让这个视图比约束指示的稍微大一点吗?比如在视图的每一侧添加4dp? - pkuszewski
我认为这是最干净的解决方案。它并不花哨,也不完美,但比其他投票结果更干净。人类会投票给流行、花哨的东西,开发者(可悲地)也不例外。 - PrzemekTom

17

为了补充针对Kotlin用户的接受答案,创建一个扩展函数并接受一个lambda表达式以更像API group.addOnClickListener { }

创建扩展函数:

fun Group.addOnClickListener(listener: (view: View) -> Unit) {
    referencedIds.forEach { id ->
        rootView.findViewById<View>(id).setOnClickListener(listener)
    }
}

用法:

group.addOnClickListener { v ->
    Log.d("GroupExt", v)
}

有没有不使用findViewByID的方法? - Cyd
1
@Cyd - 我不这么认为;你需要找到视图。请记住,这仅在设置期间每个视图发生一次,而不是在单击时发生。 - charles-allen

6
扩展方法很棒,但你可以通过将其更改为

来使其变得更好

fun Group.setAllOnClickListener(listener: (View) -> Unit) {
    referencedIds.forEach { id ->
        rootView.findViewById<View>(id).setOnClickListener(listener)
    }
}

所以调用的方式应该是这样的:
group.setAllOnClickListener {
    // code to perform on click event
}

现在,显式定义View.OnClickListener的需求已经消失了。
您也可以像这样定义自己的GroupOnClickLitener接口。
interface GroupOnClickListener {
    fun onClick(group: Group)
}

然后定义一个扩展方法,像这样:
fun Group.setAllOnClickListener(listener: GroupOnClickListener) {
    referencedIds.forEach { id ->
        rootView.findViewById<View>(id).setOnClickListener { listener.onClick(this)}
    }
}

并像这样使用它

groupOne.setAllOnClickListener(this)
groupTwo.setAllOnClickListener(this)
groupThree.setAllOnClickListener(this)

override fun onClick(group: Group) {
    when(group.id){
        R.id.group1 -> //code for group1
        R.id.group2 -> //code for group2
        R.id.group3 -> //code for group3
        else -> throw IllegalArgumentException("wrong group id")
    }
}

如果视图数量较大,则第二种方法在性能方面表现更好,因为您只需使用一个对象作为所有视图的监听器!

第二种方法具有更好的性能 - 但是您的“一个对象”大小是三倍,因为它包含所有不同的侦听器。如果侦听器不同,则将3个不同的对象设置为有意义的;如果它们相同,则仍然可以在第一种情况下声明一个侦听器,并将其传递给所有3个setter:val listener: (View -> Unit) = {...} - charles-allen

3
在ConstraintLayout 2.0.0中,你可以使用Layer来解决多个视图的点击事件,并且还支持比例动画。

3

虽然我喜欢Vitthalk的答案的总体方法,但我认为它有一个主要缺点和两个次要缺点。

  1. 它没有考虑单个视图的动态位置变化

  2. 它可能会注册不属于组的视图的点击

  3. 这不是解决这个常见问题的通用解决方案

虽然我不确定第二点的解决方案,但对于第一点和第三点,很明显有相当简单的解决方案。


1. 考虑组中元素的位置变化

这实际上相当简单。我们可以使用约束布局的工具集来调整透明视图的边缘。 我们只需使用障碍物来接收组中任何视图的最左、最右等位置。 然后我们可以将透明视图调整到障碍物而不是具体视图。

3. 通用解决方案

使用Kotlin,我们可以扩展Group-Class以包括像上面描述的View添加ClickListener的方法。 这个方法只需将障碍物添加到布局中,注意组的每个子元素、对齐到障碍物的透明视图,并向后者注册ClickListener。

这样,我们只需要在Group上调用该方法,而不需要每次需要此行为时手动将视图添加到布局中。


2

对于像我一样的Java开发者:

public class MyConstraintLayoutGroup extends Group {
    public MyConstraintLayoutGroup(Context context) {
        super(context);
    }

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

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

    public void setOnClickListener(OnClickListener listener) {
        for (int id : getReferencedIds()) {
            getRootView().findViewById(id).setOnClickListener(listener);
        }
    }
}

然而,这并未将点击状态传播到所有其他子元素。


0
fun ConstraintLayout.setAllOnClickListener(listener: (View) -> Unit) {
    children.forEach { view ->
        rootView.findViewById<View>(view.id).setOnClickListener { listener.invoke(this) }
    }
}

然后

.setAllOnClickListener {
                do something
            }

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