من المحتمل أن الجميع ، الذين يعجبون بالنوافذ الجميلة وأشرطة الأدوات والمشاهدات الأخرى التي تتحرك بسلاسة في اتجاهات مختلفة بسلاسة ، فكروا في كيفية عملها ، وربما قرأوا شيئًا عن منسق Layout ، حول Behaivor المختلفة ، والتي تسمح لك بإنشاء سحر حرفيًا على عروض Android. بالطبع ، يمكنك كتابة طرق عرض مخصصة بالسلوك المطلوب ، والذي يمكن أن يقتصر فقط على خيالك أو معرفتك في تطوير Android. ولكن بالإضافة إلى ذلك ، هناك قيود أخرى - الوقت ، لن تكتب طرق عرض مخصصة على هاكاثون ، في مشروع محدد بوقت ، ولا يمكن إتقان القرارات المكتوبة مقدمًا مع طرق العرض المخصصة من قبل أعضاء الفريق في وقت قصير. عندها يأتي الحل الأبسط والأكثر منطقية للمشكلة - لا تظهر ، استخدم أدوات Android القياسية ،يجلس الرجال الأذكياء هناك ، كل شيء سيكون في الشوكولاتة (جيدًا ، أو في حلويات Android الأخرى).
لكن ليس كل شيء بسيطًا جدًا ، هذا هو المكان الذي أعرفه عن قرب بسحر هؤلاء الرجال مثل منسق Layout ، يبدأ BottomSheetBehavior ، أو بالأحرى مع خطأ تجاهله المطورون عندما كتبوه. ستصف المقالة عملية تحديد الخطأ المتعلق بالتمرير المتداخل داخل مكونات العرض بسلوك BottomSheetBehavior ، بالإضافة إلى طرق حلها.

أول لقاء
كانت مهمتي هي إنشاء وظيفة بسيطة من خلال تمرير متداخل ، يتكون من RecyclerView أفقي ، و ScrollView ، و CoordinatorLayout ، وهو هو الذي يسمح ، مسترشدًا بآراء Behavior المتداخلة ، بتنفيذ مثل هذه الأكروبات مثل التحولات التعسفية السلسة لمختلف العروض.

: 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);
}
}
كل شيء بسيط هنا ، انسخ فئة BottomSheetBehavior ، وأضف الطريقة الخاصة updatesNestedScrollingChildRef ، والتي ستقوم بتحديث محتويات كائن الارتباط ، واستدعاء هذه الطريقة في طريقة onStartNestedScroll. فعل كل شيء يعمل.
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);
...
}
استنتاج
يوفر مطورو Android العديد من الأدوات الرائعة التي تعمل بشكل جيد ، ولكن هذا لا يعني أنهم سيعملون بشكل مثالي. يمكنك دائمًا العثور على حالة لن تعمل فيها الأداة القياسية ، كما هو الحال في حالتي.
جميع الحالات المثيرة للاهتمام!