androidandroid-recyclerviewandroid-touch-event

Distracting TouchEvent in NestedRecyclerView


I have a nested RecyclerView. I'll call them as ParentRecyclerView and ChildRecyclerView. My goal is kind of complicated.

  1. If i click ParentRecyclerView's item, then the color of ParentRecyclerView's item bckground will be changed to gray.(It means the ParentRecyclerView gets the TouchEvent)
  2. If i click ChildRecyclerView's item, then the effect is same with number 1. It means the color of parentRecyclerview's item will be changed to gray(clicked/touched, It means the ParentRecyclerView gets the TouchEvent like Number 1)
  3. If i touch and drag the ChildRecyclerView Horizontally, then i can scroll the ChildRecyclerView.(It means the ParentRecylcerView doesn't get the TouchEvent. ChildRecyclerView gets the TouchEvent.)
    I used selector with XML to change the background of the item.
    and I used TouchEvent to make it work the way I wanted it to and made a custom ConstraintLayout.

Here is the code of custom ConstraintLayout.

class TouchThroughConstraintLayout(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) {
    private var mLastMotionX = 0f
    private var mLastMotionY = 0f

    private lateinit var childViewGroup: ViewGroup

    fun setChildView(childViewGroup: ViewGroup) { // in this case, the childViewGroup is ChildRecyclerView
        this.childViewGroup = childViewGroup
    }

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                mLastMotionX = ev.x
                mLastMotionY = ev.y
                return childViewGroup.isViewInBounds(ev.rawX.toInt(), ev.rawY.toInt())
            }

            MotionEvent.ACTION_MOVE -> {
                return false
            }
        }

        return false
    }

    private fun View.isViewInBounds(x : Int, y :Int): Boolean{
        val outRect = Rect()
        val location = IntArray(2)

        getDrawingRect(outRect)
        getLocationOnScreen(location)
        outRect.offset(location[0], location[1])

        return outRect.contains(x, y)
    }
}

but the code doesn't work because When i touch the childRecyclerView, isViewInBounds() always returns true. So, the ParentRecyclerView gets the TouchEvent.
How can i solve it?? Thanks in advance. :)

enter image description here


