Отвлечение TouchEvent в NestedRecyclerView

У меня есть вложенный RecyclerView. Я назову их ParentRecyclerView и ChildRecyclerView. Моя цель довольно сложная.

  1. Если я нажму на элемент ParentRecyclerView, то цвет фона элемента ParentRecyclerView изменится на серый. (Это означает, что ParentRecyclerView получит TouchEvent)
  2. Если я нажму на элемент ChildRecyclerView, то эффект будет таким же, как и с номером 1. Это означает, что цвет элемента родительского Recyclerview будет изменен на серый (нажатие/прикосновение, это означает, что ParentRecyclerView получает TouchEvent, как номер 1)
  3. Если я коснусь и перетащу ChildRecyclerView по горизонтали, я смогу прокрутить ChildRecyclerView. (Это означает, что ParentRecyclerView не получает TouchEvent. ChildRecyclerView получает TouchEvent.)
    Я использовал селектор с XML, чтобы изменить фон элемента.
    и я использовал TouchEvent, чтобы заставить его работать так, как я хотел, и сделал собственный ConstraintLayout.

Вот код пользовательского 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)
    }
}

но код не работает, потому что когда я касаюсь childRecyclerView, isViewInBounds() всегда возвращается true. Итак, ParentRecyclerView получает TouchEvent.
Как я могу это решить?? Заранее спасибо. :)

Где вы использовали собственный макет ограничений?

ltp 19.04.2024 00:07
0
1
101
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

ОТРЕДАКТИРОВАНО:

Внесли некоторые изменения в предоставленную вами суть:

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) {
        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())
            }
        }
        return false
    }

    private fun View.isViewInBounds(x: Int, y: Int): Boolean {
        val location = IntArray(2)
        getLocationOnScreen(location)
        val left = location[0]
        val top = location[1]
        val right = left + width
        val bottom = top + height
        return x >= left && x <= right && y >= top && y <= bottom
    }
}

class ParentAdapter : ListAdapter<ParentItem, ParentAdapter.ParentViewHolder>(diffUtil) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParentViewHolder {
        val binding = ItemRecyclerviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ParentViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ParentViewHolder, position: Int) {
        holder.bind()
    }

    inner class ParentViewHolder(val binding: ItemRecyclerviewBinding) : RecyclerView.ViewHolder(binding.root) {

        lateinit var childAdapter: ChildAdapter

        init {
            binding.root.findViewById<TouchThroughConstraintLayout>(R.id.touchThroughLayout).setChildView(binding.childRecyclerView)
        }

        fun bind() = with(binding) {
            childRecyclerView.apply {
                childAdapter = ChildAdapter()
                adapter = childAdapter
            }

            childAdapter.submitList(arrayListOf(
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
                HashTag(),
            ))
        }

    }

    companion object {
        val diffUtil = object : DiffUtil.ItemCallback<ParentItem>() {

            override fun areItemsTheSame(oldItem: ParentItem, newItem: ParentItem): Boolean {
                return oldItem == newItem
            }

            override fun areContentsTheSame(oldItem: ParentItem, newItem: ParentItem): Boolean {
                return oldItem == newItem
            }

        }
    }
}

<?xml version = "1.0" encoding = "utf-8"?>
<com.example.nestedrecyclerviewtouchevent.TouchThroughConstraintLayout 
    xmlns:android = "http://schemas.android.com/apk/res/android"
    xmlns:app = "http://schemas.android.com/apk/res-auto"
    android:id = "@+id/touchThroughLayout"
    android:layout_width = "match_parent"
    android:layout_height = "60dp"
    android:clickable = "true"
    android:focusable = "true"
    android:background = "@drawable/sbg_white">

    <androidx.recyclerview.widget.RecyclerView
        android:id = "@+id/childRecyclerView"
        android:layout_width = "200dp"
        android:layout_height = "36dp"
        android:layout_gravity = "bottom"
        android:orientation = "horizontal"
        app:layout_constraintTop_toTopOf = "parent"
        app:layout_constraintStart_toStartOf = "parent"
        app:layout_constraintBottom_toBottomOf = "parent"
        app:layout_constraintEnd_toEndOf = "parent"
        app:layoutManager = "androidx.recyclerview.widget.LinearLayoutManager" />

</com.example.nestedrecyclerviewtouchevent.TouchThroughConstraintLayout>

Какой ChildView вы говорите? тот, что в ChildRecyclerView?

CodingBruceLee 17.04.2024 08:02

проблема, с которой вы сталкиваетесь, заключается в том, что метод isViewInBounds() всегда возвращает true, в результате чего родительское представление перехватывает событие касания. Итак, здесь вы можете получить события касания childRecyclerView (дочернее представление).

Akshay 17.04.2024 09:22

Внесли некоторые изменения в суть

Akshay 18.04.2024 11:07

Прежде всего, спасибо, Акшай! Это был хороший код, но когда я касаюсь childRecyclerView, эффект касания не работает.

CodingBruceLee 19.04.2024 14:20
Ответ принят как подходящий

Сделал некоторые модификации. Пожалуйста, проверьте.

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

}

