How to properly support click on selectable textview in Android

656 Views Asked by At

I have a LinearLayout with a selectable text view

<LinearLayout
    android:id="@+id/linear_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/rectangular_background">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textIsSelectable="true"
        android:text="Clickable"/>

</LinearLayout>

Assigned a click listener on LinearLayout

linear_layout.setOnClickListener {
    Toast.makeText(context, "Linear Layout clicked", Toast.LENGTH_SHORT).show()
}

Toast is not shown on click of text view when it has selectable text. I have tried a solution in this answer by adding following code in generic function to be used for all text views.

val textGestureDetector = GestureDetectorCompat(context, GestureDetector.SimpleOnGestureListener())
textGestureDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
    override fun onDoubleTap(e: MotionEvent?): Boolean = false
    override fun onDoubleTapEvent(e: MotionEvent?): Boolean = false
    override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
        textView.performClick()
        return true
    }
})

textView.setOnTouchListener { _, event ->
    textGestureDetector.onTouchEvent(event)
    false
}

This solution does work when we assign click listener on text view but when text view has no click listener, the click event is not propagated to linear layout.

Since there can be multiple child text views in the linear layout, I do not want to explicitly write code to invoke parent's click listener on each child's click. Is there a way to fix this issue with a generic solution?

Edit: Ideally I would want to create a custom text view that correctly propagates the click irrespective of text being selectable or not. That will help in solving this issue throughout my app. Correctly propagating click would mean 3 things:

  • Double clicking on textview selects the text.
  • When text view has click listener, single click invokes it
  • When text view has no click listener, single click invokes the click listener of the ancestor that has one. Note that this scenario already works well if text is not selectable
2

There are 2 best solutions below

0
beigirad On

What I understood you want to pass a click of selectable text view to its parent.

To do this perform clicking on parent in onSingleTapConfirmed of the child view (selectable textView).

val textGestureDetector = GestureDetectorCompat(this, GestureDetector.SimpleOnGestureListener())
textGestureDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
    override fun onDoubleTap(e: MotionEvent?): Boolean = false
    override fun onDoubleTapEvent(e: MotionEvent?): Boolean = false
    override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
        // pass the click event to its parent
        (textView.parent as? View)?.performClick()
        return true
    }
})

textView.setOnTouchListener { _, event ->
    textGestureDetector.onTouchEvent(event)
    false
}

linearLayout.setOnClickListener {
    Toast.makeText(context,Toast.LENGTH_LONG,Toast.LENGTH_LONG).show()
}
0
Anatolii On

Introduction

As it looks like from your question, what you're trying to achieve is as follows:

  1. If your TextView is textSelectable then single clicks on it should be delegated to the linear_layout.
  2. Long clicks or double taps should still be handled by the TextView

Possible Solution

Why not to set your custom textGestureDetector to every descendant TextView recursively? In this case, you won't need to do it manually for every child/descendant TextView.

So, first, delegate your event in onSingleTapConfirmed to the linear_layout as follows:

val textGestureDetector = GestureDetectorCompat(context, GestureDetector.SimpleOnGestureListener())
textGestureDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
    override fun onDoubleTap(e: MotionEvent?): Boolean = false
    override fun onDoubleTapEvent(e: MotionEvent?): Boolean = false
    override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
        // pass the click event to your linear_layout
        linearLayout.performClick()
        return true
    }
})

Then, add your OnClickListener to the linear_layout as follows:

linear_layout.setOnClickListener {
    Toast.makeText(context,"click just happened",Toast.LENGTH_LONG).show()
}

Finally, call initTextViewSingleTapListener (that will set your custom onTapListener to every TextView that is a descendant of linear_layout)

initTextViewSingleTapListener(linear_layout, linear_layout, textGestureDetector)

It could be implemented as follows:

fun initTextViewSingleTapListener(root: ViewGroup, currentView: View, textGestureDetector: GestureDetectorCompat) {
    if (currentView is TextView) {
        currentView.setOnTouchListener { _, event ->
            textGestureDetector.onTouchEvent(event)
            false
        }
        return
    }
    if (currentView is ViewGroup) {
        for (i in 0..currentView.childCount) {
            val child = currentView.getChildAt(i)
            if (child != null) {
                initTextViewSingleTapListener(root, child, textGestureDetector)
            }
        }
    }
}

If your TextView has a listener already, and so you do not want to replace it, then just add a check like !currentView.hasOnClickListeners() before adding your listener:

...
if (!currentView.hasOnClickListeners()) {
    currentView.setOnTouchListener { _, event ->
        textGestureDetector.onTouchEvent(event)
        false
    }
}
...

Full code

Please find the full example code below:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    val textGestureDetector = GestureDetectorCompat(context, GestureDetector.SimpleOnGestureListener())
    textGestureDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
        override fun onDoubleTap(e: MotionEvent?): Boolean = false
        override fun onDoubleTapEvent(e: MotionEvent?): Boolean = false
        override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
            // pass the click event to the linear layout
            linear_layout.performClick()
            return true
        }
    })

    linear_layout.setOnClickListener {
        Toast.makeText(context,"click just happened",Toast.LENGTH_LONG).show()
    }

    initTextViewSingleTapListener(linear_layout, linear_layout, textGestureDetector)
}

fun initTextViewSingleTapListener(root: ViewGroup, currentView: View, textGestureDetector: GestureDetectorCompat) {
    if (currentView is TextView) {
        if (currentView.hasOnClickListeners()) {
           // do nothing since your View already has a click listener on it. 
           return
        }
        currentView.setOnTouchListener { _, event ->
            textGestureDetector.onTouchEvent(event)
            false
        }
        return
    }
    if (currentView is ViewGroup) {
        for (i in 0..currentView.childCount) {
            val child = currentView.getChildAt(i)
            if (child != null) {
                initTextViewSingleTapListener(root, child, textGestureDetector)
            }
        }
    }
}

Update

Since you question requirements have changed, I'm adding an additional explanation here.

If a textSelectable TextView should delegate a single tap event to its closest ViewGroup ancestor, then in your initTextViewSingleTapListener()'s block that looks as below:

    if (currentView is ViewGroup) {
        for (i in 0..currentView.childCount) {
            val child = currentView.getChildAt(i)
            if (child != null) {
                initTextViewSingleTapListener(root, child, textGestureDetector)
            }
        }
    }

Replace root with currentView so that to delegate all single tap events to the nearest ViewGroup ancestor as:

...
initTextViewSingleTapListener(currentView, child, textGestureDetector)
...