CollapsingToolbarLayout nie rozpoznaje przewijania

94

Stworzyłem prosty układ CollapsingToolbarLayout i działa jak urok. Mój problem polega na tym, że jeśli spróbuję użyć przewijanego zwoju w widoku zagnieżdżonego przewijania , to po prostu się zatrzymuje, gdy puszczam palec. Normalne przewijanie działa tak, jak powinno.

Kod moich działań pozostaje niezmieniony => automatycznie generowana pusta aktywność. (Właśnie kliknąłem Utwórz nową pustą aktywność w Android Studio i jeszcze edytować XML).

Czytałem tutaj, że gesty przewijania na samym widoku obrazu są błędne, ale nie, że samo przewijanie jest błędne: patrz tutaj .

Próbowałem aktywować „płynne przewijanie” za pomocą kodu java. Wygląda na to, że jeśli przewinie się na tyle daleko, że widok obrazu nie będzie już widoczny, rzucane gesty zostaną rozpoznane.

TLDR: Dlaczego gest rzucania nie działa, dopóki widok obrazu jest widoczny? Mój kod XML wygląda następująco:

    <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>
user1951516
źródło
Co ciekawe, zapisałem zdarzenia dotyku w zagnieżdżonym widoku przewijania podczas wyrzucania, którego to dotyczy. Dostaje ACTION_DOWN y=98 -> ACTION_MOVE y=-40 -> ACTION_MOVE y=-33 -> ACTION_UP y=97. Wygląda na to, że ostatnie zdarzenie dotyku błędnie zgłasza się jako występujące obok pierwszego.
Xiao
Z jakiej wersji biblioteki wsparcia projektowania korzystasz?
Radu Topor
czy nadpisujesz jakieś zdarzenia dotykowe? spróbuj ustawić nestedScrollView.getParent().requestDisallowInterceptTouchEvent(true);zagnieżdżony widok przewijania
Bhargav,

Odpowiedzi:

20

Miałem dokładnie ten sam problem z CollapsingToolbarLayout z ImageView wewnątrz i NestedScrollView . Przewijanie przewijania zatrzymuje się po zwolnieniu palca.

Jednak zauważyłem coś dziwnego. Jeśli zaczniesz przewijać palcem z widoku z OnClickListener (np. Button), przewijanie fling działa idealnie.

W ten sposób naprawiłem to dziwnym rozwiązaniem. Ustaw OnClickListener (który nic nie robi) dla bezpośredniego elementu podrzędnego NestedScrollView . Wtedy działa idealnie!

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

Nadaj bezpośredniemu elementowi podrzędnemu (LinearLayout) identyfikator i ustaw OnClickListener w działaniu

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

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

Uwagi:

Przetestowano przy użyciu biblioteki Support Design Library 25.0.1

CollapsingToolbarLayout with scrollFlags = "scroll | enterAlwaysCollapsed"

jinang
źródło
AOSP powinien zostać załatany tym świetnym rozwiązaniem: D
dewiant
Powinien dostać więcej głosów lol. Przy okazji sprawiło to, że CollapsingToolbarLayout był bardzo wrażliwy na przewijanie, ale lepszy niż bieżące zepsute zachowanie.
Ahmad Fadli
1
to było zbyt piękne, aby mogło być prawdziwe, więc spróbowałem i nie działa dla mnie
Gilbert Mendoza
to jest najbardziej szalone rozwiązanie, jakie do tej pory próbowałem w SO.
Techfist
: D świetna obserwacja @jinang !!
Srichakradhar
10

Wiem, że to pytanie zostało zadane ponad rok temu, ale nadal nie wydaje się, aby ten problem został rozwiązany w bibliotekach pomocy technicznej / projektowania. Możesz oznaczyć ten problem gwiazdką, aby przesunął się wyżej w kolejce priorytetowej.