Обновлено: также вам придется удалить android:state_pressed = "true" часть вашего 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>

Добавьте логику, обрабатывающую состояние выбора родительского элемента RV:

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

}

Если необходимо сохранить другие состояния элемента rv, например положение прокрутки, вы можете выполнить частичную привязку, используя измененную полезную нагрузку:

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

}

Единственная проблема заключается в том, что когда я убрал палец, эффект касания все еще остается на ParentItem, а также, когда я прокручиваю дочерний элемент recyclerView, эффект касания также отображается. Я не хочу создавать сенсорный эффект на родительском макете (владельце просмотра), когда я прокручиваю дочерний просмотрщик! Но это действительно хороший код. Меня это очень вдохновило. :)

CodingBruceLee 19.04.2024 14:22

В моем тесте родительский RV не получает событие щелчка, когда на дочернем RV выполняется горизонтальное пролистывание.

ltp 19.04.2024 14:31

Я только что изменил исходный пользовательский макет ограничения на ваш. Что я пропустил? Не могли бы вы показать мне весь свой код с сутью или чем-то еще?

CodingBruceLee 19.04.2024 14:34

Проведите по горизонтали, а затем уберите палец. эффект все равно остается на нем.

CodingBruceLee 19.04.2024 14:35

Я проверил остальную часть вашей реализации, это вызвано вашим селектором sbg_white.xml. Закомментируйте или удалите раздел android:state_pressed = "true". Пожалуйста, смотрите обновленный ответ.

ltp 19.04.2024 14:49

Извините, если я вас запутал. Мне тоже нужен эффект. пожалуйста, проверьте мой первоначальный вопрос. Я хочу, чтобы ParentViewHolder становился серым, когда я касаюсь (щелкаю) где угодно, но я не хочу, чтобы ParentViewHolder становился серым, когда я прокручиваю дочерний rv.

CodingBruceLee 19.04.2024 14:57

Вам нужно обработать обновление выбранного состояния вашего родительского элемента recyclerview через onClickListener. Пожалуйста, проверьте обновленный ответ.

ltp 19.04.2024 14:58

Окончательно! Я сделал это!! Спасибо, лп!! Я очень ценю ваш эффект для меня. я загружу сюда свой полный код. Спасибо!!!!

CodingBruceLee 19.04.2024 15:18

@CodingBruceLee, пожалуйста. Также добавлена ​​опция, которая может вам пригодиться в обновленном ответе. Буду признателен, если вы отметите это как ответ.

ltp 19.04.2024 15:32

Наконец-то я смог это сделать. Прежде всего, спасибо ltp и Акшаю. Ребята, вы меня очень вдохновили. Прежде чем идти дальше, давайте подведем итоги.
Чего я хотел?

  1. Когда я нажимаю (касаюсь) везде на ParentViewholder, эффект касания отображается, даже если я касаюсь дочернего RecyclerView.
  2. когда я прокручиваю (прокручиваю) дочерний RecyclerView, я могу прокручивать rv, но эффект касания на ParentViewHolder не должен отображаться.

ПРИМЕЧАНИЕ. Этот код не был бы идеальным.

Основная деятельность

class MainActivity : AppCompatActivity() {
    private val binding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        val parentAdapter = ParentAdapter()
        binding.parentRecyclerView.apply {
            addItemDecoration(DividerItemDecoration(this@MainActivity, LinearLayout.VERTICAL))
            adapter = parentAdapter
            itemAnimator = null
        }

        parentAdapter.submitList(arrayListOf(
            ParentItem(),
            ParentItem(),
            .... // some items...
            ParentItem(),
        ))

    }
}

Activity_main

<?xml version = "1.0" encoding = "utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android = "http://schemas.android.com/apk/res/android"
    xmlns:app = "http://schemas.android.com/apk/res-auto"
    xmlns:tools = "http://schemas.android.com/tools"
    android:layout_width = "match_parent"
    android:layout_height = "match_parent"
    tools:context = ".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id = "@+id/parentRecyclerView"
        android:layout_width = "0dp"
        android:layout_height = "0dp"
        app:layoutManager = "androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf = "parent"
        app:layout_constraintEnd_toEndOf = "parent"
        app:layout_constraintStart_toStartOf = "parent"
        app:layout_constraintTop_toTopOf = "parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Родительский адаптер

