在外部区域达到阈值后,允许BottomSheet向上滑动 [英] Allow BottomSheet to slide up after threshold is reached on an area outside

查看:84
本文介绍了在外部区域达到阈值后,允许BottomSheet向上滑动的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试复制当前Google Maps的行为,该行为允许从底部栏向上滑动时显示底部表单.请注意,在下面的记录中,我首先点击底部栏上的一个按钮,然后向上滑动,这又将其后面的薄片暴露出来.

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

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

解决方案

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

I am trying to replicate a behavior that the current Google Maps has which allows the bottom sheet to be revealed when sliding up from the bottom bar. Notice in the recording below that I first tap on one of the buttons at the bottom bar and then slide up, which in turn reveals the sheet behind it.

I cannot find anywhere explained how something like this can be achieved. I tried exploring the BottomSheetBehavior and customizing it, but nowhere I can find a way to track the initial tap and then let the sheet take over the movement once the touch slop threshold is reached.

How can I achieve this behavior without resorting to libraries? Or are there any official Google/Android views that allow this behavior between two sections (the navigation bar and bottom sheet)?

解决方案

Took some time but I found a solution based on examples and discussion provided by two authors, their contributions can be found here:

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

https://github.com/gavingt/BottomSheetTest

The basic logic is to handle touch events in onInterceptTouchEvent in a custom BottomSheetBehavior and check in a CoordinatorLayout if the given view (from now on named proxy view) is of interest for the rest of the touch delegation in isPointInChildBounds. This can be adapted to use more than one proxy view if needed, the only change necessary for this is to make a proxy view list and iterate the list instead of using a single proxy view reference.

Below follows the code example of this implementation. Do note that this is only configured to handle vertical movements, if horizontal movements are necessary then adapt the code to your need.

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);

    }

}

Result with transparent status bar and navigation bar to help visualize the bottom sheet sliding up, but excluded from the code above since it was not relevant for this question.

Note: It is possible you might not even need a custom bottom sheet behavior if your bottom sheet layout contains a certain scrollable view type (NestedScrollView for example) that can be used as is by the CoordinatorLayout, so try without the custom bottom sheet behavior once your layout is ready since it will make this simpler.

这篇关于在外部区域达到阈值后,允许BottomSheet向上滑动的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