Solution

  • Made some modifications. Please check.

    class TouchThroughConstraintLayout(context: Context, attrs: AttributeSet?)
        : ConstraintLayout(context, attrs) {
        private var mLastMotionX = 0f
        private var mLastMotionY = 0f
    
        private lateinit var childViewGroup: ViewGroup
    
        private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
    
        fun setChildView(childViewGroup: ViewGroup) {
            this.childViewGroup = childViewGroup
        }
    
        private var childViewIsTouched = false
        private var childViewIsSwipedHorinzontally = false
    
        override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
            when (ev.action) {
                MotionEvent.ACTION_DOWN -> {
                    mLastMotionX = ev.x
                    mLastMotionY = ev.y
                    // Check if the touch is within the bounds of the child view group
                    childViewIsTouched = childViewGroup.isViewInBounds(ev.rawX.toInt(), ev.rawY.toInt())
                    // Always intercept the touch event to handle it in onTouchEvent
                    return true
                }
            }
            return super.onInterceptTouchEvent(ev)
        }
    
    
        override fun onTouchEvent(event: MotionEvent): Boolean {
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    childViewIsSwipedHorinzontally = false
    
                    //Pass the event to the child view if child is touched
                    if (childViewIsTouched) {
                        childViewGroup.dispatchTouchEvent(event)
                    }
                }
    
                MotionEvent.ACTION_MOVE -> {
                    //Pass the event to the child view if childView is touched Touched and swiped
                    if (childViewIsTouched && isHorizontalSwipe(event)) {
                        childViewIsSwipedHorinzontally = true
                        childViewGroup.dispatchTouchEvent(event)
                    }
    
                }
                //If child view is not swiped consume the event
                MotionEvent.ACTION_UP,
                MotionEvent.ACTION_CANCEL -> {
                    return if (!childViewIsSwipedHorinzontally) {
                        super.onTouchEvent(event)
                    }else{
                        false
                    }
                }
            }
            return super.onTouchEvent(event)
        }
    
        private fun isHorizontalSwipe(event: MotionEvent): Boolean {
            val adx = abs(event.x - mLastMotionX)
            return adx > touchSlop
        }
    
        private fun View.isViewInBounds(x : Int, y :Int): Boolean{
            val outRect = Rect()
            val location = IntArray(2)
    
            getDrawingRect(outRect)
            getLocationOnScreen(location)
            outRect.offset(location[0], location[1])
    
            return outRect.contains(x, y)
        }
    
    }
    
    

    Edit: Also you have to remove android:state_pressed="true" portion of your sbg_white.xml.

    <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="http://schemas.android.com/apk/res/android" >
        <item android:state_enabled="false">
            <shape android:shape="rectangle">
                <solid android:color="#EAEAEA" />
            </shape>
        </item>
        <item android:state_selected="true">
            <shape android:shape="rectangle">
                <solid android:color="#FFF1F1F1"/>
            </shape>
        </item>
    <!--    <item android:state_pressed="true">
            <shape android:shape="rectangle">
                <solid android:color="#FFF1F1F1"/>
            </shape>
        </item>-->
        <item>
            <shape android:shape="rectangle">
                <solid android:color="#FFF"/>
            </shape>
        </item>
    </selector>
    
    

    Add logic handling the selection state of parent RV item:

    class ParentAdapter: ListAdapter<ParentItem, ParentAdapter.ParentViewHolder>(diffUtil) {
    
      private var selectedPosition = -1
    
      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParentViewHolder {
            val binding = ItemRecyclerviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
            return ParentViewHolder(binding).apply{
                itemView.setOnClickListener{
                    val previousSelectedPosition = selectedPosition
                    selectedPosition = bindingAdapterPosition
                    notifyItemChanged(previousSelectedPosition)
                    notifyItemChanged(selectedPosition)
                    true
                }
            }
        }
    
        override fun onBindViewHolder(holder: ParentViewHolder, position: Int) {
            //update selected state accordingly
            holder.itemView.isSelected = position == selectedPosition
    
            holder.bind()
        }
    
       //rest of your codes
    
    }
    
    

    If it's necessary to retain the other states of the rv item like the scroll position, you can do partial bind using change payload:

    class ParentAdapter: ListAdapter<ParentItem, ParentAdapter.ParentViewHolder>(diffUtil) {
    
       companion object{
           private const val PAYLOAD_SELECTED_STATE = "selected_state"
       }
    
      private var selectedPosition = -1
    
      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParentViewHolder {
            val binding = ItemRecyclerviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
            return ParentViewHolder(binding).apply{
                itemView.setOnClickListener{
                    val previousSelectedPosition = selectedPosition
                    selectedPosition = bindingAdapterPosition
                    notifyItemChanged(previousSelectedPosition, PAYLOAD_SELECTED_STATE)
                    notifyItemChanged(selectedPosition, PAYLOAD_SELECTED_STATE )
                    true
                }
            }
        }
    
        override fun onBindViewHolder(
            holder: NewsViewHolder,
            position: Int,
            payloads: MutableList<Any>
        ) {
            if (payloads.isEmpty()) {
                super.onBindViewHolder(holder, position, payloads)
            }else {
                for (payload in payloads) {         
                    if (payload == PAYLOAD_SELECTED_STATE) {
                       //update selected state accordingly
                        holder.itemView.isSelected = position == selectedPosition
                    }
                }
            }
        }
    
        override fun onBindViewHolder(holder: ParentViewHolder, position: Int) {
            //update selected state accordingly
            holder.itemView.isSelected = position == selectedPosition
    
            holder.bind()
        }
    
       //rest of your codes
    
    }