CollapsingToolbarLayout не распознает прокрутку

94

Я создал простой CollapsingToolbarLayout, и он работает как шарм. Моя проблема в том, что если я попытаюсь использовать прокрутку на nestedscrollview , она просто остановится, когда я отпущу палец. Обычная прокрутка работает как надо.

Код моих действий не изменился => автоматически сгенерированное пустое действие. (Я просто щелкнул создать новую пустую активность в студии Android и еще не отредактировал XML).

Я читал здесь, что жесты прокрутки на самом изображении ошибочны, но не то, что сама прокрутка ошибочна: см. Здесь .

Я пробовал активировать "плавную прокрутку" через java-код. Кажется, что если я прокручиваю достаточно далеко, чтобы изображение больше не было видно, тогда распознаются жесты броска.

TL; DR: почему жест бросания не работает, пока отображается изображение? Мой XML-код выглядит так:

    <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
источник
Интересно, что я регистрировал события касания во вложенном просмотре прокрутки во время затронутого броска. Это получается ACTION_DOWN y=98 -> ACTION_MOVE y=-40 -> ACTION_MOVE y=-33 -> ACTION_UP y=97. Похоже, что последнее событие касания неверно сообщает о себе как о находящемся рядом с первым.
Сяо
Какую версию библиотеки поддержки дизайна вы используете?
Раду Топор
вы отменяете какие-либо события касания? попробуйте установить nestedScrollView.getParent().requestDisallowInterceptTouchEvent(true);свой вложенный вид прокрутки
Бхаргав

Ответы:

20

У меня была точно такая же проблема с CollapsingToolbarLayout с ImageView внутри и NestedScrollView . Когда палец отпущен, прокрутка останавливается.

Однако я заметил кое-что странное. Если вы начнете прокручивать пальцем из представления с OnClickListener (например, Button), прокрутка будет работать отлично.

Таким образом, я исправил это странным решением. Установите OnClickListener (который ничего не делает) для прямого потомка NestedScrollView . Тогда он отлично работает!

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

Дайте прямому дочернему элементу (LinearLayout) идентификатор и установите OnClickListener в Activity

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

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

Примечания:

Протестировано с использованием библиотеки поддержки дизайна 25.0.1

CollapsingToolbarLayout с scrollFlags = "scroll | enterAlwaysCollapsed"

цзинань
источник
AOSP следует пропатчить с помощью этого отличного решения: D
deviant
Должно быть больше голосов, лол. Кстати, это сделало CollapsingToolbarLayout очень чувствительным к прокрутке, но лучше, чем текущее нарушенное поведение.
Ахмад Фадли
1
это было слишком хорошо, чтобы быть правдой, поэтому я попробовал, и это не сработало для меня
Гилберт Мендоза
это самое безумное решение, которое я пробовал до сих пор в SO.
Techfist
: D отличное наблюдение @jinang !!
Srichakradhar
10

Я знаю, что этот вопрос был задан более года назад, но эта проблема, похоже, не решена в библиотеках поддержки / дизайна. Вы можете пометить эту проблему, чтобы она переместилась дальше в очереди приоритетов.

Тем не менее, я попробовал большинство опубликованных решений для этого, в том числе решение от patrick-iv, но безуспешно. Единственный способ, которым я смог приступить к работе, - это имитировать бросок и вызвать его программно, если в нем был обнаружен определенный набор условий onPreNestedScroll(). За несколько часов отладки я заметил, что onNestedFling()никогда не вызывался броском вверх (прокрутка вниз) и, похоже, был использован преждевременно. Я не могу сказать со 100% уверенностью, что это будет работать для 100% реализаций, но это работает достаточно хорошо для моих целей, поэтому я в конечном итоге согласился на это, хотя это довольно хакерский и определенно не то, что я хотел сделать.

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

И примените его к AppBar

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

Демо CheeseSquare: До После

Wchristiansen
источник
Это действительно лучше, чем ничего, но не совсем то, что ожидал бы опытный пользователь Android. Спасибо за ссылку на проблему, я ее пометил.
Raphael Royer-Rivard
Пришлось удалить enterAlwayslayout_ScrollFlag, чтобы он заработал, но теперь работает хорошо
Александр Г.
3

Я попробовал решение Floofer, но оно мне все еще не подходило. Так что я придумал лучшую версию его поведения. AppBarLayout теперь плавно расширяется и сворачивается при бросании.

Примечание. Я использовал отражение, чтобы взломать это, поэтому он может не работать идеально с версией библиотеки Android Design, отличной от 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);
    }
}

Чтобы использовать его, установите новое поведение для вашего 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));
Рафаэль Ройер-Ривар
источник
Ваш класс требует int в своем конструкторе, но в коде вы ничего не отправляете конструктору
bluesummers
Моя беда, добавил я.
Рафаэль Ройер-Ривар
Это выглядит хорошо, это делает прокрутку плавной, но у меня есть один вопрос: можно ли позволить NestedScrollView прокручивать в AppBarLayout, как только AppBarLayout достигнет вершины, а также, когда я прокручиваю вниз, AppBarLayout появляется наконец, когда NestedScrollView полностью прокручивается, затем AppBarLayout начинает расширяться.
Zijian Wang
@ZijianWang Пожалуйста, объясните мне, что вы имеете в виду под «прокруткой в ​​AppBarLayout», и я также не понимаю ваш второй вопрос, можете ли вы его перефразировать?
Рафаэль Ройер-Ривар
0

Этот ответ решил эту проблему для меня. Создайте такой обычай 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;
    }
}

и добавьте это AppBarLayoutпримерно так:

<android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        ...
        app:layout_behavior="com.example.test.FlingBehavior">
Патрик Эльмквист
источник
1
Не сработало, поскольку проблема в другом вопросе заключалась в RecyclerView, который здесь не используется.
Феликс Эдельманн
0

Я просто публикую это здесь, чтобы другие не пропустили это в комментариях. Ответ Jinang работает прекрасно, но спасибо AntPachon за то, что он указал на гораздо более простой метод для того же. Вместо того, чтобы реализовывать OnClickметод Child of the NestedScrollViewпрограммно, лучше установить clickable=trueв xml для дочернего элемента.

(Используя тот же пример, что и у Цзинаня )

<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

В коде :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;

Когда я использую прокрутку в NestedScrollView, иногда «mIsBeingDragged = false», поэтому NestedScrollView не отправляет событие бросания.

Когда я удаляю if (mIsBeingDragged)заявление.

 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;

проблем не будет. Но я не знаю, какие еще серьезные проблемы возникнут

БОЛЬШАЯ РЫБА
источник
Добавьте больше деталей, чтобы облегчить понимание ответа. Вы написали Когда я удаляю если ... откуда вы говорите, чтобы удалить если ?
Девендра Сингх