web-dev-qa-db-fra.com

Comment éviter que CollapsingToolbarLayout ne soit cassé ou "vacillant" lors du défilement?

Contexte

Supposons que vous ayez créé une application qui a une interface utilisateur similaire à celle que vous pouvez créer via l'assistant d '"activité de défilement", mais vous souhaitez que les drapeaux de défilement aient un accrochage, en tant que tel:

<Android.support.design.widget.CollapsingToolbarLayout ... app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" >

Le problème

Il s'avère que, dans de nombreux cas, il a des problèmes de claquement. Parfois, l'interface utilisateur ne s'aligne pas en haut/en bas, ce qui fait que CollapsingToolbarLayout reste entre les deux.

Parfois, il essaie également de s'aligner dans une direction, puis décide de s'aligner dans l'autre.

Vous pouvez voir les deux problèmes sur la vidéo jointe ici .

Ce que j'ai essayé

Je pensais que c'était l'un des problèmes pour lesquels j'obtenais lorsque j'utilisais setNestedScrollingEnabled (false) sur un RecyclerView à l'intérieur, alors j'ai demandé à ce sujet ici , mais j'ai remarqué que même avec la solution et sans utiliser cette commande du tout et même lors de l'utilisation d'un simple NestedScrollView (tel que créé par l'assistant), je peux toujours remarquer ce comportement.

C'est pourquoi j'ai décidé de signaler ce problème, ici .

Malheureusement, je n'ai trouvé aucune solution de contournement pour ces bugs étranges ici sur StackOverflow.

La question

Pourquoi cela se produit, et plus important encore: comment puis-je éviter ces problèmes tout en utilisant le comportement qu'il est censé avoir?


EDIT: voici une version Kotlin améliorée de Nice de la réponse acceptée:

class RecyclerViewEx @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : RecyclerView(context, attrs, defStyle) {
    private var mAppBarTracking: AppBarTracking? = null
    private var mView: View? = null
    private var mTopPos: Int = 0
    private var mLayoutManager: LinearLayoutManager? = null

    interface AppBarTracking {
        fun isAppBarIdle(): Boolean
        fun isAppBarExpanded(): Boolean
    }

    override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?, type: Int): Boolean {
        if (mAppBarTracking == null)
            return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
        if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking!!.isAppBarIdle()
                && isNestedScrollingEnabled) {
            if (dy > 0) {
                if (mAppBarTracking!!.isAppBarExpanded()) {
                    consumed!![1] = dy
                    return true
                }
            } else {
                mTopPos = mLayoutManager!!.findFirstVisibleItemPosition()
                if (mTopPos == 0) {
                    mView = mLayoutManager!!.findViewByPosition(mTopPos)
                    if (-mView!!.top + dy <= 0) {
                        consumed!![1] = dy - mView!!.top
                        return true
                    }
                }
            }
        }
        if (dy < 0 && type == ViewCompat.TYPE_TOUCH && mAppBarTracking!!.isAppBarExpanded()) {
            consumed!![1] = dy
            return true
        }

        val returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
        if (offsetInWindow != null && !isNestedScrollingEnabled && offsetInWindow[1] != 0)
            offsetInWindow[1] = 0
        return returnValue
    }

    override fun setLayoutManager(layout: RecyclerView.LayoutManager) {
        super.setLayoutManager(layout)
        mLayoutManager = layoutManager as LinearLayoutManager
    }

    fun setAppBarTracking(appBarTracking: AppBarTracking) {
        mAppBarTracking = appBarTracking
    }

    fun setAppBarTracking(appBarLayout: AppBarLayout) {
        val appBarIdle = AtomicBoolean(true)
        val appBarExpanded = AtomicBoolean()
        appBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener {
            private var mAppBarOffset = Integer.MIN_VALUE

            override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
                if (mAppBarOffset == verticalOffset)
                    return
                mAppBarOffset = verticalOffset
                appBarExpanded.set(verticalOffset == 0)
                appBarIdle.set(mAppBarOffset >= 0 || mAppBarOffset <= -appBarLayout.totalScrollRange)
            }
        })
        setAppBarTracking(object : AppBarTracking {
            override fun isAppBarIdle(): Boolean = appBarIdle.get()
            override fun isAppBarExpanded(): Boolean = appBarExpanded.get()
        })
    }

    override fun fling(velocityX: Int, inputVelocityY: Int): Boolean {
        var velocityY = inputVelocityY
        if (mAppBarTracking != null && !mAppBarTracking!!.isAppBarIdle()) {
            val vc = ViewConfiguration.get(context)
            velocityY = if (velocityY < 0) -vc.scaledMinimumFlingVelocity
            else vc.scaledMinimumFlingVelocity
        }

        return super.fling(velocityX, velocityY)
    }
}
26
android developer

