当兄弟滚动到最后时打开底部 sheet?

Open bottom sheet when sibling scrolling reaches the end?

有什么方法可以 "forward" 将事件从一个滚动视图滚动到底部 sheet,以便当我 over-scroll第一个滚动视图?

考虑一下这个小应用程序:

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 的底部 sheet,我可以像往常一样上下滑动它。另外,我可以看到我滚动的内容,我可以像往常一样上下滚动它。

假设我处于第二张图片中显示的状态。我的 NestedScrollView 一直滚动到底部,我的底部 sheet 折叠了。我希望能够在 NestedScrollView 上向上滑动( 而不是 在底部 sheet),并且由于它不能再滚动了,所以该滑动手势 而不是 被发送到底部 sheet,以便它开始扩展。基本上,让应用程序表现得好像我的手势是在底部执行的 sheet,而不是滚动视图。

我的第一个想法是查看 NestedScrollView.OnScrollChangeListener,但我无法让它工作,因为它在滚动内容的边界停止触发(毕竟,它监听滚动 改变,当你在边缘时什么都没有改变)。

我还查看了创建自己的 BottomSheetBehavior 子类并尝试覆盖 onInterceptTouchEvent(),但 运行 在两个地方遇到了麻烦。首先,我只想在兄弟滚动视图位于底部时捕获事件,我可以这样做,但我现在正在捕获 all 事件(使得无法向后滚动兄弟向上)。其次,BottomSheetBehavior 内的 private 字段 mIgnoreEvents 阻止了底部 sheet 的实际扩展。我可以使用反射来访问这个字段并防止它阻止我,但感觉很糟糕。

编辑:我花了更多时间研究 AppBarLayout.ScrollingViewBehavior,因为这似乎非常接近我想要的(它将一个视图上的滑动转换为另一个视图上的调整大小),但这似乎是手动设置的一个像素一个像素地偏移,底部 sheets 的表现不尽如人意。

这是一个包含更通用解决方案的更新。它现在处理隐藏和 "skip collapsed" 标准底部视图行为。

以下解决方案使用自定义 BottomSheetBehavior。这是一个基于您发布的具有适当自定义行为的应用程序的小应用程序的快速视频:

MyBottomSheetBehavior 扩展了 BottomSheetBehavior 并为所需的行为做了繁重的工作。 MyBottomSheetBehavior 处于被动状态,直到 NestedScrollView 达到其底部滚动限制。 onNestedScroll() 标识已达到限制并按滚动量偏移底部 sheet,直到达到完全展开底部的偏移量 sheet。这就是扩展逻辑。

一旦底部sheet从底部释放,底部sheet被认为是"captured",直到用户从屏幕上抬起手指。当捕获底部 sheet 时,onNestPreScroll() 处理将底部 sheet 移向屏幕底部。这就是崩溃的逻辑。

BottomSheetBehavior 不提供操纵底部 sheet 的方法,除了完全折叠或展开它。所需的其他功能已锁定在基本行为的 package-private 功能中。为了解决这个问题,我创建了一个名为 BottomSheetBehaviorAccessors 的新 class,它与股票行为共享一个包 (android.support.design.widget)。此 class 提供对新行为中使用的一些 package-private 方法的访问。

MyBottomSheetBehavior 还包含 BottomSheetBehavior.BottomSheetCallback 和其他常规功能的回调。

MyBottomSheetBehavior.java

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>