在外部区域达到阈值后,允许BottomSheet向上滑动

时间:2019-03-08 17:57:22

标签: android google-maps bottomnavigationview bottom-sheet

我正在尝试复制当前Google地图所具有的一种行为,即当从底部栏向上滑动时,底部页面可以显示出来。 请注意,在下面的记录中,我首先点击底部栏上的一个按钮,然后向上滑动,这又将其后面的薄片暴露出来。

enter image description here

我在任何地方都找不到解释如何实现这样的目标。我曾尝试探索BottomSheetBehavior并对其进行自定义,但无处可寻觅一种方法来跟踪初始点击,然后在达到触摸倾斜阈值时让工作表接管运动。

如何在不依靠库的情况下实现此行为?还是有任何官方的Google / Android视图允许两个部分(导航栏和底部)之间的这种行为?

3 个答案:

答案 0 :(得分:0)

您可以尝试这样的操作(它是伪代码,希望您理解我的意思):

<FrameLayout id="+id/bottomSheet">

    <View id="exploreNearby bottomMargin="buttonContainerHeight/>
    <LinearLayout>
        <Button id="explore"/>
        <Button id="explore"/>
        <Button id="explore"/>
    </LinearLayout>

    <View width="match" height="match" id="+id/touchCatcher"

</FrameLayout>

在覆盖onTouch()的bottomSheet视图上添加一个手势检测器。它使用SimpleOnGestureListener等待“滚动”事件-除了滚动事件以外的所有事件,您都可以照常向下复制到视图。

在滚动事件中,您可以将explorerNearby扩展为增量(确保其不会递归或变高或变低)。

答案 1 :(得分:0)

Bottom sheet类已经为您完成此操作。只需将其窥视高度设置为0,它就应该已经侦听了向上滑动的手势。

但是,我不确定它是否可以在0的窥视高度下工作。因此,如果不起作用,只需将窥视高度设置为20dp,并使底板布局的顶部透明,这样就不会可见。

那应该可以解决问题,除非我误解了您的问题。如果您的目标是仅需轻敲底部并向上滑动,即可调出应该非常简单的底部。

您“可能”遇到的一个可能问题是,由于按钮已经消耗了底页而导致底页没有收到触摸事件。如果发生这种情况,您将需要为整个屏幕创建一个触摸处理程序,并每次都返回要处理的“ true”,然后将触摸事件转发到基础视图,这样当您超出底部选项卡的阈值时栏,您开始将触摸事件发送到底部工作表布局,而不是标签栏。

听起来很难。大多数类都具有onTouch,您只需将其转发即可。但是,如果不按照我在前两种情况中描述的方式对您不起作用,那就走那条路吧。

最后,另一个可行的选择是将标签按钮创建为bottomSheetLayout的一部分,并使窥视高度等于标签栏。然后确保选项卡栏限制在底部父页面的底部,以便在向上滑动时仅停留在底部。这样一来,您就可以单击按钮或获得免费的底页行为。

快乐编码!

答案 2 :(得分:0)

花些时间,但我发现了一个基于两位作者提供的示例和讨论的解决方案,可以在这里找到他们的贡献:

https://gist.github.com/davidliu/c246a717f00494a6ad237a592a3cea4f

https://github.com/gavingt/BottomSheetTest

基本逻辑是在自定义onInterceptTouchEvent中处理BottomSheetBehavior中的触摸事件,并检查CoordinatorLayout中给定视图(从现在起名为proxy view)是否为isPointInChildBounds中其余的触摸委托感兴趣。 如果需要,可以调整为使用多个代理视图,唯一必要的更改是制作一个代理视图列表并迭代该列表,而不是使用单个代理视图引用。

下面是此实现的代码示例。请注意,这仅配置为处理垂直运动,如果需要水平运动,则根据您的需要修改代码。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<com.example.tabsheet.CustomCoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/customCoordinatorLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="@android:color/darker_gray">

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:icon="@drawable/ic_launcher_background"
            android:text="Tab 1" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:icon="@drawable/ic_launcher_background"
            android:text="Tab 2" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:icon="@drawable/ic_launcher_background"
            android:text="Tab 3" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:icon="@drawable/ic_launcher_background"
            android:text="Tab 4" />

        <com.google.android.material.tabs.TabItem
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:icon="@drawable/ic_launcher_background"
            android:text="Tab 5" />

    </com.google.android.material.tabs.TabLayout>

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:id="@+id/bottomSheet"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#3F51B5"
        android:clipToPadding="false"
        app:behavior_peekHeight="0dp"
        app:layout_behavior=".CustomBottomSheetBehavior" />