Mise à jour J'ai légèrement modifié le code pour résoudre les problèmes restants - au moins ceux que je peux reproduire. La mise à jour clé consistait à supprimer dy uniquement lorsque l'AppBar était développé ou réduit. Dans la première itération, dispatchNestedPreScroll() supprimait le défilement sans vérifier l'état de l'AppBar pour un état réduit.

D'autres changements sont mineurs et entrent dans la catégorie du nettoyage. Les blocs de code sont mis à jour ci-dessous.


Cette réponse résout le problème de la question concernant RecyclerView. L'autre réponse que j'ai donnée est toujours valable et s'applique ici. RecyclerView a les mêmes problèmes que NestedScrollView qui ont été introduits dans 26.0.0-beta2 des bibliothèques de support.

Le code ci-dessous est basé sur cette réponse à une question connexe mais inclut le correctif pour le comportement erratique de l'AppBar. J'ai supprimé le code qui corrigeait le défilement étrange car il ne semble plus être nécessaire.

AppBarTracking.Java

public interface AppBarTracking {
    boolean isAppBarIdle();
    boolean isAppBarExpanded();
}

MyRecyclerView.Java

public class MyRecyclerView extends RecyclerView {

    public MyRecyclerView(Context context) {
        this(context, null);
    }

    public MyRecyclerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    private AppBarTracking mAppBarTracking;
    private View mView;
    private int mTopPos;
    private LinearLayoutManager mLayoutManager;

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
                                           int type) {

        // App bar latching trouble is only with this type of movement when app bar is expanded
        // or collapsed. In touch mode, everything is OK regardless of the open/closed status
        // of the app bar.
        if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking.isAppBarIdle()
                && isNestedScrollingEnabled()) {
            // Make sure the AppBar stays expanded when it should.
            if (dy > 0) { // swiped up
                if (mAppBarTracking.isAppBarExpanded()) {
                    // Appbar can only leave its expanded state under the power of touch...
                    consumed[1] = dy;
                    return true;
                }
            } else { // swiped down (or no change)
                // Make sure the AppBar stays collapsed when it should.
                // Only dy < 0 will open the AppBar. Stop it from opening by consuming dy if needed.
                mTopPos = mLayoutManager.findFirstVisibleItemPosition();
                if (mTopPos == 0) {
                    mView = mLayoutManager.findViewByPosition(mTopPos);
                    if (-mView.getTop() + dy <= 0) {
                        // Scroll until scroll position = 0 and AppBar is still collapsed.
                        consumed[1] = dy - mView.getTop();
                        return true;
                    }
                }
            }
        }

        boolean returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
        // Fix the scrolling problems when scrolling is disabled. This issue existed prior
        // to 26.0.0-beta2.
        if (offsetInWindow != null && !isNestedScrollingEnabled() && offsetInWindow[1] != 0) {
            offsetInWindow[1] = 0;
        }
        return returnValue;
    }

    @Override
    public void setLayoutManager(RecyclerView.LayoutManager layout) {
        super.setLayoutManager(layout);
        mLayoutManager = (LinearLayoutManager) getLayoutManager();
    }

    public void setAppBarTracking(AppBarTracking appBarTracking) {
        mAppBarTracking = appBarTracking;
    }

    @SuppressWarnings("unused")
    private static final String TAG = "MyRecyclerView";
}

