CollapsingToolbarLayout ne reconnaît pas le défilement


93

J'ai créé un simple CollapsingToolbarLayout et cela fonctionne comme un charme. Mon problème est que si j'essaie d'utiliser un fling scroll sur le nestedscrollview , il s'arrête juste lorsque je relâche mon doigt. Le défilement normal fonctionne comme il se doit.

Mon code d'activités est inchangé => activité vide générée automatiquement . (Je viens de cliquer sur créer une nouvelle activité vide dans le studio Android et encore modifié le XML).

J'ai lu ici, que les gestes de défilement sur l'image vue elle-même sont bogués, mais pas, que le défilement lui-même est bogué: voir ici .

J'ai essayé d'activer le "défilement fluide" à travers le code java. Il semble que si je fais défiler suffisamment loin pour que la vue de l'image ne soit plus visible, les gestes de lancement sont alors reconnus.

TLDR: Pourquoi le geste fling ne fonctionne-t-il pas tant que l'image vue est visible? Mon code XML ressemble à ceci:

    <android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/profile_app_bar_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        android:fitsSystemWindows="true">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/profile_collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:contentScrim="?attr/colorPrimary"
            app:expandedTitleMarginStart="48dp"
            app:expandedTitleMarginEnd="64dp"
            android:fitsSystemWindows="true">

            <ImageView
                android:id="@+id/image"
                android:layout_width="match_parent"
                android:layout_height="420dp"
                android:scaleType="centerCrop"
                android:fitsSystemWindows="true"
                android:src="@drawable/headerbg"
                android:maxHeight="192dp"

                app:layout_collapseMode="parallax"/>

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                app:layout_collapseMode="pin" />

        </android.support.design.widget.CollapsingToolbarLayout>

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

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        app:layout_anchor="@id/profile_app_bar_layout"
        app:layout_anchorGravity="bottom|right|end"
        android:layout_height="@dimen/fab_size_normal"
        android:layout_width="@dimen/fab_size_normal"
        app:elevation="2dp"
        app:pressedTranslationZ="12dp"
        android:layout_marginRight="8dp"
        android:layout_marginEnd="8dp"/>

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/profile_content_scroll"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_gravity="fill_vertical"
        android:minHeight="192dp"
        android:overScrollMode="ifContentScrolls"
        >

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/LoremIpsum"/>

        </RelativeLayout>

    </android.support.v4.widget.NestedScrollView>

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

Intéressant, j'ai enregistré les événements tactiles sur la vue de défilement imbriquée pendant un fling affecté. Ça devient ACTION_DOWN y=98 -> ACTION_MOVE y=-40 -> ACTION_MOVE y=-33 -> ACTION_UP y=97. Il semble que le dernier événement tactile se signale à tort comme étant à côté du premier.
Xiao

Quelle version de la bibliothèque de support de conception utilisez-vous?
Radu Topor

écrasez-vous des événements tactiles? essayez de régler nestedScrollView.getParent().requestDisallowInterceptTouchEvent(true);votre vue de défilement imbriquée
Bhargav

Réponses:


20

J'ai eu exactement le même problème avec CollapsingToolbarLayout avec ImageView à l' intérieur et NestedScrollView . Le défilement fling s'arrête lorsque le doigt est relâché.

Cependant, j'ai remarqué quelque chose d'étrange. Si vous commencez à faire défiler avec votre doigt d'une vue avec OnClickListener (par exemple Button), le défilement fling fonctionne parfaitement.

Ainsi je l'ai corrigé avec une solution étrange. Définissez OnClickListener (qui ne fait rien) sur l'enfant direct de NestedScrollView . Ensuite, cela fonctionne parfaitement!

<android.support.v4.widget.NestedScrollView 
   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:layout_width="match_parent"
   android:layout_height="match_parent"
   app:layout_behavior="@string/appbar_scrolling_view_behavior">

  <LinearLayout
      android:id="@+id/content_container"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:orientation="vertical">

    <!-- Page Content -->

  </LinearLayout>

</android.support.v4.widget.NestedScrollView>

Donner un identifiant à l'enfant direct (LinearLayout) et définir OnClickListener dans Activity

ViewGroup mContentContainer = (ViewGroup) findViewById(R.id.content_container);    
mContentContainer.setOnClickListener(this);