data class ParentItem(
    val name: String = ""
)

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)
        val holder = ParentViewHolder(binding)
        holder.itemView.setOnClickListener {
            notifyItemChanged(holder.adapterPosition, true)
            true
        }

        return holder
    }

    override fun onBindViewHolder(
        holder: ParentViewHolder,
        position: Int,
        payloads: MutableList<Any>
    ) {
        if (payloads.isNotEmpty()) {
            holder.bindForClick()
            return
        }

        super.onBindViewHolder(holder, position, payloads)
    }

    override fun onBindViewHolder(holder: ParentViewHolder, position: Int) {
        holder.bind(selectedPosition)
    }

    class ParentViewHolder(val binding: ItemRecyclerviewBinding): RecyclerView.ViewHolder(binding.root) {

        lateinit var childAdapter: ChildAdapter

        init {
            binding.root.setChildView(binding.childRecyclerView)
        }

        fun bindForClick() {
            itemView.isSelected = true
            itemView.postDelayed({
                itemView.isSelected = false
            }, 100)

        }

        fun bind(selectedPosition: Int) = with(binding) {
            childRecyclerView.apply {
                childAdapter = ChildAdapter()
                adapter = childAdapter
            }

            childAdapter.submitList(arrayListOf(
                HashTag(),
                HashTag(),
                ... // some items...
                HashTag(),
            ))
        }

    }

    companion object {
        val diffUtil = object : DiffUtil.ItemCallback<ParentItem>() {

            override fun areItemsTheSame(oldItem: ParentItem, newItem: ParentItem): Boolean {
                return oldItem == newItem
            }

            override fun areContentsTheSame(oldItem: ParentItem, newItem: ParentItem): Boolean {
                return oldItem == newItem
            }

        }
    }

}

item_recyclerview.xml(для держателя просмотра родительского адаптера)

<?xml version = "1.0" encoding = "utf-8"?>
<com.example.nestedtouchthroughrecyclerview.TouchThroughConstraintLayout xmlns:android = "http://schemas.android.com/apk/res/android"
    xmlns:app = "http://schemas.android.com/apk/res-auto"
    android:id = "@+id/touchThroughLayout"
    android:layout_width = "match_parent"
    android:layout_height = "60dp"
    android:clickable = "true"
    android:focusable = "true"
    android:background = "@drawable/sbg_white">

    <androidx.recyclerview.widget.RecyclerView
        android:id = "@+id/childRecyclerView"
        android:layout_width = "200dp"
        android:layout_height = "36dp"
        android:layout_gravity = "bottom"
        android:orientation = "horizontal"
        app:layout_constraintTop_toTopOf = "parent"
        app:layout_constraintStart_toStartOf = "parent"
        app:layout_constraintBottom_toBottomOf = "parent"
        app:layout_constraintEnd_toEndOf = "parent"
        app:layoutManager = "androidx.recyclerview.widget.LinearLayoutManager" />

</dcom.example.nestedtouchthroughrecyclerview.TouchThroughConstraintLayout>

Детский адаптер

data class HashTag(
    val hashTags: List<String> = arrayListOf("noodle", "ramen", "meat", "cake", "happy", "skate", "castle"),
)

class ChildAdapter : ListAdapter<HashTag, ChildAdapter.ChildViewHolder>(diffUtil) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChildViewHolder {
        val binding = ItemHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ChildViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ChildViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    class ChildViewHolder(val binding: ItemHashtagBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: HashTag) {
            binding.tvHashTag.text = "cake"
        }

    }

    companion object {
        val diffUtil = object : DiffUtil.ItemCallback<HashTag>() {

            override fun areItemsTheSame(oldItem: HashTag, newItem: HashTag): Boolean {
                return oldItem == newItem
            }

            override fun areContentsTheSame(oldItem: HashTag, newItem: HashTag): Boolean {
                return oldItem == newItem
            }

        }
    }

}

item_hashtag.xml

<?xml version = "1.0" encoding = "utf-8"?>
<TextView xmlns:android = "http://schemas.android.com/apk/res/android"
    xmlns:tools = "http://schemas.android.com/tools"
    android:id = "@+id/tvHashTag"
    android:layout_width = "40dp"
    android:layout_height = "20dp"
    android:background = "@drawable/bg_hashtag_btn"
    android:gravity = "center"
    android:textStyle = "bold"
    android:paddingHorizontal = "3dp"
    android:layout_marginEnd = "3dp"
    android:layout_marginBottom = "7dp"
    android:textColor = "@color/black"
    android:textSize = "13dp"
    tools:text = "test"
    tools:ignore = "SpUsage" />

bg_hashtag.xml

<?xml version = "1.0" encoding = "utf-8"?>
<shape xmlns:android = "http://schemas.android.com/apk/res/android" android:shape = "rectangle">
    <solid android:color = "#e6e6e6"/>
    <corners android:radius = "5dp"/>
    <size android:width = "30dp" android:height = "15dp" />
</shape>

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>

Другие вопросы по теме

Похожие вопросы

Глубокие ссылки на шаблоны Android с параметрами запроса
Как я могу наблюдать, когда список, хранящийся в другом классе, изменяется для обновления моего пользовательского интерфейса Compose?
Как добавить собственный код C++ в приложение AOSP с помощью JNI? Я пытаюсь изменить EmbeddedKitchenSinkApp. Я работаю с Android 11
Как узнать, что вызвало внутреннюю ошибку компилятора в Android Studio?
Следует ли мне избегать создания классов и их компиляции во время выполнения в Android?
Java.lang.ClassCastException при инициализации моих моделей представления, которые будут созданы во время выполнения
TopAppBar с сбоем в работе ScrollBehavior с HorizontalPager
Как я могу оптимизировать получение данных на моем внутреннем сервере KTOR при обработке запросов API?
Как наблюдать за изменением настроек приложения в приложении?
Использование привязки Flutter Getx, которая видна в видимости, не работает, даже если данные изменились