ScrollingActivity.Java

public class ScrollingActivity extends AppCompatActivity
        implements AppBarTracking {

    private MyRecyclerView mNestedView;
    private int mAppBarOffset;
    private boolean mAppBarIdle = false;
    private int mAppBarMaxOffset;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scrolling);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        mNestedView = findViewById(R.id.nestedView);

        final AppBarLayout appBar = findViewById(R.id.app_bar);

        appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                mAppBarOffset = verticalOffset;
                // mAppBarOffset = 0 if app bar is expanded; If app bar is collapsed then
                // mAppBarOffset = mAppBarMaxOffset
                // mAppBarMaxOffset is always <=0 (-AppBarLayout.getTotalScrollRange())
                // mAppBarOffset should never be > zero or less than mAppBarMaxOffset
                mAppBarIdle = (mAppBarOffset >= 0) || (mAppBarOffset <= mAppBarMaxOffset);
            }
        });

        appBar.post(new Runnable() {
            @Override
            public void run() {
                mAppBarMaxOffset = -appBar.getTotalScrollRange();
            }
        });

        findViewById(R.id.disableNestedScrollingButton).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                // If the AppBar is fully expanded or fully collapsed (idle), then disable
                // expansion and apply the patch; otherwise, set a flag to disable the expansion
                // and apply the patch when the AppBar is idle.
                setExpandEnabled(false);
            }
        });

        findViewById(R.id.enableNestedScrollingButton).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                setExpandEnabled(true);
            }
        });

        mNestedView.setAppBarTracking(this);
        mNestedView.setLayoutManager(new LinearLayoutManager(this));
        mNestedView.setAdapter(new Adapter() {
            @Override
            public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
                return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(
                        Android.R.layout.simple_list_item_1,
                        parent,
                        false)) {
                };
            }

            @SuppressLint("SetTextI18n")
            @Override
            public void onBindViewHolder(final ViewHolder holder, final int position) {
                ((TextView) holder.itemView.findViewById(Android.R.id.text1)).setText("item " + position);
            }

            @Override
            public int getItemCount() {
                return 100;
            }
        });
    }

    private void setExpandEnabled(boolean enabled) {
        mNestedView.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isAppBarExpanded() {
        return mAppBarOffset == 0;
    }

    @Override
    public boolean isAppBarIdle() {
        return mAppBarIdle;
    }

    @SuppressWarnings("unused")
    private static final String TAG = "ScrollingActivity";
}

Que se passe-t-il ici?

De la question, il était évident que la disposition ne fermait pas ou n'ouvrait pas la barre d'application comme il se doit lorsque le doigt de l'utilisateur n'est pas sur l'écran. Lors du glissement, la barre d'application se comporte comme il se doit.

Dans la version 26.0.0-beta2, de nouvelles méthodes ont été introduites - spécifiquement dispatchNestedPreScroll () avec un nouvel argument type. L'argument type spécifie si le mouvement spécifié par dx et dy est dû au fait que l'utilisateur touche l'écran ViewCompat.TYPE_TOUCH ou non ViewCompat.TYPE_NON_TOUCH .

Bien que le code spécifique à l'origine du problème n'ait pas été identifié, la solution du correctif consiste à supprimer le mouvement vertical dans dispatchNestedPreScroll() (éliminer dy) lorsque cela est nécessaire en ne laissant pas le mouvement vertical se propager. En effet, la barre d'application doit être verrouillée en place lorsqu'elle est développée et ne pourra pas commencer à se fermer tant qu'elle ne se fermera pas par un geste tactile. La barre d'application est également verrouillée lorsqu'elle est fermée jusqu'à ce que RecyclerView soit positionné au maximum et qu'il y ait suffisamment de dy pour ouvrir la barre d'application tout en effectuant un geste tactile.

Ce n'est donc pas tant une solution que le découragement de conditions problématiques.

