Probablemente todos, admirando las hermosas ventanas, barras de herramientas y otras vistas que se mueven mágicamente sin problemas en diferentes direcciones, pensaron en cómo funciona, probablemente incluso leyeron algo sobre CoordianatorLayout, sobre varios Behaivor, que le permiten crear literalmente magia en las vistas de Android. Por supuesto, puede escribir vistas personalizadas con el comportamiento deseado, que solo puede estar limitado por su imaginación o su conocimiento en el desarrollo de Android. Pero además de esto, hay otra limitación: el tiempo, no escribirás vistas personalizadas en un hackathon, en un proyecto con límite de tiempo, y los miembros del equipo no podrán dominar las decisiones escritas por adelantado con vistas personalizadas en poco tiempo. Es entonces cuando llega la solución más simple y lógica al problema: no presumir, usar herramientas estándar de Android,los tipos inteligentes están sentados allí, todo estará en chocolate (bueno, o en otros dulces de Android).
Pero no es tan simple, aquí es donde conozco de cerca la magia de tipos como CoordinatorLayout, BottomSheetBehavior, o más bien con un error que los desarrolladores pasaron por alto cuando los escribieron. El artículo describirá el proceso de identificación de un error relacionado con el desplazamiento anidado dentro de los componentes de la vista con el comportamiento de BottomSheetBehavior, así como las formas de resolverlo.

Primera cita
Mi tarea era hacer una funcionalidad simple con un desplazamiento anidado, que consistía en un RecyclerView horizontal, ScrollView y CoordinatorLayout, es él quien permite, guiado por las vistas anidadas de Behavior, realizar tales acrobacias aéreas como cambios arbitrarios suaves de varias vistas.

: CoordinatorLayout, , RecyclerView, android.material BottomSheetBehavior, , . item- , TextView — item-, NestedScrollView, ( TextView).

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:nestedScrollingEnabled="false"
app:behavior_hideable="false"
app:behavior_peekHeight="80dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
recycler_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorControlHighlight"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="@color/colorAccent"
android:text="@string/head_text"
android:textAlignment="center"
android:textColor="@android:color/white"
android:textSize="24sp" />
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/item_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textSize="24sp" />
</androidx.core.widget.NestedScrollView>
</LinearLayout>
NestedScrollView? , Android, , ScrollView, , , , .
layout, RecyclerView.Adapter RecyclerView.ViewHolder, , , ...
RecyclerViewAdapter.kt
import android.content.Context
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
class RecyclerViewAdapter(private val context: Context, private val itemList: List<ItemModel>) :
RecyclerView.Adapter<RecyclerViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
return RecyclerViewHolder.create(context, parent)
}
override fun getItemCount() = itemList.size
override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
holder.bind(itemList[position])
}
}
RecyclerViewHolder.kt
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.widget.NestedScrollView
import androidx.recyclerview.widget.RecyclerView
class RecyclerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(model: ItemModel) {
val textView: TextView = itemView.findViewById(R.id.item_text_view)
val scrollView = itemView.findViewById(R.id.scroll_view) as NestedScrollView
textView.text = ""
for (i in 1..50) {
textView.append(model.number.toString() + "\n")
}
}
companion object {
fun create(context: Context, parent: ViewGroup): RecyclerViewHolder {
return RecyclerViewHolder(
LayoutInflater.from(context).inflate(
R.layout.recycler_item,
parent,
false
)
)
}
}
}
ItemModel.kt
data class ItemModel(val number: Int)
MainActvity.kt
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private val itemList = arrayListOf<ItemModel>().apply {
for (i in 0..100) {
this.add(ItemModel(i))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recycler_view.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
recycler_view.adapter = RecyclerViewAdapter(this, itemList)
}
}
, , , … ? , NestedScrollView , ? recycler item, … … item- , , item-. RecyclerView, — View, Recycler , . , - NestedScrollView, .
, — item- . onScrollChange:
scrollView.setOnScrollChangeListener { v: NestedScrollView?, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int ->
Log.i("TAG", "OnScrollChange")
}
item , … , - , :
scrollView.setOnTouchListener { view, motionEvent ->
Log.i("TAG", motionEvent.toString())
false
}
, , , , , , , , , , - - , , .

