当同级滚动到末尾时打开底部工作表?

7
有没有办法将一个滚动视图的滚动事件“转发”到我的底部表单上,这样当我在第一个滚动视图上超滚时,我的底部表单就开始展开了呢?
考虑一下这个小应用程序:
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        int peekHeight = getResources().getDimensionPixelSize(R.dimen.bottom_sheet_peek_height); // 96dp

        View bottomSheet = findViewById(R.id.bottomSheet);
        BottomSheetBehavior<View> behavior = BottomSheetBehavior.from(bottomSheet);
        behavior.setPeekHeight(peekHeight);
    }
}

<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.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- LinearLayout holding children to scroll through -->

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

    <View
        android:id="@+id/bottomSheet"
        android:layout_width="300dp"
        android:layout_height="400dp"
        android:layout_gravity="center_horizontal"
        app:layout_behavior="android.support.design.widget.BottomSheetBehavior"/>

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

开箱即用,这样做完全没问题。我可以看到96dp的底部表格,并且可以像平常一样向上和向下滑动它。此外,我可以看到我的滚动内容,并且可以像平常一样向上和向下滚动。

enter image description here enter image description here enter image description here

假设我处于第二张图片所示的状态。我的NestedScrollView已经滚动到底部,我的底部表格被折叠起来。我想能够在NestedScrollView上向上滑动(而不是在底部表格上),并且由于它无法再滚动,因此该滑动手势将被发送到底部表格,使其开始展开。基本上,让应用程序表现得好像我的手势是在底部表格上执行的,而不是在滚动视图上执行的。我的第一个想法是查看NestedScrollView.OnScrollChangeListener,但由于它在滚动内容的边界处停止触发(毕竟,它监听滚动更改,而当您处于边缘时,没有任何变化),因此我无法让它起作用。
我还尝试创建自己的BottomSheetBehavior子类并尝试覆盖onInterceptTouchEvent(),但遇到了两个问题。首先,我只想捕获当兄弟滚动视图在底部时的事件,我可以做到这一点,但现在我捕获了所有事件(使兄弟无法向上滚动)。其次,BottomSheetBehavior内部的mIgnoreEvents字段阻止底部表格实际扩展。我可以使用反射来访问此字段并防止它阻止我,但那感觉很邪恶。

编辑:我花了更多时间研究AppBarLayout.ScrollingViewBehavior,因为它似乎非常接近我想要的内容(它将一个视图上的轻扫转换为另一个视图上的调整大小),但它似乎是手动逐像素设置偏移量,而底部表格的行为不完全相同。


问题:当NestedScrollView滚动到底部并且底部表格已折叠,向屏幕顶部滑动会打开底部表格。如果用户反向滑动并向屏幕底部滑动而不将手指从NestedScrollView上抬起,您希望发生什么? 底部表格是否应该反向折叠或者NestedScrollView应该滚动? - Cheticamp
@Cheticamp 如果在一个完美的世界里,只需一个手势,底部工作表就会折叠。我想看到的是,手势的行为就像用户的手指放在底部工作表上一样。然而,如果有一种方法只能打开工作表,但反向操作将“取消”该手势并滚动NestedScrollView,那也是可以接受的。只是不太理想。 - Ben P.
1个回答

11
这是一个更通用的更新版本,现在可以处理标准底部视图行为的隐藏和“跳过折叠”。以下解决方案使用自定义BottomSheetBehavior。这里是一个基于您发布的应用程序的小应用程序的快速视频,其中包含自定义行为:enter image description here。MyBottomSheetBehavior扩展BottomSheetBehavior,并执行所需行为的重要工作。 MyBottomSheetBehavior是被动的,直到NestedScrollView达到其底部滚动限制。 onNestedScroll()识别已达到限制并通过滚动量偏移底片,直到达到完全展开的底片的偏移量。这是扩展逻辑。一旦底片从底部释放,底片被认为是“捕获”,直到用户从屏幕上抬起手指为止。在底片被捕获时,onNestPreScroll()处理将底片向屏幕底部移动。这是折叠逻辑。 BottomSheetBehavior除了完全折叠或展开它之外,没有提供操作底片的方法。需要的其他功能被锁定在基本行为的包私有函数中。为了解决这个问题,我创建了一个名为BottomSheetBehaviorAccessors的新类,它与库行为共享包(android.support.design.widget)。该类提供对一些在新行为中使用的包私有方法的访问权限。 MyBottomSheetBehavior还适应了BottomSheetBehavior.BottomSheetCallback的回调和其他通用功能。
public class MyBottomSheetBehavior<V extends View> extends BottomSheetBehaviorAccessors<V> {