@Override
public void onClick(View view) {
    int viewId = view.getId();
}

Remarques:

Testé à l'aide de la bibliothèque de conception de support 25.0.1

CollapsingToolbarLayout with scrollFlags = "scroll | enterAlwaysCollapsed"


AOSP devrait être patché avec cette excellente solution: D
déviant

Devrait obtenir plus de voix lol. Btw cela a rendu le CollapsingToolbarLayout très sensible au défilement, mais meilleur que le comportement interrompu actuel.
Ahmad Fadli

1
c'était trop beau pour être vrai alors je l'ai essayé et ça ne fonctionne pas pour moi
Gilbert Mendoza

c'est la solution la plus folle que j'ai essayée jusqu'ici dans SO.
Techfist

: D grande observation @jinang !!
Srichakradhar

10

Je sais que cette question a été posée il y a plus d'un an, mais ce problème ne semble toujours pas être résolu dans les bibliothèques Support / Design. Vous pouvez marquer ce problème pour qu'il monte plus haut dans la file d'attente prioritaire.

Cela dit, j'ai essayé la plupart des solutions publiées pour cela, y compris celle de patrick-iv sans succès. La seule façon dont j'ai pu me rendre au travail était d'imiter le fling et de l'appeler par programme si un certain ensemble de conditions était détecté onPreNestedScroll(). Dans les quelques heures de mon débogage, j'ai remarqué que le onNestedFling()n'a jamais été appelé sur un fling vers le haut (défilement vers le bas) et semblait être consommé prématurément. Je ne peux pas dire avec 100% de certitude que cela fonctionnera pour 100% des implémentations, mais cela fonctionne assez bien pour mes utilisations, alors j'ai fini par me contenter de cela, même si c'est assez piraté et certainement pas ce que je voulais faire.

public class NestedScrollViewBehavior extends AppBarLayout.Behavior {

    // Lower value means fling action is more easily triggered
    static final int MIN_DY_DELTA = 4;
    // Lower values mean less velocity, higher means higher velocity
    static final int FLING_FACTOR = 20;

    int mTotalDy;
    int mPreviousDy;
    WeakReference<AppBarLayout> mPreScrollChildRef;

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                                  View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        // Reset the total fling delta distance if the user starts scrolling back up
        if(dy < 0) {
            mTotalDy = 0;
        }
        // Only track move distance if the movement is positive (since the bug is only present
        // in upward flings), equal to the consumed value and the move distance is greater
        // than the minimum difference value
        if(dy > 0 && consumed[1] == dy && MIN_DY_DELTA < Math.abs(mPreviousDy - dy)) {
            mPreScrollChildRef = new WeakReference<>(child);
            mTotalDy += dy * FLING_FACTOR;
        }
        mPreviousDy = dy;
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
                                       View directTargetChild, View target, int nestedScrollAxes) {
        // Stop any previous fling animations that may be running
        onNestedFling(parent, child, target, 0, 0, false);
        return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
    }

    @Override
    public void onStopNestedScroll(CoordinatorLayout parent, AppBarLayout abl, View target) {
        if(mTotalDy > 0 && mPreScrollChildRef != null && mPreScrollChildRef.get() != null) {
            // Programmatically trigger fling if all conditions are met
            onNestedFling(parent, mPreScrollChildRef.get(), target, 0, mTotalDy, false);
            mTotalDy = 0;
            mPreviousDy = 0;
            mPreScrollChildRef = null;
        }
        super.onStopNestedScroll(parent, abl, target);
    }
}

Et appliquez-le à l'AppBar

AppBarLayout scrollView = (AppBarLayout)findViewById(R.id.appbar);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams)scrollView.getLayoutParams();
params.setBehavior(new NestedScrollViewBehavior());

Démo CheeseSquare: Avant Après


C'est en effet mieux que rien, mais pas tout à fait ce à quoi un utilisateur Android expérimenté pourrait s'attendre. Merci d'avoir lié le problème, je l'ai ajouté.
Raphael Royer-Rivard

Il a enterAlwaysfallu supprimer layout_ScrollFlag pour que cela fonctionne, mais fonctionne bien maintenant
Alexandre G

3

J'ai essayé la solution Floofer mais ce n'était toujours pas assez bon pour moi. Je suis donc venu avec une meilleure version de son comportement. L'AppBarLayout se développe maintenant et se réduit en douceur lors du lancement.