, , . ? , NestedScrollView OnTouch. item-:
itemView.setOnTouchListener { view, motionEvent ->
Log.i("TAG", motionEvent.toString())
false
}
. , recycler, , . : CoordinatorLayout, — item-, .
, CoordinatorLayout, , , — CoordinatorLayout, , FrameLayout. , RecyclerView material — BottomSheetBehavior. , , .
BottomSheetBehavior
BottomSheetBehavior — java , CoordinatorLayout.Behavior, (CoordinatorLayout). , , . ? BottomSheetBehavior TestBehavior, , , -. RecyclerView .
TestBehavior.java
import android.content.Context;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
public class TestBehavior<V extends View> extends BottomSheetBehavior<V> {
public TestBehavior(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent event) {
Log.i("TAG", "onInterceptTouchEvent");
return super.onInterceptTouchEvent(parent, child, event);
}
@Override
public void onRestoreInstanceState(
@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull Parcelable state) {
Log.i("TAG", "onRestoreInstanceState");
super.onRestoreInstanceState(parent, child, state);
}
@Override
public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams layoutParams) {
Log.i("TAG", "onAttachedToLayoutParams");
super.onAttachedToLayoutParams(layoutParams);
}
@Override
public void onDetachedFromLayoutParams() {
Log.i("TAG", "onDetachedFromLayoutParams");
super.onDetachedFromLayoutParams();
}
@Override
public boolean onLayoutChild(
@NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection) {
Log.i("TAG", "onLayoutChild");
return super.onLayoutChild(parent, child, layoutDirection);
}
@Override
public boolean onTouchEvent(
@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent event) {
Log.i("TAG", "onTouchEvent " + event.toString());
return super.onTouchEvent(parent, child, event);
}
@Override
public boolean onStartNestedScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View directTargetChild,
@NonNull View target,
int axes,
int type) {
Log.i("TAG", "onStartNestedScroll");
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
}
@Override
public void onNestedPreScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View target,
int dx,
int dy,
@NonNull int[] consumed,
int type) {
Log.i("TAG", "onNestedPreScroll");
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
@Override
public void onStopNestedScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View target,
int type) {
Log.i("TAG", "onStopNestedScroll");
super.onStopNestedScroll(coordinatorLayout, child, target, type);
}
@Override
public void onNestedScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View target,
int dxConsumed,
int dyConsumed,
int dxUnconsumed,
int dyUnconsumed,
int type,
@NonNull int[] consumed) {
Log.i("TAG", "onNestedScroll");
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
}
@Override
public boolean onNestedPreFling(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View target,
float velocityX,
float velocityY) {
Log.i("TAG", "onNestedPreFling");
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
}
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:nestedScrollingEnabled="false"
app:behavior_hideable="false"
app:behavior_peekHeight="80dp"
app:layout_behavior=".TestBehavior" />
. item- : onInterceptTouchEvent, onStartNestedScroll, onNestedPreScroll, onNestedScroll, onStopNestedScroll. : onInterceptTouchEvent, onStartNestedScroll, onNestedPreScroll, onStopNestedScroll. , onNestedPreScroll . onNestedPreScroll BottomSheetBehavior , , , :
View scrollingChild = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null;
if (target != scrollingChild) {
return;
}
, nestedScrollingChildRef? View, View, CoordinatorLayout View, .
@Nullable WeakReference<View> nestedScrollingChildRef;
, onLayoutChild:
@Override
public boolean onLayoutChild(
@NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection) {
...
nestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
return true;
}
Recycler, , item, View, BottomSheetBehavior, , , View , , — . , .
, ? - view Recycler-, , , BottomSheetBehavior , , : (: , ) Java Reflection API. , .
Java Reflection API

Java Reflection API, TestBeahvior, Runtime , , , onStartNestedScroll, onNestedPreScroll.
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
public class TestBeahvior<V extends View> extends BottomSheetBehavior<V> {
public TestBeahvior(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onStartNestedScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View directTargetChild,
@NonNull View target,
int axes,
int type) {
try {
Field nestedScrollingChildRefField = this.getClass().getSuperclass().getDeclaredField("nestedScrollingChildRef");
nestedScrollingChildRefField.setAccessible(true);
nestedScrollingChildRefField.set(this, new WeakReference<>(target));
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
}
}
Aquí todo es simple, copiamos la clase BottomSheetBehavior, agregamos el método privado refreshNestedScrollingChildRef, que actualizará el contenido del objeto de enlace, llame a este método en el método onStartNestedScroll. Hecho, todo funciona.
private void refreshNestedScrollingChildRef(View view) {
nestedScrollingChildRef = new WeakReference<>(view);
}
@Override
public boolean onStartNestedScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View directTargetChild,
@NonNull View target,
int axes,
int type) {
refreshNestedScrollingChildRef(target);
...
}
Conclusión
Los desarrolladores de Android proporcionan muchas herramientas geniales y que funcionan bien, pero eso no significa que todas funcionen perfectamente. Siempre puede encontrar un caso en el que la herramienta estándar no funcionará, como en mi caso.
¡Todos los casos interesantes!