禁用用户拖动BottomSheet

125

我试图禁用 BottomSheet 上的用户拖动。我想要禁用的原因有两个。1. 它阻止了 ListView 向下滚动,2. 我不希望用户使用拖动来关闭,而是在 BottomSheetView 上使用按钮来关闭。这是我所做的。

 bottomSheetBehavior = BottomSheetBehavior.from(bottomAnc);
    bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
        @Override
        public void onStateChanged(@NonNull View bottomSheet, int newState) {
            if (newState == BottomSheetBehavior.STATE_EXPANDED) {
                //Log.e("BottomSheet", "Expanded");
            } else if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
                //Log.e("BottomSheet", "Collapsed");
            }
        }

        @Override
        public void onSlide(@NonNull View bottomSheet, float slideOffset) {
            // React to dragging events
            bottomSheet.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    int action = MotionEventCompat.getActionMasked(event);
                    switch (action) {
                        case MotionEvent.ACTION_DOWN:
                            return false;
                        default:
                            return true;
                    }
                }
            });
        }
    });

底部弹出窗布局(bottomSheetLayout)
    <?xml version="1.0" encoding="utf-8"?><FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
app:behavior_hideable="true"
app:behavior_peekHeight="0dp"
app:layout_behavior="@string/bottom_sheet_behavior"
android:id="@+id/bottomSheet">

<android.support.v7.widget.CardView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:elevation="10dp">

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

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center_vertical">

            <TextView
                android:id="@+id/text1"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="Order Items"
                android:layout_margin="16dp"
                android:textAppearance="@android:style/TextAppearance.Large"/>


            <Button
                android:layout_width="50dp"
                android:layout_height="wrap_content"
                android:layout_marginRight="5dp"
                android:background="@drawable/bg_accept"/>

            <Button
                android:layout_width="50dp"
                android:layout_height="wrap_content"
                android:layout_marginRight="8dp"
                android:background="@drawable/bg_cancel"/>

        </LinearLayout>

        <ListView
            android:id="@+id/item_edit"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/white"
            android:divider="@color/md_divider_black"
            android:dividerHeight="1dp"/>

    </LinearLayout>

</android.support.v7.widget.CardView>

37个回答

106

可能现在已经不相关了,但我还是会把它留在这里:

import android.content.Context
import android.util.AttributeSet
import androidx.coordinatorlayout.widget.CoordinatorLayout
import android.view.MotionEvent
import android.view.View
import com.google.android.material.bottomsheet.BottomSheetBehavior

@Suppress("unused")
class LockableBottomSheetBehavior<V : View> : BottomSheetBehavior<V> {
    constructor() : super()
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    var swipeEnabled = true

    override fun onInterceptTouchEvent(
        parent: CoordinatorLayout,
        child: V,
        event: MotionEvent
    ): Boolean {
        return if (swipeEnabled) {
            super.onInterceptTouchEvent(parent, child, event)
        } else {
            false
        }
    }

    override fun onTouchEvent(parent: CoordinatorLayout, child: V, event: MotionEvent): Boolean {
        return if (swipeEnabled) {
            super.onTouchEvent(parent, child, event)
        } else {
            false
        }
    }

    override fun onStartNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        child: V,
        directTargetChild: View,
        target: View,
        axes: Int,
        type: Int
    ): Boolean {
        return if (swipeEnabled) {
            super.onStartNestedScroll(
                coordinatorLayout,
                child,
                directTargetChild,
                target,
                axes,
                type
            )
        } else {
            false
        }
    }

    override fun onNestedPreScroll(
        coordinatorLayout: CoordinatorLayout,
        child: V,
        target: View,
        dx: Int,
        dy: Int,
        consumed: IntArray,
        type: Int
    ) {
        if (swipeEnabled) {
            super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
        }
    }

    override fun onStopNestedScroll(
        coordinatorLayout: CoordinatorLayout,
        child: V,
        target: View,
        type: Int
    ) {
        if (swipeEnabled) {
            super.onStopNestedScroll(coordinatorLayout, child, target, type)
        }
    }

    override fun onNestedPreFling(
        coordinatorLayout: CoordinatorLayout,
        child: V,
        target: View,
        velocityX: Float,
        velocityY: Float
    ): Boolean {
        return if (swipeEnabled) {
            super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY)
        } else {
            false
        }
    }
}

并将其用于您的xml文件中:

app:layout_behavior="com.your.package.LockableBottomSheetBehavior"

它禁用所有用户操作,当你只想通过编程控制BottomSheet时可以使用。


