Hoffe auf das SDK und mache nichts falsch: Das Problem verschachtelter Schriftrollen im BottomSheetBehavior

Wahrscheinlich hat jeder, der die schönen Fenster, Symbolleisten und anderen Ansichten bewundert, die sich auf magische Weise reibungslos in verschiedene Richtungen bewegen, darüber nachgedacht, wie es funktioniert, und wahrscheinlich sogar etwas über CoordianatorLayout gelesen, über verschiedene Behaivor, mit denen Sie in Android-Ansichten buchstäblich Magie erzeugen können. Natürlich können Sie benutzerdefinierte Ansichten mit dem gewünschten Verhalten schreiben, das nur durch Ihre Vorstellungskraft oder Ihr Wissen in der Android-Entwicklung eingeschränkt werden kann. Abgesehen davon gibt es noch eine weitere Einschränkung: Sie schreiben keine benutzerdefinierten Ansichten für einen Hackathon in einem zeitgebundenen Projekt, und Entscheidungen, die im Voraus mit benutzerdefinierten Ansichten getroffen wurden, können von Teammitgliedern nicht in kurzer Zeit gemeistert werden. Dann kommt die einfachste und logischste Lösung für das Problem - nicht angeben, Standard-Android-Tools verwenden,kluge Kerle sitzen da, alles wird in Schokolade sein (na ja, oder in anderen Android-Süßigkeiten).


Aber nicht alles ist so einfach. Hier beginnt meine enge Bekanntschaft mit der Magie von Leuten wie CoordinatorLayout, BottomSheetBehavior oder vielmehr mit einem Fehler, den Entwickler beim Schreiben übersehen haben. Der Artikel beschreibt den Prozess des Identifizierens eines Fehlers im Zusammenhang mit verschachteltem Scrollen in Ansichtskomponenten mit dem Verhalten von BottomSheetBehavior sowie Möglichkeiten, ihn zu beheben.


Koordinator-Layout-Animation


Erstes Treffen


Meine Aufgabe war es, mit einer verschachtelten Schriftrolle, die aus einer horizontalen RecyclerView, einer ScrollView und einem CoordinatorLayout besteht, einfache Funktionen zu erstellen.


Zweck Animation


: 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


Reflexionsmeme


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


Hier ist alles einfach, kopieren Sie die BottomSheetBehavior-Klasse, fügen Sie die private Methode refreshNestedScrollingChildRef hinzu, die den Inhalt des Link-Objekts aktualisiert, und rufen Sie diese Methode in der onStartNestedScroll-Methode auf. Fertig, alles funktioniert.


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

Fazit


Android-Entwickler bieten viele coole und gut funktionierende Tools, aber das bedeutet nicht, dass sie alle perfekt funktionieren. Sie können immer einen Fall finden, in dem das Standardwerkzeug nicht funktioniert, wie in meinem Fall.


Alle interessanten Fälle!

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


All Articles