Remarque: j'ai utilisé la réflexion pour me frayer un chemin, donc cela peut ne pas fonctionner parfaitement avec une version de la bibliothèque Android Design différente de 25.0.0.

public class SmoothScrollBehavior extends AppBarLayout.Behavior {
    private static final String TAG = "SmoothScrollBehavior";
    //The higher this value is, the faster the user must scroll for the AppBarLayout to collapse by itself
    private static final int SCROLL_SENSIBILITY = 5;
    //The real fling velocity calculation seems complex, in this case it is simplified with a multiplier
    private static final int FLING_VELOCITY_MULTIPLIER = 60;

    private boolean alreadyFlung = false;
    private boolean request = false;
    private boolean expand = false;
    private int velocity = 0;
    private int nestedScrollViewId;

    public SmoothScrollBehavior(int nestedScrollViewId) {
        this.nestedScrollViewId = nestedScrollViewId;
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                                  View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        if(Math.abs(dy) >= SCROLL_SENSIBILITY) {
            request = true;
            expand = dy < 0;
            velocity = dy * FLING_VELOCITY_MULTIPLIER;
        } else {
            request = false;
        }
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
                                       View directTargetChild, View target, int nestedScrollAxes) {
        request = false;
        return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes);
    }

    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout appBarLayout, View target) {
        if(request) {
            NestedScrollView nestedScrollView = (NestedScrollView) coordinatorLayout.findViewById(nestedScrollViewId);
            if (expand) {
                //No need to force expand if it is already ready expanding
                if (nestedScrollView.getScrollY() > 0) {
                    int finalY = getPredictedScrollY(nestedScrollView);
                    if (finalY <= 0) {
                        //since onNestedFling does not work to expand the AppBarLayout, we need to manually expand it
                        expandAppBarLayoutWithVelocity(coordinatorLayout, appBarLayout, velocity);
                    }
                }
            } else {
                //onNestedFling will collapse the AppBarLayout with an animation time relative to the velocity
                onNestedFling(coordinatorLayout, appBarLayout, target, 0, velocity, true);

                if(!alreadyFlung) {
                    //TODO wait for AppBarLayout to be collapsed before scrolling for even smoother visual
                    nestedScrollView.fling(velocity);
                }
            }
        }
        alreadyFlung = false;
        super.onStopNestedScroll(coordinatorLayout, appBarLayout, target);
    }

    private int getPredictedScrollY(NestedScrollView nestedScrollView) {
        int finalY = 0;
        try {
            //With reflection, we can get the ScrollerCompat from the NestedScrollView to predict where the scroll will end
            Field scrollerField = nestedScrollView.getClass().getDeclaredField("mScroller");
            scrollerField.setAccessible(true);
            Object object = scrollerField.get(nestedScrollView);
            ScrollerCompat scrollerCompat = (ScrollerCompat) object;
            finalY = scrollerCompat.getFinalY();
        } catch (Exception e ) {
            e.printStackTrace();
            //If the reflection fails, it will return 0, which means the scroll has reached top
            Log.e(TAG, "Failed to get mScroller field from NestedScrollView through reflection. Will assume that the scroll reached the top.");
        }
        return finalY;
    }

    private void expandAppBarLayoutWithVelocity(CoordinatorLayout coordinatorLayout, AppBarLayout appBarLayout, float velocity) {
        try {
            //With reflection, we can call the private method of Behavior that expands the AppBarLayout with specified velocity
            Method animateOffsetTo = getClass().getSuperclass().getDeclaredMethod("animateOffsetTo", CoordinatorLayout.class, AppBarLayout.class, int.class, float.class);
            animateOffsetTo.setAccessible(true);
            animateOffsetTo.invoke(this, coordinatorLayout, appBarLayout, 0, velocity);
        } catch (Exception e) {
            e.printStackTrace();
            //If the reflection fails, we fall back to the public method setExpanded that expands the AppBarLayout with a fixed velocity
            Log.e(TAG, "Failed to get animateOffsetTo method from AppBarLayout.Behavior through reflection. Falling back to setExpanded.");
            appBarLayout.setExpanded(true, true);
        }
    }

    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY) {
        alreadyFlung = true;
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }
}

Pour l'utiliser, définissez un nouveau comportement sur votre AppBarLayout.

AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.app_bar);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) appBarLayout.getLayoutParams();
params.setBehavior(new SmoothScrollBehavior(R.id.nested_scroll_view));

Votre classe demande un int dans son constructeur, mais dans le code, vous n'envoyez rien au constructeur
bluesummers

Mon mauvais, je l'ai ajouté.
Raphael Royer-Rivard

Cela semble bon, cela rend le défilement fluide, mais j'ai une question, est-il possible de laisser NestedScrollView défiler dans AppBarLayout une fois que AppBarLayout atteint le haut, et aussi, lorsque je fais défiler vers le bas, AppBarLayout apparaît enfin, lorsque le NestedScrollView complètement défilé, puis AppBarLayout commence à se développer.
Zijian Wang

@ZijianWang Veuillez m'expliquer ce que vous entendez par «faites défiler dans AppBarLayout» et je ne comprends pas non plus votre deuxième question, pouvez-vous la reformuler?
Raphael Royer-Rivard

0

Cette réponse a résolu ce problème pour moi. Créez une personnalisation AppBarLayout.Behaviorcomme celle-ci:

public final class FlingBehavior extends AppBarLayout.Behavior {
    private static final int TOP_CHILD_FLING_THRESHOLD = 3;
    private boolean isPositive;

    public FlingBehavior() {
    }

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

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
            velocityY = velocityY * -1;
        }
        if (target instanceof RecyclerView && velocityY < 0) {
            final RecyclerView recyclerView = (RecyclerView) target;
            final View firstChild = recyclerView.getChildAt(0);
            final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);
            consumed = childAdapterPosition > TOP_CHILD_FLING_THRESHOLD;
        }
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        isPositive = dy > 0;
    }
}

et ajoutez-le AppBarLayoutcomme ceci:

<android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        ...
        app:layout_behavior="com.example.test.FlingBehavior">

1
Cela n'a pas fonctionné car le problème dans l'autre question était le RecyclerView qui n'est pas utilisé ici.
Felix Edelmann

0

Je publie simplement ceci ici pour que les autres ne le manquent pas dans les commentaires. La réponse de Jinang fonctionne à merveille, mais bravo à AntPachon pour avoir souligné une méthode beaucoup plus simple pour la même chose. Au lieu d'implémenter une OnClickméthode sur le Child of the NestedScrollViewprogramme, une meilleure façon est de définirclickable=true le xml pour l'enfant.

(En utilisant le même exemple que celui de Jinang )

<android.support.v4.widget.NestedScrollView 
   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:layout_width="match_parent"
   android:layout_height="match_parent"
   app:layout_behavior="@string/appbar_scrolling_view_behavior">

  <LinearLayout
      android:id="@+id/content_container"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:orientation="vertical"
      android:clickable="true" >                  <!-- new -->

    <!-- Page Content -->

  </LinearLayout>

</android.support.v4.widget.NestedScrollView>

-1

Dans le code :https://android.googlesource.com/platform/frameworks/support/+/master/core-ui/java/android/support/v4/widget/NestedScrollView.java#834

       case MotionEvent.ACTION_UP:
            if (mIsBeingDragged) {
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker,
                        mActivePointerId);
                if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                    flingWithNestedDispatch(-initialVelocity);
                } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                        getScrollRange())) {
                    ViewCompat.postInvalidateOnAnimation(this);
                }
            }
            mActivePointerId = INVALID_POINTER;
            endDrag();
            break;

Quand j'utilise un fling scroll sur le NestedScrollView parfois "mIsBeingDragged = false", donc NestedScrollView ne dispense pas d'événement fling.

Lorsque je supprime la if (mIsBeingDragged)déclaration.

 case MotionEvent.ACTION_UP:
        //if (mIsBeingDragged) {
            final VelocityTracker velocityTracker = mVelocityTracker;
            velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
            int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker,
                    mActivePointerId);
            if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                flingWithNestedDispatch(-initialVelocity);
            } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                    getScrollRange())) {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        //}
        mActivePointerId = INVALID_POINTER;
        endDrag();
        break;

il n'y aura aucun problème. Mais je ne sais pas quels autres problèmes graves seront causés


Ajoutez plus de détails pour rendre la réponse facile à comprendre, Vous avez écrit Quand je supprime le si ... d'où vous parlez pour supprimer si ?
Devendra Singh
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.