CollapsingToolbarLayout non riconosce lo scroll fling


93

Ho creato un semplice CollapsingToolbarLayout e funziona a meraviglia. Il mio problema è che se provo a usare uno scorrimento di lancio sulla nestedscrollview , si ferma solo quando rilascio il dito. Lo scorrimento normale funziona come dovrebbe.

Il codice delle mie attività è invariato => attività vuota generata automaticamente . (Ho appena fatto clic su Crea nuova attività vuota in Android Studio e ho ancora modificato l'XML).

Ho letto qui che i gesti di scorrimento sulla visualizzazione dell'immagine stessa sono difettosi, ma non che lo scorrimento stesso sia difettoso: vedere qui .

Ho provato ad attivare lo "scorrimento fluido" tramite codice Java. Sembra che se scorro abbastanza lontano da non visualizzare più l'immagine, i gesti di lancio vengono riconosciuti.

TLDR: Perché il gesto di lancio non funziona fintanto che l'immagine è visibile? Il mio codice XML ha questo aspetto:

    <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>

Interessante, ho registrato gli eventi di tocco nella visualizzazione a scorrimento annidato durante un'avventura interessata. Ottiene ACTION_DOWN y=98 -> ACTION_MOVE y=-40 -> ACTION_MOVE y=-33 -> ACTION_UP y=97. Sembra che l'ultimo evento di tocco si stia riportando erroneamente come successivo al primo.
Xiao

Quale versione della libreria di supporto alla progettazione stai utilizzando?
Radu Topor

stai ignorando eventuali eventi di tocco? prova a impostare nestedScrollView.getParent().requestDisallowInterceptTouchEvent(true);la visualizzazione a scorrimento nidificata
Bhargav

Risposte:


20

Ho avuto esattamente lo stesso problema con CollapsingToolbarLayout con ImageView all'interno e NestedScrollView . Lo scorrimento del lancio si ferma quando il dito viene rilasciato.

Tuttavia, ho notato qualcosa di strano. Se inizi a scorrere con il dito da una vista con OnClickListener (es. Button), lo scorrimento di fling funziona perfettamente.

Così l'ho risolto con una strana soluzione. Imposta OnClickListener (che non fa nulla) sul figlio diretto di NestedScrollView . Allora funziona perfettamente!

<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>

Assegna al figlio diretto (LinearLayout) un ID e imposta OnClickListener in Activity

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

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

Appunti:

Testato utilizzando Support Design Library 25.0.1

CollapsingToolbarLayout with scrollFlags = "scroll | enterAlwaysCollapsed"


AOSP dovrebbe essere patchato con questa ottima soluzione: D
deviante

Dovrebbe ottenere più voti lol. Btw questo ha reso il CollapsingToolbarLayout molto sensibile allo scorrimento, ma migliore del comportamento corrotto corrente.
Ahmad Fadli

1
questo era troppo bello per essere vero, quindi l'ho provato e non funziona per me
Gilbert Mendoza

questa è la soluzione più pazza che ho provato finora in SO.
Techfist

: D ottima osservazione @jinang !!
Srichakradhar

10

So che questa domanda è stata posta più di un anno fa, ma questo problema non sembra ancora essere risolto nelle librerie di supporto / progettazione. Puoi aggiungere questo numero a Speciali in modo che si sposti più in alto nella coda delle priorità.

Detto questo, ho provato la maggior parte delle soluzioni pubblicate per questo, inclusa quella di patrick-iv senza successo. L'unico modo in cui sono stato in grado di mettermi al lavoro era imitare l'avventura e chiamarla a livello di programmazione se veniva rilevato un certo insieme di condizioni onPreNestedScroll(). Nelle poche ore del mio debug ho notato che onNestedFling()non veniva mai chiamato in un'avventura verso l'alto (scorri verso il basso) e sembrava essere consumato prematuramente. Non posso dire con certezza al 100% che funzionerà per il 100% delle implementazioni, ma funziona abbastanza bene per i miei usi, quindi ho finito per accontentarmi, anche se è piuttosto complicato e sicuramente non è quello che volevo fare.

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

E applicalo all'AppBar

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

Demo di CheeseSquare: prima dopo


È davvero meglio di niente, ma non proprio quello che si aspetterebbe un utente Android esperto. Grazie per aver collegato il problema, l'ho aggiunto a Speciali.
Raphael Royer-Rivard

Ho dovuto rimuovere enterAlwayslayout_ScrollFlag perché funzionasse, ma ora funziona bene
Alexandre G

3

Ho provato la soluzione di Floofer ma non era ancora abbastanza buona per me. Quindi ho escogitato una versione migliore del suo comportamento. L'AppBarLayout ora si espande e si comprime senza problemi durante il lancio.

Nota: ho usato la riflessione per hackerarmi, quindi potrebbe non funzionare perfettamente con una versione della libreria di Android Design diversa dalla 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);
    }
}

Per usarlo, imposta un nuovo comportamento su 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));

La tua classe richiede un int nel suo 'costruttore, ma nel codice non invii nulla al costruttore
bluesummers

Colpa mia, l'ho aggiunta.
Raphael Royer-Rivard

Questo sembra buono, rende lo scorrimento fluido, ma ho una domanda, è possibile lasciare che NestedScrollView scorra in AppBarLayout una volta che AppBarLayout raggiunge la parte superiore, e inoltre, quando scorro verso il basso, AppBarLayout appare alla fine, quando NestedScrollView completamente fatto scorrere, quindi AppBarLayout inizia ad espandersi.
Zijian Wang

@ZijianWang Per favore, spiegami cosa intendi per "scorrere in AppBarLayout" e anche io non capisco la tua seconda domanda, puoi riformularla?
Raphael Royer-Rivard

0

Questa risposta ha risolto questo problema per me. Crea un custom AppBarLayout.Behaviorcome questo:

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

e aggiungilo a AppBarLayoutquesto:

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

1
Non ha funzionato perché il problema nell'altra domanda era il RecyclerView che non è utilizzato qui.
Felix Edelmann

0

Sto solo postando questo qui in modo che altri non lo perdano nei commenti. La risposta di Jinang funziona magnificamente, ma complimenti ad AntPachon per aver indicato un metodo molto più semplice per lo stesso. Invece di implementare un OnClickmetodo a livello di Child of the NestedScrollViewprogrammazione, un modo migliore è impostare clickable=truel'xml per il bambino.

(Utilizzando lo stesso esempio di 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

Nel codice :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;

Quando uso uno scorrimento fling su NestedScrollView a volte "mIsBeingDragged = false", quindi NestedScrollView non invia l'evento fling.

Quando elimino la if (mIsBeingDragged)dichiarazione.

 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;

non ci saranno problemi. Ma non so quali altri gravi problemi verranno causati


Aggiungi più dettagli per rendere la risposta più facile da capire, hai scritto Quando rimuovo if ... da dove stai parlando per rimuovere if ?
Devendra Singh
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.