    // The bottom sheet that interests us.
    private View mBottomSheet;

    // Offset when sheet is expanded.
    private int mMinOffset;

    // Offset when sheet is collapsed.
    private int mMaxOffset;

    // This is the  bottom of the bottom sheet's parent.
    private int mParentBottom;

    // True if the bottom sheet is being moved through nested scrolls from NestedScrollView.
    private boolean mSheetCaptured = false;

    // True if the bottom sheet is touched directly and being dragged.
    private boolean mIsheetTouched = false;

    // Set to true on ACTION_DOWN on the NestedScrollView
    private boolean mScrollStarted = false;

    @SuppressWarnings("unused")
    public MyBottomSheetBehavior() {
    }

    @SuppressWarnings("unused")
    public MyBottomSheetBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
            mSheetCaptured = false;
            mIsheetTouched = parent.isPointInChildBounds(child, (int) ev.getX(), (int) ev.getY());
            mScrollStarted = !mIsheetTouched;
        }
        return super.onInterceptTouchEvent(parent, child, ev);
    }

    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
        mMinOffset = Math.max(0, parent.getHeight() - child.getHeight());
        mMaxOffset = Math.max(parent.getHeight() - getPeekHeight(), mMinOffset);
        mBottomSheet = child;
        mParentBottom = parent.getBottom();
        return super.onLayoutChild(parent, child, layoutDirection);
    }

    @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                  @NonNull V child, @NonNull View target, int dx, int dy,
                                  @NonNull int[] consumed, int type) {
        if (dy >= 0 || !mSheetCaptured || type != ViewCompat.TYPE_TOUCH
            || !(target instanceof NestedScrollView)) {
            super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
            return;
        }
        // Pointer moving downward (dy < 0: scrolling toward top of data)
        if (child.getTop() - dy <= mMaxOffset) {
            // Dragging...
            ViewCompat.offsetTopAndBottom(child, -dy);
            setStateInternalAccessor(STATE_DRAGGING);
            consumed[1] = dy;
        } else if (isHideable()) {
            // Hide...
            ViewCompat.offsetTopAndBottom(child, Math.min(-dy, mParentBottom - child.getTop()));
            consumed[1] = dy;
        } else if (mMaxOffset - child.getTop() > 0) {
            // Collapsed...
            ViewCompat.offsetTopAndBottom(child, mMaxOffset - child.getTop());
            consumed[1] = dy;
        }
        if (consumed[1] != 0) {
            dispatchOnSlideAccessor(child.getTop());
        }
    }

    @Override
    public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child,
                               @NonNull View target, int dxConsumed, int dyConsumed,
                               int dxUnconsumed, int dyUnconsumed, int type) {
        if (dyUnconsumed <= 0 || !(target instanceof NestedScrollView)
            || type != ViewCompat.TYPE_TOUCH || getState() == STATE_HIDDEN) {
            mSheetCaptured = false;
        } else if (!mSheetCaptured) {
            // Capture the bottom sheet only if it is at its collapsed height.
            mSheetCaptured = isSheetCollapsed();
        }
        if (!mSheetCaptured) {
            super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed,
                                 dxUnconsumed, dyUnconsumed, type);
            return;
        }

        /*
            If the pointer is moving upward (dyUnconsumed > 0) and the scroll view isn't
            consuming scroll (dyConsumed == 0) then the scroll view  must be at the end
            of its scroll.
        */
        if (child.getTop() - dyUnconsumed < mMinOffset) {
            // Expanded...
            ViewCompat.offsetTopAndBottom(child, mMinOffset - child.getTop());
        } else {
            // Dragging...
            ViewCompat.offsetTopAndBottom(child, -dyUnconsumed);
            setStateInternalAccessor(STATE_DRAGGING);
        }
        dispatchOnSlideAccessor(child.getTop());
    }

    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
        if (mScrollStarted) {
            // Ignore initial call to this method before anything has happened.
            mScrollStarted = false;
        } else if (!mIsheetTouched) {
            snapBottomSheet();
        }
        super.onStopNestedScroll(coordinatorLayout, child, target);
    }

    private void snapBottomSheet() {
        if ((mMaxOffset - mBottomSheet.getTop()) > (mMaxOffset - mMinOffset) / 2) {
            setState(BottomSheetBehavior.STATE_EXPANDED);
        } else if (shouldHideAccessor(mBottomSheet, 0)) {
            setState(BottomSheetBehavior.STATE_HIDDEN);
        } else {
            setState(BottomSheetBehavior.STATE_COLLAPSED);
        }
    }

    private boolean isSheetCollapsed() {
        return mBottomSheet.getTop() == mMaxOffset;
    }

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

