这是具有更通用解决方案的更新。它现在处理标准底视图行为的隐藏和“跳过折叠”。
以下解决方案使用自定义BottomSheetBehavior。这是一个基于您发布的应用程序的小应用程序的快速视频,其中包含适当的自定义行为:
MyBottomSheetBehavior 扩展 BottomSheetBehavior 并为所需的行为完成繁重的工作。 MyBottomSheetBehavior 是被动的,直到 NestedScrollView 达到其底部滚动限制。 onNestedScroll() 标识已达到限制并将底部工作表偏移滚动量,直到达到完全展开的底部工作表的偏移量。这就是展开逻辑。
一旦底部工作表从底部释放,底部工作表将被视为“已捕获”,直到用户将手指从屏幕上移开。当底部工作表被捕获时,onNestPreScroll() 处理将底部工作表移向屏幕底部。这是崩溃的逻辑。
BottomSheetBehavior 除了完全折叠或展开底部工作表之外,不提供操作底部工作表的方法。需要的其他功能被锁定在基本行为的包私有功能中。为了解决这个问题,我创建了一个名为 BottomSheetBehaviorAccessors 的新类,它与股票行为共享一个包 (android.support.design.widget)。此类提供对新行为中使用的一些包私有方法的访问。
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>