问题
Is there any way to "forward" scroll events from one scrolling view to my bottom sheet, so that my bottom sheet begins to expand when I over-scroll the first scrolling view?
Consider this tiny app:
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>
Out of the box, this works just fine. I see 96dp worth of my bottom sheet, and I can swipe it up and down as normal. Additionally, I can see my scrolling content, and I can scroll it up and down as normal.
Let's assume I'm at the state shown in the second image. My NestedScrollView
is scrolled all the way to the bottom and my bottom sheet is collapsed. I'd like to be able to swipe upwards on the NestedScrollView
(not on the bottom sheet) and, since it can't scroll any farther, have that swipe gesture instead be sent to the bottom sheet, so that it begins to expand. Basically, have the app behave as though my gesture had been performed on the bottom sheet, not the scroll view.
My first thought was to look at NestedScrollView.OnScrollChangeListener
, but I couldn't get that to work since it stops being triggered at the boundaries of the scrolling content (after all, it listens for scroll changes, and nothing's changing when you're at the edges).
I also took a look at creating my own subclass of BottomSheetBehavior
and trying to override onInterceptTouchEvent()
, but ran into trouble in two places. First, I only want to capture events when the sibling scroll view is at the bottom, and I could do that, but I was now capturing all events (making it impossible to scroll the sibling back up). Second, the private
field mIgnoreEvents
inside BottomSheetBehavior
was blocking the bottom sheet from actually expanding. I can use reflection to access this field and prevent it from blocking me, but that feels evil.
Edit: I spent some more time looking into AppBarLayout.ScrollingViewBehavior
, since that seemed to be pretty close to what I wanted (it converts swipes on one view into resizing on another), but that appears to manually set the offset pixel by pixel, and bottom sheets don't quite behave that way.
回答1:
This is an update with a more general solution. It now handles being hidden and "skip collapsed" of the standard bottom view behavior.
The following solution uses a custom BottomSheetBehavior
. Here is a quick video of a small app based upon your posted app with the custom behavior in place:
MyBottomSheetBehavior
extends BottomSheetBehavior
and does the heavy lifting for the desired behavior. MyBottomSheetBehavior
is passive until the NestedScrollView
reaches its bottom scroll limit. onNestedScroll()
identifies that the limit has been reached and offsets the bottom sheet by the amount of the scroll until the offset for the fully expanded bottom sheet is reached. This is the expansion logic.
Once the bottom sheet is released from the bottom, the bottom sheet is considered "captured" until the user lifts a finger from the screen. While the bottom sheet is captured, onNestPreScroll()
handles moving the bottom sheet toward the bottom of the screen. This is the collapsing logic.
BottomSheetBehavior
doesn't provide a means to manipulate the bottom sheet other than to completely collapse or expand it. Other functionality that is needed is locked up in package-private functions of the base behavior. To get around this, I created a new class called BottomSheetBehaviorAccessors
that shares a package (android.support.design.widget
) with the stock behavior. This class provides access to some package-private methods that are used in the new behavior.
MyBottomSheetBehavior
also accommodates the callbacks of BottomSheetBehavior.BottomSheetCallback
and other general functionality.
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>
来源:https://stackoverflow.com/questions/48939710/open-bottom-sheet-when-sibling-scrolling-reaches-the-end