La dernière partie du code MyRecyclerView traite d'un problème qui a été identifié dans cette question traitant des mouvements de défilement incorrects lorsque le défilement imbriqué est désactivé. C'est la partie qui vient après l'appel au super de dispatchNestedPreScroll() qui change la valeur de offsetInWindow[1]. La pensée derrière ce code est la même que celle présentée dans la réponse acceptée à la question. La seule différence est que, puisque le code de défilement imbriqué sous-jacent a changé, l'argument offsetInWindow est parfois nul. Heureusement, il semble être non nul quand c'est important, donc la dernière partie continue de fonctionner.

La mise en garde est que ce "correctif" est très spécifique à la question posée et n'est pas une solution générale. Le correctif aura probablement une durée de conservation très courte car je m'attends à ce qu'un problème aussi évident soit résolu sous peu.

9
Cheticamp

Il semble que les appels onStartNestedScroll et onStopNestedScroll puissent être réorganisés et entraîner un accrochage "instable". J'ai fait un petit hack dans AppBarLayout.Behavior. Je ne veux pas vraiment gâcher tous ces trucs en activité comme proposé par d'autres réponses.

@SuppressWarnings("unused")
public class ExtAppBarLayoutBehavior extends AppBarLayout.Behavior {

    private int mStartedScrollType = -1;
    private boolean mSkipNextStop;

    public ExtAppBarLayoutBehavior() {
        super();
    }

    public ExtAppBarLayoutBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
        if (mStartedScrollType != -1) {
            onStopNestedScroll(parent, child, target, mStartedScrollType);
            mSkipNextStop = true;
        }
        mStartedScrollType = type;
        return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type);
    }

    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target, int type) {
        if (mSkipNextStop) {
            mSkipNextStop = false;
            return;
        }
        if (mStartedScrollType == -1) {
            return;
        }
        mStartedScrollType = -1;
        // Always pass TYPE_TOUCH, because want to snap even after fling
        super.onStopNestedScroll(coordinatorLayout, abl, target, ViewCompat.TYPE_TOUCH);
    }
}

Utilisation dans la disposition XML:

<Android.support.design.widget.CoordinatorLayout>

    <Android.support.design.widget.AppBarLayout
        app:layout_behavior="com.example.ExtAppBarLayoutBehavior">

        <!-- Put here everything you usually add to AppBarLayout: CollapsingToolbarLayout, etc... -->

    </Android.support.design.widget.AppBarLayout>

    <!-- Content: recycler for example -->
    <Android.support.v7.widget.RecyclerView
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

    ...

</Android.support.design.widget.CoordinatorLayout>

Il est très probable que la cause première du problème dans le RecyclerView. N'ayez pas l'occasion de creuser plus profondément maintenant.

9
vyndor

Modifier Le code a été mis à jour pour le rendre plus conforme au code de la réponse acceptée. Cette réponse concerne NestedScrollView alors que la réponse acceptée concerne RecyclerView.


C'est un problème qui a été introduit dans la version API 26.0.0-beta2. Cela ne se produit pas sur la version bêta 1 ou avec l'API 25. Comme vous l'avez noté, cela se produit également avec l'API 26.0.0. Généralement, le problème semble être lié à la façon dont les flings et le défilement imbriqué sont traités dans la beta2. Il y a eu une réécriture majeure du défilement imbriqué (voir "Continuer le défilement" ), il n'est donc pas surprenant que ce type de problème soit apparu.

Je pense que l'excès de défilement n'est pas éliminé correctement quelque part dans NestedScrollView. La solution consiste à consommer tranquillement certains parchemins qui sont des parchemins "non tactiles" (type == ViewCompat.TYPE_NON_TOUCH) Lorsque l'AppBar est développé ou réduit. Cela arrête le rebond, permet des accrochages et, généralement, améliore le comportement de l'AppBar.

ScrollingActivity a été modifié pour suivre l'état de l'AppBar afin d'indiquer s'il est développé ou non. Un nouvel appel de classe "MyNestedScrollView" remplace dispatchNestedPreScroll() (le nouveau, voir ici ) pour manipuler la consommation de l'excès de défilement.

