Mungkin semua orang, mengagumi jendela yang indah, bilah alat, dan tampilan lain secara ajaib bergerak mulus ke arah yang berbeda, berpikir tentang cara kerjanya, bahkan mungkin membaca sesuatu tentang CoordianatorLayout, tentang berbagai Behaivor, yang memungkinkan Anda untuk membuat keajaiban secara harfiah pada tampilan android. Tentu saja, Anda dapat menulis tampilan khusus dengan perilaku yang diinginkan, yang hanya dapat dibatasi oleh imajinasi Anda atau pengetahuan Anda dalam pengembangan Android. Tapi selain itu, ada batasan lain - waktu, Anda tidak akan menulis pandangan khusus pada hackathon, dalam proyek terikat waktu, dan keputusan yang ditulis sebelumnya dengan pandangan kustom tidak dapat dikuasai oleh anggota tim dalam waktu singkat. Saat itulah solusi paling sederhana dan paling logis untuk masalah datang - jangan pamer, gunakan alat Android standar,orang-orang pintar duduk di sana, semuanya akan menjadi cokelat (baik, atau di permen android lainnya).
Tapi tidak semuanya begitu sederhana, di sinilah kenalan dekat saya dengan keajaiban orang-orang seperti CoordinatorLayout, BottomSheetBehavior dimulai, atau lebih tepatnya dengan bug yang diabaikan pengembang ketika mereka menulisnya. Artikel ini akan menjelaskan proses mengidentifikasi bug yang terkait dengan pengguliran bersarang di dalam komponen tampilan dengan perilaku BottomSheetBehavior, serta cara untuk mengatasinya.

Pertemuan pertama
Tugas saya adalah membuat fungsionalitas sederhana dengan scroll bersarang, yang terdiri dari RecyclerView horizontal, ScrollView, dan CoordinatorLayout, dialah yang memungkinkan, dipandu oleh pandangan bersarang Behavior, untuk melakukan aerobatik seperti perpindahan acak dari berbagai tampilan.

: 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);
}
}
Semuanya sederhana di sini, salin kelas BottomSheetBehavior, tambahkan metode pribadi refreshNestedScrollingChildRef, yang akan memperbarui konten objek tautan, panggil metode ini dalam metode onStartNestedScroll. Selesai, semuanya bekerja.
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);
...
}
Kesimpulan
Pengembang Android menyediakan banyak alat keren dan berfungsi dengan baik, tetapi itu tidak berarti bahwa semuanya akan bekerja dengan sempurna. Anda selalu dapat menemukan kasus di mana alat standar tidak akan berfungsi, seperti dalam kasus saya.
Semua kasus menarik!