To powiedziawszy, wypróbowałem większość opublikowanych rozwiązań, w tym rozwiązanie autorstwa patrick-iv bez powodzenia. Jedynym sposobem, w jaki mogłem zabrać się do pracy, było naśladowanie flingu i wywoływanie go programowo, jeśli wykryto określony zestaw warunków onPreNestedScroll(). W ciągu kilku godzin debugowania zauważyłem, że onNestedFling()plik nigdy nie był wywoływany w górę (przewijanie w dół) i wydawał się być przedwcześnie zużyty. Nie mogę powiedzieć ze stuprocentową pewnością, że zadziała to w 100% wdrożeń, ale działa wystarczająco dobrze do moich zastosowań, więc ostatecznie zdecydowałem się na to, mimo że jest to dość hakerskie i zdecydowanie nie to, co chciałem zrobić.

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

I zastosuj go do AppBar

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

Wersja demonstracyjna CheeseSquare: Przed Po

wchristiansen
źródło
To rzeczywiście lepsze niż nic, ale nie do końca to, czego oczekiwałby doświadczony użytkownik Androida. Dziękujemy za połączenie problemu, oznaczyłem go gwiazdką.
Raphael Royer-Rivard
Musiałem usunąć enterAlwayslayout_ScrollFlag, aby działał, ale teraz działa dobrze
Alexandre G
3

Wypróbowałem rozwiązanie firmy Floofer, ale nadal nie było to dla mnie wystarczająco dobre. Więc wymyśliłem lepszą wersję jego zachowania. AppBarLayout teraz rozwija się i zwija płynnie podczas rzutowania.

Uwaga: włamałem się do tego za pomocą refleksji, więc może to nie działać idealnie z wersją biblioteki Android Design inną niż 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);
    }
}

Aby z niego skorzystać, ustaw nowe zachowanie w 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));
Raphael Royer-Rivard
źródło
Twoja klasa wymaga int w swoim konstruktorze, ale w kodzie nie wysyłasz nic do konstruktora
bluesummers
Moja wina, dodałem to.
Raphael Royer-Rivard
Wygląda to dobrze, sprawia, że ​​przewijanie jest płynne, ale mam jedno pytanie, czy można pozwolić, aby NestedScrollView przewinął się do AppBarLayout, gdy AppBarLayout osiągnie górę, a także, kiedy przewijam w dół, AppBarLayout pojawia się w końcu, gdy NestedScrollView całkowicie przewinięty, a następnie AppBarLayout zaczyna się rozwijać.
Zijian Wang
@ZijianWang Proszę wyjaśnić, co masz na myśli, mówiąc „przewiń do AppBarLayout”. Nie rozumiem też drugiego pytania. Czy możesz je sformułować inaczej?
Raphael Royer-Rivard
0

Ta odpowiedź rozwiązała ten problem za mnie. Utwórz taki niestandardowy AppBarLayout.Behavior:

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

i dodaj to w AppBarLayoutten sposób:

<android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        ...
        app:layout_behavior="com.example.test.FlingBehavior">
patrick.elmquist
źródło
1
Nie zadziałało, ponieważ problemem w drugim pytaniu był RecyclerView, który nie jest tutaj używany.
Felix Edelmann
0

Po prostu publikuję to tutaj, aby inni nie przegapili tego w komentarzach. Odpowiedź Jinanga działa pięknie, ale pochwała AntPachonowi za wskazanie znacznie prostszej metody na to samo. Zamiast implementować OnClickmetodę w sposób Child of the NestedScrollViewprogramowy, lepszym sposobem jest ustawienieclickable=true w XML dla dziecka.

(Korzystając z tego samego przykładu co 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>
Kaushik NP
źródło
-1

W kodzie :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;

Kiedy używam przewijania fling w NestedScrollView czasami „mIsBeingDragged = false”, więc NestedScrollView nie wysyła zdarzenia fling.

Kiedy usunę if (mIsBeingDragged)oświadczenie.

 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;

nie będzie problemu. Ale nie wiem, jakie inne poważne problemy zostaną spowodowane

WIELKIE RYBY
źródło
Dodaj więcej szczegółów, aby ułatwić zrozumienie odpowiedzi. Napisałeś, kiedy usuwam if ... z miejsca, w którym mówisz, aby usunąć if ?
Devendra Singh