3
这是禁用BottomSheetBehaviour的最佳答案。 上面的一位男士也发表了类似的解决方案,但他没有写如何覆盖其他事件,比如 *onTouchEvent()*。另一方面,如果您使用标志而不是 false,则可以改进您的答案。 - murt
4
你如何在BottomSheetFragment中使用这个? - user3144836
7
你需要在XML文件中明确引用这个类。app:layout_behavior="com.my.package.UserLockBottomSheetBehavior" - Steve
3
在某些情况下,这仍然不起作用。如果我们在底部工作表片段中有一个列表,它仍然会被拖动。 - Deepak Joshi
1
@DeepakJoshi 你肯定用过 SwipeRefreshLayout。如果不使用它,底部表格就不能在 RecyclerView 向上滚动时被拖拽下来。 - Debdeep
显示剩余14条评论

86

编辑于2023年5月22日

请使用addBottomSheetCallback替代setBottomSheetCallback

感谢,@abedullah


setBottomSheetCallback方法的onStateChanged中检查状态,如果状态是BottomSheetBehavior.STATE_DRAGGING,则将其更改为BottomSheetBehavior.STATE_EXPANDED,这样您就可以阻止用户进行STATE_DRAGGING,示例如下:
final BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
        behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
            @Override
            public void onStateChanged(@NonNull View bottomSheet, int newState) {
                if (newState == BottomSheetBehavior.STATE_DRAGGING) {
                    behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
                }
            }

            @Override
            public void onSlide(@NonNull View bottomSheet, float slideOffset) {
            }
        });

使用按钮打开/关闭底部工作表,如下所示

fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (behavior.getState() == BottomSheetBehavior.STATE_HIDDEN) {
                    behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
                } else {
                    behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
                }
            }
        });

不要使用 setPeekHeightapp:behavior_peekHeight

通过上述方法,您可以达到您的目标。


1
不错的技巧。我没注意到。谢谢。还有,你能帮忙解决这个问题吗?当我第一次告诉它要扩展时,它是透明的,我可以看到后面的视图,但在我点击 SheetView 中的 EditText 之前,我无法进行交互,也无法使其可见。 - Tonespy
1
我尝试过这个,但状态最终停留在STATE_SETTLING。我有一个按钮来打开和关闭底部工作表,如果它是隐藏的,我会展开它。如果它已经展开,我会将其隐藏。由于它被卡在了SETTLING状态,所以在拖动底部工作表后我的按钮无法正常工作。对此有什么想法吗? - Gokhan Arik
3
这个解决方案不可靠;底部页面会出现问题,就像Gokhan说的那样...当它处于这种糟糕状态时,诸如将新片段加载到底部页面的调用将只会变成空白。 - Ray W
10
如果您在底部工作表内使用了嵌套滚动视图,则无法正常工作。 - Rishabh Chandel
1
它的功能完美,但 setBottomSheetCallback 已经过时了!请使用 addBottomSheetCallback - Abdullah
显示剩余15条评论

60
implementation 'com.google.android.material:material:1.2.0-alpha05'

你可以这样禁用BottomSheet的拖动功能。

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
    val dialog = BottomSheetDialog(requireContext(), theme)
    dialog.setOnShowListener {
        setBottomSheetExpanded(dialog)
    }
    return dialog
}

open fun setBottomSheetExpanded(bottomSheetDialog: BottomSheetDialog) {
    val bottomSheet =
        bottomSheetDialog.findViewById<View>(R.id.design_bottom_sheet) as FrameLayout?
    bottomSheet?.let {
        val behavior: BottomSheetBehavior<*> = BottomSheetBehavior.from(bottomSheet)
        val layoutParams = bottomSheet.layoutParams
        bottomSheet.layoutParams = layoutParams
        behavior.state = BottomSheetBehavior.STATE_EXPANDED
        behavior.isDraggable = false / true
    }

}

编辑) 该库已更新!您可以使用新的库版本

implementation 'com.google.android.material:material:1.4.0'

示例相同,祝你好运和编写出优秀的代码。


6
这应该成为默认答案。在尝试了其他所有解决方案之前,我浪费了宝贵的时间,但没有一个可以与它相比。 - Hank Chan
我无法理解这个答案。所以,请详细解释。 - Pradeep Simba

35

好的,所以接受的答案对我不起作用。然而,Виталий Обидейко的答案启发了我的最终解决方案。

首先,我创建了以下自定义BottomSheetBehavior。 它覆盖了所有涉及触摸的方法,并在被锁定时返回false(或什么也不做)。 否则,它就像一个普通的BottomSheetBehavior一样。 这会禁用用户向下拖动的能力,并且不影响在代码中更改状态。

LockableBottomSheetBehavior.java

public class LockableBottomSheetBehavior<V extends View> extends BottomSheetBehavior<V> {

    private boolean mLocked = false;

    public LockableBottomSheetBehavior() {}

    public LockableBottomSheetBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setLocked(boolean locked) {
        mLocked = locked;
    }

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
        boolean handled = false;

