Use group in ConstraintLayout to listen for click events on multiple views

27,298

Solution 1

The Group in ConstraintLayout is just a loose association of views AFAIK. It is not a ViewGroup, so you will not be able to use a single click listener like you did when the views were in a ViewGroup.

As an alternative, you can get a list of ids that are members of your Group in your code and explicitly set the click listener. (I have not found official documentation on this feature, but I believe that it is just lagging the code release.) See documentation on getReferencedIds here.

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.
            }
        });
    }

In Kotlin you can build an extension function for that.

Kotlin:

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

Then call the function on the group:

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

Update

The referenced ids are not immediately available in 2.0.0-beta2 although they are in 2.0.0-beta1 and before. "Post" the code above to grab the reference ids after layout. Something like this will work.

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()
                }
            })
        }
    }
}

This should work for releases prior to 2.0.0-beta2, so you can just do this and not have to do any version checks.

Solution 2

The better way to listen to click events from multiple views is to add a transparent view as a container on top of all required views. This view has to be at the end (i.e on top) of all the views you need to perform a click on.

Sample container view :

<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"/>

Above sample contains all four constraint boundaries within that, we can add views that to listen together and as it is a view, we can do whatever we want, such as ripple effect.

Solution 3

To complement the accepted answer for Kotlin users create an extension function and accept a lambda to feel more like the API group.addOnClickListener { }.

Create the extension function:

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

usage:

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

Solution 4

The extension method is great but you can make it even better by changing it to

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

So the calling would be like this

group.setAllOnClickListener {
    // code to perform on click event
}

Now the need for explicitly defining View.OnClickListener is now gone.

You can also define your own interface for GroupOnClickLitener like this

interface GroupOnClickListener {
    fun onClick(group: Group)
}

and then define an extension method like this

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

and use it like 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")
    }
}

The second approach has a better performance if the number of views is large since you only use one object as a listener for all the views!

Solution 5

While I like the general approach in Vitthalk's answer I think it has one major drawback and two minor ones.

  1. It does not account for dynamic position changes of the single views

  2. It may register clicks for views that are not part of the group

  3. It is not a generic solution to this rather common problem

While I'm not sure about a solution to the second point, there clearly are quite easy ones to the first and third.


1. Accounting position changes of element in the group

This is actually rather simple. One can use the toolset of the constraint layout to adjust the edges of the transparent view. We simply use Barriers to receive the leftmost, rightmost etc. positions of any View in the group. Then we can adjust the transparent view to the barriers instead of concrete views.

3. Generic solution

Using Kotlin we can extend the Group-Class to include a method that adds a ClickListener onto a View as described above. This method simply adds the Barriers to the layout paying attention to every child of the group, the transparent view that is aligned to the barriers and registers the ClickListener to the latter one.

This way we simply need to call the method on the Group and do not need to add the views to the layout manually everytime we need this behaviour.

Share:
27,298

Related videos on Youtube

Endzeit
Author by

Endzeit

Updated on July 09, 2022

Comments

  • Endzeit
    Endzeit almost 2 years

    Basically I'd like to attach a single OnClickListener to multiple views inside a ConstraintLayout.

    Before migrating to the ConstraintLayout the views where inside one layout onto which I could add a listener. Now they are on the same layer with other views right under the ConstraintLayout.

    I tried adding the views to a android.support.constraint.Group and added a OnClickListener to it programmatically.

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

    However this does not seem to work as of the ConstraintLayout version 1.1.0-beta2

    Have I done something wrong, is there a way to achieve this behaviour or do I need to attach the listener to each of the single views?

  • Michael Vescovo
    Michael Vescovo about 6 years
    or you can put it on the bottom if you want to change the background
  • Ivan Wooll
    Ivan Wooll about 6 years
    I love extension functions!
  • Guilherme Lima Pereira
    Guilherme Lima Pereira almost 6 years
    The kotlin extension didn't work for me: Attempt to invoke virtual method 'void android.view.View.setOnClickListener(android.view.View$OnCli‌​ckListener)' on a null object reference
  • Jeff Barger
    Jeff Barger almost 6 years
    Be careful doing this - the ConstraintLayout Group is designed to manage visibility. I just got bit by this - we were trying to use a group to be able to set a common click listener just like this answer, but also wanted to set the visibility of one of the views outside of the group, and it didn't work.
  • lucas_sales
    lucas_sales over 5 years
    Just to look more kotlinish I made a small change fun Group.setAllOnClickListener(listener: (View) -> Unit) { referencedIds.forEach { id -> rootView.findViewById<View>(id).setOnClickListener(listener) } }
  • hopia
    hopia almost 5 years
    This works pretty well. You don't need to group your views together. You can even add a selector to the View's background to indicate the user pressed it.
  • toidv
    toidv almost 5 years
    Had an overlapping issue with multiple Constraints Group in different Fragment
  • Harshad Prajapati
    Harshad Prajapati almost 5 years
    thanks, @Cheticamp great solutions with kotlin extension function. This is absolutely working with implementation 'androidx.constraintlayout:constraintlayout:1.1.3' but if you want to use MotionLayout and Group both using latest implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2' this is not working. as you can not find referencedIds from group. So if you want to use both MotionLayout and Group use implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1'
  • AntekM
    AntekM over 4 years
    One issue with using this approach is that we will end up with non-clickable white spaces between elements of the group, which may result in poor user experience
  • Richard
    Richard over 4 years
    Has there been an extended functionality of Group so that I can only set one click listener to said Group as I would to a ViewGroup?
  • reavcn
    reavcn over 4 years
    @Richard not yet AFAIK
  • reavcn
    reavcn over 4 years
    @AntekM This might be true in some cases, however it really depends on how the layout is built. If you have multiple referenced ids each one either above or below the others, then you won't end up with non-clickable spaces
  • pkuszewski
    pkuszewski about 4 years
    Do you have any tip on how to make this view slightly bigger than indicated by constraints? Like add 4dp on each side of the view?
  • Przemo
    Przemo almost 4 years
    I find it the cleanest solution. It is not fancy, and not perfect, but cleaner than one which outvotes the rest. Human-being vote for popular, fancy things, and devs are (sadly) no different.
  • Cyd
    Cyd over 3 years
    is there a way not to use the findViewByID?
  • charles-allen
    charles-allen over 3 years
    "The second approach has a better performance" - But your "one object" is 3 times the size because it contains all the different listeners. Surely if the listeners are different it makes sense to set 3 different objects; and if they're the same, you can still declare one listener in the first case and pass it to all 3 setters: val listener: (View -> Unit) = {...}
  • charles-allen
    charles-allen over 3 years
    @Cyd - I don't think so; you need to find the views. Remember this only happens once per view during setup, not at click.
  • benzabill
    benzabill almost 3 years
    Very cool solution. Good call that there will be dead spaces depending on how the layout is built @reavcn
  • Mohd Naushad
    Mohd Naushad almost 3 years
    i used this extention in my home fragment but when i click two times on bottom navigation home icon then this extention is not working