Le code suivant devrait suffire pour empêcher AppBarLayout de vaciller et de refuser de s'aligner. (XML devra également changer pour s'adapter à MyNestedSrollView. Ce qui suit s'applique uniquement à la prise en charge de la lib 26.0.0-beta2 et au-dessus.)

AppBarTracking.Java

public interface AppBarTracking {
    boolean isAppBarIdle();
    boolean isAppBarExpanded();
}

ScrollingActivity.Java

public class ScrollingActivity extends AppCompatActivity implements AppBarTracking {

    private int mAppBarOffset;
    private int mAppBarMaxOffset;
    private MyNestedScrollView mNestedView;
    private boolean mAppBarIdle = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AppBarLayout appBar;

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scrolling);
        final Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        appBar = findViewById(R.id.app_bar);
        mNestedView = findViewById(R.id.nestedScrollView);
        mNestedView.setAppBarTracking(this);
        appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                mAppBarOffset = verticalOffset;
            }
        });

        appBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public final void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                mAppBarOffset = verticalOffset;
                // mAppBarOffset = 0 if app bar is expanded; If app bar is collapsed then
                // mAppBarOffset = mAppBarMaxOffset
                // mAppBarMaxOffset is always <=0 (-AppBarLayout.getTotalScrollRange())
                // mAppBarOffset should never be > zero or less than mAppBarMaxOffset
                mAppBarIdle = (mAppBarOffset >= 0) || (mAppBarOffset <= mAppBarMaxOffset);
            }
        });

        mNestedView.post(new Runnable() {
            @Override
            public void run() {
                mAppBarMaxOffset = mNestedView.getMaxScrollAmount();
            }
        });
    }

    @Override
    public boolean isAppBarIdle() {
        return mAppBarIdle;
    }

    @Override
    public boolean isAppBarExpanded() {
        return mAppBarOffset == 0;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_scrolling, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @SuppressWarnings("unused")
    private static final String TAG = "ScrollingActivity";
}

MyNestedScrollView.Java

public class MyNestedScrollView extends NestedScrollView {

    public MyNestedScrollView(Context context) {
        this(context, null);
    }

    public MyNestedScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyNestedScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        setOnScrollChangeListener(new View.OnScrollChangeListener() {
            @Override
            public void onScrollChange(View view, int x, int y, int oldx, int oldy) {
                mScrollPosition = y;
            }
        });
    }

    private AppBarTracking mAppBarTracking;
    private int mScrollPosition;

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow,
                                           int type) {

        // App bar latching trouble is only with this type of movement when app bar is expanded
        // or collapsed. In touch mode, everything is OK regardless of the open/closed status
        // of the app bar.
        if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking.isAppBarIdle()
                && isNestedScrollingEnabled()) {
            // Make sure the AppBar stays expanded when it should.
            if (dy > 0) { // swiped up
                if (mAppBarTracking.isAppBarExpanded()) {
                    // Appbar can only leave its expanded state under the power of touch...
                    consumed[1] = dy;
                    return true;
                }
            } else { // swiped down (or no change)
                // Make sure the AppBar stays collapsed when it should.
                if (mScrollPosition + dy < 0) {
                    // Scroll until scroll position = 0 and AppBar is still collapsed.
                    consumed[1] = dy + mScrollPosition;
                    return true;
                }
            }
        }

        boolean returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
        // Fix the scrolling problems when scrolling is disabled. This issue existed prior
        // to 26.0.0-beta2. (Not sure that this is a problem for 26.0.0-beta2 and later.)
        if (offsetInWindow != null && !isNestedScrollingEnabled() && offsetInWindow[1] != 0) {
            Log.d(TAG, "<<<<offsetInWindow[1] forced to zero");
            offsetInWindow[1] = 0;
        }
        return returnValue;
    }

    public void setAppBarTracking(AppBarTracking appBarTracking) {
        mAppBarTracking = appBarTracking;
    }

    @SuppressWarnings("unused")
    private static final String TAG = "MyNestedScrollView";
}
5
Cheticamp