        if (!mLocked) {
            handled = super.onInterceptTouchEvent(parent, child, event);
        }

        return handled;
    }

    @Override
    public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
        boolean handled = false;

        if (!mLocked) {
            handled = super.onTouchEvent(parent, child, event);
        }

        return handled;
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes) {
        boolean handled = false;

        if (!mLocked) {
            handled = super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
        }

        return handled;
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed) {
        if (!mLocked) {
            super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        }
    }

    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
        if (!mLocked) {
            super.onStopNestedScroll(coordinatorLayout, child, target);
        }
    }

    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY) {
        boolean handled = false;

        if (!mLocked) {
            handled = super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
        }

        return handled;

    }
}

以下是使用示例。在我的情况下,我需要底部工作表在展开时锁定。

activity_home.xml

<?xml version="1.0" encoding="utf-8"?>
<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.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <android.support.design.widget.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_scrollFlags="scroll|snap"
            app:titleEnabled="false"/>
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"/>
    </android.support.design.widget.AppBarLayout>

    <!-- Use layout_behavior to set your Behavior-->
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        app:layout_behavior="com.myapppackage.LockableBottomSheetBehavior"/>

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

HomeActivity.java

public class HomeActivity extends AppCompatActivity {
    BottomSheetBehavior mBottomSheetBehavior;

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

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
        recyclerView.setAdapter(new SomeAdapter());

        mBottomSheetBehavior = BottomSheetBehavior.from(recyclerView);
        mBottomSheetBehavior.setBottomSheetCallback(new MyBottomSheetCallback());
    }

    class MyBottomSheetCallback extends BottomSheetBehavior.BottomSheetCallback() {
        @Override
        public void onStateChanged(@NonNull View bottomSheet, int newState) {
            if (newState == BottomSheetBehavior.STATE_EXPANDED) {
                if (mBottomSheetBehavior instanceof LockableBottomSheetBehavior) {
                    ((LockableBottomSheetBehavior) mBottomSheetBehavior).setLocked(true);
                }
            }
        }

        @Override
        public void onSlide(@NonNull View bottomSheet, float slideOffset) {}
    });
}

希望这能帮助消除很多困惑!


1
很好,最好的答案是我们可以避免这些状态的变通方法,从而避免错过事件。谢谢。 - Tấn Nguyên
@James - 回答得不错,但我现在无法设置setPeekHeight()。有什么想法吗? - Adarsh Yadav
1
这是一个不错的解决方法,尽管它到今天为止还没有更新。OnNestedPreScroll和某些其他方法已被弃用。需要更新这些方法,然后它就可以正常工作了。 - Ajay
5
你好,它在BottomSheetDialogFragment上无效,我仍然可以拖动底部表单。 - florian-do
我遇到了以下问题- 无法膨胀行为子类com.rp.th.sit.order.cart.presentation.view.cartutil.LockableBottomSheetBehavior这是为什么? - sweet_vish
显示剩余2条评论

23

我最后编写了一种解决方法来解决动态禁用用户拖动的用例,其中BottomSheetBehavior被子类化以覆盖onInterceptTouchEvent,并在自定义标志(在这种情况下为mAllowUserDragging)设置为false时忽略它:

import android.content.Context;
import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class WABottomSheetBehavior<V extends View> extends BottomSheetBehavior<V> {
    private boolean mAllowUserDragging = true;
    /**
     * Default constructor for instantiating BottomSheetBehaviors.
     */
    public WABottomSheetBehavior() {
        super();
    }

    /**
     * Default constructor for inflating BottomSheetBehaviors from layout.
     *
     * @param context The {@link Context}.
     * @param attrs   The {@link AttributeSet}.
     */
    public WABottomSheetBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setAllowUserDragging(boolean allowUserDragging) {
        mAllowUserDragging = allowUserDragging;
    }

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
        if (!mAllowUserDragging) {
            return false;
        }
        return super.onInterceptTouchEvent(parent, child, event);
    }
}

在您的布局 XML 中:

    <FrameLayout
        android:id="@+id/bottom_sheet_frag_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:behavior_hideable="true"
        app:behavior_peekHeight="@dimen/bottom_sheet_peek_height"
        app:elevation="@dimen/bottom_sheet_elevation"
        app:layout_behavior="com.example.ray.WABottomSheetBehavior" />

到目前为止,这是在需要时禁用Bottom Sheet用户拖动最一致的解决方案。

其他依赖于在onStateChanged回调中触发另一个setState调用的所有解决方案都会导致BottomSheet进入糟糕的状态,或者引起显著的UX问题(在将setState调用发布到Runnable的情况下)。

希望这能帮助某些人:)

Ray


