Provavelmente todo mundo, admirando as belas janelas, barras de ferramentas e outras visualizações que se movem magicamente suavemente em diferentes direções, pensou em como funciona, provavelmente até leu algo sobre o CoordianatorLayout, sobre vários Behaivor, que permitem criar literalmente mágica nas visualizações do Android. Obviamente, você pode escrever visualizações personalizadas com o comportamento desejado, que pode ser limitado apenas pela sua imaginação ou pelo seu conhecimento no desenvolvimento do Android. Mas, além disso, há outra limitação: tempo, você não escreverá visualizações personalizadas em um hackathon, em um projeto com prazo determinado, e as decisões escritas previamente com visualizações personalizadas não poderão ser dominadas pelos membros da equipe em pouco tempo. É então que surge a solução mais simples e lógica para o problema - não se mostre, use as ferramentas padrão do Android,caras espertos estão sentados lá, tudo estará em chocolate (bem, ou em outros doces para Android).
Mas nem tudo é tão simples, é aqui que meu íntimo conhecimento da magia de caras como CoordinatorLayout, BottomSheetBehavior começa, ou melhor, com um bug que os desenvolvedores ignoraram quando os escreveram. O artigo descreverá o processo de identificação de um bug relacionado à rolagem aninhada dentro dos componentes da exibição com o comportamento BottomSheetBehavior, bem como maneiras de resolvê-lo.

Primeiro encontro
Minha tarefa era tornar a funcionalidade simples com um pergaminho aninhado, consistindo de um RecyclerView horizontal, ScrollView e CoordinatorLayout, é ele quem permite, guiado pelas visualizações aninhadas do Behavior, realizar acrobacias como mudanças arbitrárias e suaves de várias visualizações.

: 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);
}
}
Tudo é simples aqui, copie a classe BottomSheetBehavior, adicione o método privado refreshNestedScrollingChildRef, que atualizará o conteúdo do objeto de link, chame esse método no método onStartNestedScroll. Feito, tudo 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);
...
}
Conclusão
Os desenvolvedores do Android fornecem muitas ferramentas legais e que funcionam bem, mas isso não significa que elas funcionem perfeitamente. Você sempre pode encontrar um caso em que a ferramenta padrão não funcione, como no meu caso.
Todos os casos interessantes!