Espérons sur le SDK et ne vous trompez pas: le problème des parchemins imbriqués dans le BottomSheetBehavior

Probablement tout le monde, admirant les belles fenêtres, barres d'outils et autres vues se déplaçant comme par magie dans différentes directions, a réfléchi à son fonctionnement, a probablement même lu quelque chose sur CoordianatorLayout, sur divers Behaivor, qui vous permettent de créer littéralement de la magie sur les vues Android. Bien sûr, vous pouvez écrire des vues personnalisées avec le comportement souhaité, qui ne peut être limité que par votre imagination ou vos connaissances en développement Android. Mais en plus de cela, il y a une autre limitation - le temps, vous n'écrirez pas de vues personnalisées sur un hackathon, dans un projet limité dans le temps, et les décisions écrites à l'avance avec des vues personnalisées ne peuvent pas être maîtrisées par les membres de l'équipe en peu de temps. C'est alors que la solution la plus simple et la plus logique au problème vient - ne pas se montrer, utiliser des outils Android standard,les gars intelligents sont assis là, tout sera en chocolat (enfin, ou dans d'autres bonbons androïdes).


Mais tout n'est pas si simple, c'est là que commence ma connaissance étroite de la magie de gars comme CoordinatorLayout, BottomSheetBehavior, ou plutôt avec un bug que les développeurs ont négligé lorsqu'ils les ont écrits. L'article décrira le processus d'identification d'un bogue lié au défilement imbriqué à l'intérieur des composants de la vue avec le comportement de BottomSheetBehavior, ainsi que les moyens de le résoudre.


Coordinateur Animation de mise en page


Première rencontre


Ma tâche consistait à créer des fonctionnalités simples avec un défilement imbriqué, composé d'un RecyclerView horizontal, d'un ScrollView et d'un CoordinatorLayout, c'est lui qui, guidé par les vues imbriquées de Behaviour, peut effectuer des acrobaties aériennes comme des déplacements arbitraires en douceur de diverses vues.


Animation de but


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


NestedScrollViewMeme


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
}

, , , , , , , , , , - - , , .



Interceptmeme


, , . ? , 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


Reflectionmeme


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);
    }
}


Tout est simple ici, copiez la classe BottomSheetBehavior, ajoutez la méthode privée refreshNestedScrollingChildRef, qui mettra à jour le contenu de l'objet lien, appelez cette méthode dans la méthode onStartNestedScroll. Terminé, tout fonctionne.


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);
        ...
    }

Conclusion


Les développeurs Android fournissent de nombreux outils sympas et qui fonctionnent bien, mais cela ne signifie pas qu'ils fonctionneront tous parfaitement. Vous pouvez toujours trouver un cas dans lequel l'outil standard ne fonctionnera pas, comme dans mon cas.


Tous les cas intéressants!

Source: https://habr.com/ru/post/undefined/


All Articles