4
那很不错。 - Odys
3
请使用NestedScrollView替代FrameLayout,并设置bottomSheetFragContainer.setNestedScrollingEnabled(false); - Afzal N
1
解决方法:通过设置回调 CoordinatorLayout.Behavior behavior = layoutParams.getBehavior(); 如果该行为不为空并且是属于BottomSheetBehavior,那么就设置((BottomSheetBehavior) behavior).setBottomSheetCallback(mBottomSheetBehaviorCallback); - LOG_TAG
4
对我来说这不起作用!PS:我在底部工作表中有一个可滚动的文本。 - Thorvald
6
在初始化期间如何进行转换?这会给我一个警告。WABottomSheetBehavior<View> behavior = (WABottomSheetBehavior) BottomSheetBehavior.from(sheetView); - Leo DroidCoder
显示剩余6条评论

13

回答比较晚,但这是对我有效的方法,与其他人建议的有点不同。

你可以尝试将cancelable属性设置为false,即:

setCancelable(false);

然后在 setupDialog 方法中手动处理您希望关闭对话框的事件。

@Override
public void setupDialog(final Dialog dialog, final int style) {

    // handle back button
    dialog.setOnKeyListener(new DialogInterface.OnKeyListener() {
        @Override
        public boolean onKey(final DialogInterface dialog, final int keyCode, final KeyEvent event) {
            if (keyCode == KeyEvent.KEYCODE_BACK) {
                dialog.dismiss();
            }
            return true;
        }
    });

    // handle touching outside of the dialog
    final View touchOutsideView = getDialog().getWindow().getDecorView().findViewById(android.support.design.R.id.touch_outside);
    touchOutsideView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(final View view) {
            dialog.dismiss();
        }
    });
}

使用ListView在对话框片段中可以解决问题,这是其他解决方案遇到困难的地方。


不错的简洁解决方案。对于阅读此内容的任何人,您(可能)需要在关闭对话框之前进行其他检查,例如event.isCanceled()event.getAction() == MotionEvent.ACTION_UP,这将防止误触发关闭操作。 - Eric Bachhuber
谢谢您。这是禁用拖动的最简单解决方案。 - AVJ

10

期望行为:

  • 拖动向下时,底部菜单不会关闭
  • 如果在对话框窗口之外点击,则底部菜单将关闭

代码:

class MyBottomSheet : BottomSheetDialogFragment () {

   override fun onActivityCreated(savedInstanceState: Bundle?) {
       super.onActivityCreated(savedInstanceState)
       disableBottomSheetDraggableBehavior()
   }

   private fun disableBottomSheetDraggableBehavior() {
      this.isCancelable = false
      this.dialog?.setCanceledOnTouchOutside(true)
   }

 }

由于某些原因,我无法通过触摸外部关闭对话框,但禁用拖动是有效的。 - Gastón Saillén

8

接受的答案在我使用的第一个测试设备上不起作用。反弹效果也不够流畅。似乎最好在用户释放拖动后才将状态设置为STATE_EXPANDED。以下是我的版本:

    final BottomSheetBehavior behavior = BottomSheetBehavior.from(findViewById(R.id.bottomSheet));
    behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
        @Override
        public void onStateChanged(@NonNull View bottomSheet, int newState) {
            if (newState > BottomSheetBehavior.STATE_DRAGGING)
                bottomSheet.post(new Runnable() {
                    @Override public void run() {
                        behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
                    }
                });
        }

        @Override
        public void onSlide(@NonNull View bottomSheet, float slideOffset) {
        }
    });

1
让我告诉你将其放入可运行文件中的问题,除非这正是你想要的。你无法使用按钮来解除它,因为它需要拖动才能解除。而且,它总是会响应拖动,只是它会阻止用户拖动以解除。 - Tonespy

7

使用 'com.google.android.material:material:1.2.0-alpha06'

可以很好地与 NestedScrollViewRecyclerView 搭配使用

示例代码:

    LinearLayout contentLayout = findViewById(R.id.contentLayout);
    sheetBehavior = BottomSheetBehavior.from(contentLayout);
    sheetBehavior.setDraggable(false);

sheetBehavior.setDraggable(false); 这一行代码在onViewCreated方法中完成了工作。干杯! - undefined

7
将以下代码添加到 BottomSheetBehavior 对象中。拖动将被禁用。 对我来说运行得很好。
final BottomSheetBehavior behavior = BottomSheetBehavior.from((View) view.getParent());
    behavior.setHideable(false);
    behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {

      @Override
      public void onStateChanged(@NonNull View bottomSheet, int newState) {
        if (newState == BottomSheetBehavior.STATE_DRAGGING) {
          behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
        }

      }
      @Override
      public void onSlide(@NonNull View bottomSheet, float slideOffset) {

      }
});

1
这并不会禁用滑动操作。它会完全折叠底部工作表。 - AdamHurwitz

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