</com.example.tabsheet.CustomCoordinatorLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        final CustomCoordinatorLayout customCoordinatorLayout;
        final CoordinatorLayout bottomSheet;
        final TabLayout tabLayout;

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        customCoordinatorLayout = findViewById(R.id.customCoordinatorLayout);
        bottomSheet = findViewById(R.id.bottomSheet);
        tabLayout = findViewById(R.id.tabLayout);

        iniList(bottomSheet);
        customCoordinatorLayout.setProxyView(tabLayout);

    }

    private void iniList(final ViewGroup parent) {

        @ColorInt int backgroundColor;
        final int padding;
        final int maxItems;
        final float density;
        final NestedScrollView nestedScrollView;
        final LinearLayout linearLayout;
        final ColorDrawable dividerDrawable;
        int i;
        TextView textView;
        ViewGroup.LayoutParams layoutParams;

        density = Resources.getSystem().getDisplayMetrics().density;
        padding = (int) (20 * density);
        maxItems = 50;
        backgroundColor = ContextCompat.getColor(this, android.R.color.holo_blue_bright);
        dividerDrawable = new ColorDrawable(Color.WHITE);

        layoutParams = new ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT
        );

        nestedScrollView = new NestedScrollView(this);
        nestedScrollView.setLayoutParams(layoutParams);
        nestedScrollView.setClipToPadding(false);
        nestedScrollView.setBackgroundColor(backgroundColor);

        linearLayout = new LinearLayout(this);
        linearLayout.setLayoutParams(layoutParams);
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
        linearLayout.setDividerDrawable(dividerDrawable);

        for (i = 0; i < maxItems; i++) {

            textView = new TextView(this);
            textView.setText("Item " + (1 + i));
            textView.setPadding(padding, padding, padding, padding);

            linearLayout.addView(textView, layoutParams);

        }

        nestedScrollView.addView(linearLayout);
        parent.addView(nestedScrollView);

    }

}

CustomCoordinatorLayout.java

public class CustomCoordinatorLayout extends CoordinatorLayout {

    private View proxyView;

    public CustomCoordinatorLayout(@NonNull Context context) {
        super(context);
    }

    public CustomCoordinatorLayout(
        @NonNull Context context,
        @Nullable AttributeSet attrs
    ) {
        super(context, attrs);
    }

    public CustomCoordinatorLayout(
        @NonNull Context context,
        @Nullable AttributeSet attrs,
        int defStyleAttr
    ) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean isPointInChildBounds(
        @NonNull View child,
        int x,
        int y
    ) {

        if (super.isPointInChildBounds(child, x, y)) {
            return true;
        }

        // we want to intercept touch events if they are
        // within the proxy view bounds, for this reason
        // we instruct the coordinator layout to check
        // if this is true and let the touch delegation
        // respond to that result
        if (proxyView != null) {
            return super.isPointInChildBounds(proxyView, x, y);
        }

        return false;

    }

    // for this example we are only interested in intercepting
    // touch events for a single view, if more are needed use
    // a List<View> viewList instead and iterate in 
    // isPointInChildBounds
    public void setProxyView(View proxyView) {
        this.proxyView = proxyView;
    }

}

CustomBottomSheetBehavior.java

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

    // we'll use the device's touch slop value to find out when a tap
    // becomes a scroll by checking how far the finger moved to be
    // considered a scroll. if the finger moves more than the touch
    // slop then it's a scroll, otherwise it is just a tap and we
    // ignore the touch events
    private int touchSlop;
    private float initialY;
    private boolean ignoreUntilClose;

    public CustomBottomSheetBehavior(
        @NonNull Context context,
        @Nullable AttributeSet attrs
    ) {
        super(context, attrs);
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    public boolean onInterceptTouchEvent(
        @NonNull CoordinatorLayout parent,
        @NonNull V child,
        @NonNull MotionEvent event
    ) {

        // touch events are ignored if the bottom sheet is already
        // open and we save that state for further processing
        if (getState() == STATE_EXPANDED) {

            ignoreUntilClose = true;
            return super.onInterceptTouchEvent(parent, child, event);

        }

        switch (event.getAction()) {

            // this is the first event we want to begin observing
            // so we set the initial value for further processing
            // as a positive value to make things easier
            case MotionEvent.ACTION_DOWN:
                initialY = Math.abs(event.getRawY());
                return super.onInterceptTouchEvent(parent, child, event);

            // if the last bottom sheet state was not open then
            // we check if the current finger movement has exceed
            // the touch slop in which case we return true to tell
            // the system we are consuming the touch event
            // otherwise we let the default handling behavior
            // since we don't care about the direction of the
            // movement we ensure its difference is a positive
            // integer to simplify the condition check
            case MotionEvent.ACTION_MOVE:
                return !ignoreUntilClose
                    && Math.abs(initialY - Math.abs(event.getRawY())) > touchSlop
                    || super.onInterceptTouchEvent(parent, child, event);

            // once the tap or movement is completed we reset
            // the initial values to restore normal behavior
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                initialY = 0;
                ignoreUntilClose = false;
                return super.onInterceptTouchEvent(parent, child, event);

        }

        return super.onInterceptTouchEvent(parent, child, event);

    }

}

带有透明状态栏和导航栏的结果有助于可视化向上滑动的底部工作表,但由于上面的代码与该问题无关,因此被排除在上面的代码之外。

注意:如果您的底部工作表布局包含某种NestedScrollView可以使用的可滚动视图类型(例如,CoordinatorLayout),则可能甚至不需要自定义的底部工作表行为。 },因此请在布局准备就绪后尝试不使用自定义底页行为,因为这样做会更简单。

example