У меня есть вложенный RecyclerView. Я назову их ParentRecyclerView и ChildRecyclerView.
Моя цель довольно сложная.
TouchEvent
)TouchEvent
, как номер 1)TouchEvent
. ChildRecyclerView получает TouchEvent
.) 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
.
Как я могу это решить?? Заранее спасибо. :)
ОТРЕДАКТИРОВАНО:
Внесли некоторые изменения в предоставленную вами суть:
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?
проблема, с которой вы сталкиваетесь, заключается в том, что метод isViewInBounds() всегда возвращает true, в результате чего родительское представление перехватывает событие касания. Итак, здесь вы можете получить события касания childRecyclerView (дочернее представление).
Внесли некоторые изменения в суть
Прежде всего, спасибо, Акшай! Это был хороший код, но когда я касаюсь childRecyclerView, эффект касания не работает.
Сделал некоторые модификации. Пожалуйста, проверьте.
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, эффект касания также отображается. Я не хочу создавать сенсорный эффект на родительском макете (владельце просмотра), когда я прокручиваю дочерний просмотрщик! Но это действительно хороший код. Меня это очень вдохновило. :)
В моем тесте родительский RV не получает событие щелчка, когда на дочернем RV выполняется горизонтальное пролистывание.
Я только что изменил исходный пользовательский макет ограничения на ваш. Что я пропустил? Не могли бы вы показать мне весь свой код с сутью или чем-то еще?
Проведите по горизонтали, а затем уберите палец. эффект все равно остается на нем.
Я проверил остальную часть вашей реализации, это вызвано вашим селектором sbg_white.xml. Закомментируйте или удалите раздел android:state_pressed = "true". Пожалуйста, смотрите обновленный ответ.
Извините, если я вас запутал. Мне тоже нужен эффект. пожалуйста, проверьте мой первоначальный вопрос. Я хочу, чтобы ParentViewHolder становился серым, когда я касаюсь (щелкаю) где угодно, но я не хочу, чтобы ParentViewHolder становился серым, когда я прокручиваю дочерний rv.
Вам нужно обработать обновление выбранного состояния вашего родительского элемента recyclerview через onClickListener. Пожалуйста, проверьте обновленный ответ.
Окончательно! Я сделал это!! Спасибо, лп!! Я очень ценю ваш эффект для меня. я загружу сюда свой полный код. Спасибо!!!!
@CodingBruceLee, пожалуйста. Также добавлена опция, которая может вам пригодиться в обновленном ответе. Буду признателен, если вы отметите это как ответ.
Наконец-то я смог это сделать. Прежде всего, спасибо ltp и Акшаю. Ребята, вы меня очень вдохновили.
Прежде чем идти дальше, давайте подведем итоги.
Чего я хотел?
ПРИМЕЧАНИЕ. Этот код не был бы идеальным.
Основная деятельность
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>
Где вы использовали собственный макет ограничений?