BottomSheetBehaviorAccessors

package android.support.design.widget; // important!

// A "friend" class to provide access to some package-private methods in `BottomSheetBehavior`.
public class BottomSheetBehaviorAccessors<V extends View> extends BottomSheetBehavior<V> {

    @SuppressWarnings("unused")
    protected BottomSheetBehaviorAccessors() {
    }

    @SuppressWarnings("unused")
    public BottomSheetBehaviorAccessors(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    protected void setStateInternalAccessor(int state) {
        super.setStateInternal(state);
    }

    protected void dispatchOnSlideAccessor(int top) {
        super.dispatchOnSlide(top);
    }

    protected boolean shouldHideAccessor(View child, float yvel) {
        return mHideable && super.shouldHide(child, yvel);
    }

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

MainActivity.java

public class MainActivity extends AppCompatActivity{
    private View mBottomSheet;
    MyBottomSheetBehavior<View> mBehavior;

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

        int peekHeight = getResources().getDimensionPixelSize(R.dimen.bottom_sheet_peek_height); // 96dp
        mBottomSheet = findViewById(R.id.bottomSheet);
        mBehavior = (MyBottomSheetBehavior) MyBottomSheetBehavior.from(mBottomSheet);
        mBehavior.setPeekHeight(peekHeight);
    }
}

activity_main.xml

<android.support.design.widget.CoordinatorLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:stateListAnimator="@null"
        android:theme="@style/AppTheme.AppBarOverlay"
        app:expanded="false"
        app:layout_behavior="android.support.design.widget.AppBarLayout$Behavior">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsingToolbarLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_scrollFlags="scroll|exitUntilCollapsed"
            app:statusBarScrim="?attr/colorPrimaryDark">

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="250dp"
                android:layout_marginTop="?attr/actionBarSize"
                android:scaleType="centerCrop"
                android:src="@drawable/seascape1"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="1.0"
                tools:ignore="ContentDescription" />

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

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

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

    <com.example.bottomsheetoverscroll.MyNestedScrollView
        android:id="@+id/nestedScrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <View
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:background="@android:color/holo_blue_light" />

            <View
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:background="@android:color/holo_red_light" />

            <View
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:background="@android:color/holo_blue_light" />

            <View
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:background="@android:color/holo_red_light" />

            <View
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:background="@android:color/holo_blue_light" />

            <View
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:background="@android:color/holo_red_light" />

            <View
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:background="@android:color/holo_green_light" />

        </LinearLayout>
    </com.example.bottomsheetoverscroll.MyNestedScrollView>

    <TextView
        android:id="@+id/bottomSheet"
        android:layout_width="300dp"
        android:layout_height="400dp"
        android:layout_gravity="center_horizontal"
        android:background="@android:color/white"
        android:text="Bottom Sheet"
        android:textAlignment="center"
        android:textSize="24sp"
        android:textStyle="bold"
        app:layout_behavior="com.example.bottomsheetoverscroll.MyBottomSheetBehavior" />
    <!--app:layout_behavior="android.support.design.widget.BottomSheetBehavior" />-->

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

谢谢您的回复。这确实回答了我所提出的问题...不幸的是,对于我的实际应用程序来说,破坏BottomSheetCallback是不可接受的。我希望我能够适应您的答案。 - Ben P.
@BenP。回答已更新。 - Cheticamp
@BenP。最终更新行为。它过度报告滚动状态,现在处理回调和“可隐藏”和“跳过折叠”的选项。 - Cheticamp
@Cheticamp,com.example.bottomsheetoverscroll.MyNestedScrollView是什么? - Charles
@Charles 我认为那是早期回答的版本。最终版本只有一个 NestedScrollView,而不是自定义的。 - Cheticamp
@Cheticamp 你知道如何使用Material库使其正常工作吗?setStateInternaldispatchOnSlide不再公开可用。 - rewgoes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接