首页 文章

使用官方支持库23.x.向上滑动图像像谷歌 Map 一样的bottomSheet

提问于
浏览
39

Update
我想完成谷歌 Map 所具有的相同行为 with Support Library 23.x.+ and without ANY 3rd library

注意:这不是一个重复的问题,因为:

  • 我想使用行为,支持库和没有任何第三方库(我在问题 Headers 和上面的描述中添加了它)

  • 我想在下一个gif中看到 ALL behaviors ,其他问题是要求一两个行为并使用任何方式来实现它 .

like you can see in this gif

我已经有官方的bottomSheet工作(甚至在选项卡和视图寻呼机内) .

是什么让我发疯 is how to achieve the image behavior that come up from the BottomSheet when sliding up using official bottomSheet? .

我尝试使用像FAB这样的锚没有成功 .
我读了一些关于使用滚动监听器的内容,但是ppl说它不像谷歌 Map 那样平滑和快速 .

我的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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.MasterActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay"
            app:layout_scrollFlags="scroll|enterAlways|snap">

            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                style="?android:attr/borderlessButtonStyle"
                android:text="Departure"
                android:layout_gravity="center"
                android:id="@+id/buttonToolBar"
                />


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

        <android.support.design.widget.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:tabBackground="@android:color/white"
            app:tabTextColor="@color/colorAccent"
            app:tabSelectedTextColor="@color/colorAccent"/>

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

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />


    <android.support.v4.widget.NestedScrollView
        android:id="@+id/asdf"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:behavior_peekHeight="100dp"
        android:fitsSystemWindows="true"
            app:layout_behavior="android.support.design.widget.BottomSheetBehavior">

        <LinearLayout
            android:id="@+id/qwert"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:paddingBottom="16dp"
            android:background="@android:color/white"
            android:padding="15dp">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="BOOTOMSHEET TITLE"
                    android:textAppearance="@style/TextAppearance.AppCompat.Title" />

            <Button
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Button1"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="text 2"
                android:layout_margin="10dp"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="text 3"
                android:layout_margin="10dp"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="text 4"
                android:layout_margin="10dp"/>


            <FrameLayout
                android:layout_width="match_parent"
                android:layout_height="320dp"
                android:background="@color/colorAccent">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:text="Your remaining content here"
                    android:textColor="@android:color/white" />

            </FrameLayout>
        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>


    <android.support.design.widget.FloatingActionButton
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        app:layout_anchor="@id/asdf"
        app:layout_anchorGravity="top|right|end"
        android:src="@drawable/abc_ic_search_api_mtrl_alpha_copy"
        android:layout_margin="@dimen/fab_margin"
        android:clickable="true"/>

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

2 回答

  • 67

    如果要使用Support Library 23.4.0实现它 . 我会告诉你我是如何得到它以及它是如何工作的 .

    注意:我为我的英语道歉,我试着给出一个编程答案(只是简短并且混合了有用的代码)但似乎不够好......

    到目前为止,我可以看到活动/片段具有以下行为:

    • 2个带有动画的工具栏,可响应底部的纸张移动 .

    • FAB在靠近"modal toolbar"(您向上滑动时出现的那个)时隐藏 .

    • 底部背后的背景图像,具有某种视差效果 .

    • 工具栏中的 Headers (TextView),当底页到达时显示 .

    • 通知satus栏可以将其背景变为透明或全彩 .

    • 具有"anchor"状态的自定义底部工作表行为 .

    注2:这个答案谈论6件不是1或2的事情,就像其他问题一样, can you see the difference now?

    好的,现在让我们逐一检查一下:

    ToolBars
    当您在谷歌 Map 中打开该视图时,您可以看到可以搜索的工具栏,它不会像谷歌 Map 那样做,因为我想做的更通用 . 无论如何 ToolBarAppBarLayout 内部,当你开始拖动BottomSheet时它被隐藏了,当BottomSheet到达 COLLAPSED 状态时它再次出现 .
    要实现它,您需要:

    • 创建一个 Behavior 并从 AppBarLayout.ScrollingViewBehavior 扩展它

    • 覆盖 layoutDependsOnonDependentViewChanged 方法 . 这样做你将听取bottomSheet的动作 .

    • 创建一些方法来隐藏和取消隐藏带有动画的AppBarLayout / ToolBar .

    这是我为第一个工具栏或ActionBar做的方式:

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof NestedScrollView;
    }
    
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
                                          View dependency) {
    
        if (mChild == null) {
            initValues(child, dependency);
            return false;
        }
    
        float dVerticalScroll = dependency.getY() - mPreviousY;
        mPreviousY = dependency.getY();
    
        //going up
        if (dVerticalScroll <= 0 && !hidden) {
            dismissAppBar(child);
            return true;
        }
    
        return false;
    }
    
    private void initValues(final View child, View dependency) {
    
        mChild = child;
        mInitialY = child.getY();
    
        BottomSheetBehaviorGoogleMapsLike bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(dependency);
        bottomSheetBehavior.addBottomSheetCallback(new BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
            @Override
            public void onStateChanged(@NonNull View bottomSheet, @BottomSheetBehaviorGoogleMapsLike.State int newState) {
                if (newState == BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED ||
                        newState == BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN)
                    showAppBar(child);
            }
    
            @Override
            public void onSlide(@NonNull View bottomSheet, float slideOffset) {
    
            }
        });
    }
    
    private void dismissAppBar(View child){
        hidden = true;
        AppBarLayout appBarLayout = (AppBarLayout)child;
        mToolbarAnimation = appBarLayout.animate().setDuration(mContext.getResources().getInteger(android.R.integer.config_shortAnimTime));
        mToolbarAnimation.y(-(mChild.getHeight()+25)).start();
    }
    
    private void showAppBar(View child) {
        hidden = false;
        AppBarLayout appBarLayout = (AppBarLayout)child;
        mToolbarAnimation = appBarLayout.animate().setDuration(mContext.getResources().getInteger(android.R.integer.config_mediumAnimTime));
        mToolbarAnimation.y(mInitialY).start();
    }
    

    the complete file if you need it

    第二个工具栏或"Modal"工具栏:
    你必须覆盖相同的方法,但在这个方法中你必须要注意更多的行为:

    • 用动画显示/隐藏工具栏

    • 更改statur栏颜色/背景

    • 显示/隐藏工具栏中的BottomSheet Headers

    • 关闭bottomSheet或将其发送到折叠状态

    这个的代码有点广泛所以我会让the link

    The FAB

    这也是一种自定义行为,但是从 FloatingActionButton.Behavior 延伸 . 在 onDependentViewChanged 中,您必须查看它到达"offSet"或您要隐藏它的位置 . 在我的情况下,我想隐藏它靠近第二个工具栏,所以我深入FAB父(一个CoordiantorLayout)寻找包含ToolBar的AppBarLayout,然后我使用像 OffSet 的ToolBar位置:

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, View dependency) {
    
        if (offset == 0)
            setOffsetValue(parent);
    
        if (dependency.getY() <=0)
            return false;
    
        if (child.getY() <= (offset + child.getHeight()) && child.getVisibility() == View.VISIBLE)
            child.hide();
        else if (child.getY() > offset && child.getVisibility() != View.VISIBLE)
            child.show();
    
        return false;
    }
    

    Complete Custom FAB Behavior link

    The Image behind the BottomSheet with parallax effect
    像其他人一样,它是一个自定义行为,这个中唯一的"complicated"事件是一个小算法,它将图像锚定到BottomSheet并避免图像崩溃,如默认视差效果:

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
                                          View dependency) {
    
        if (mYmultiplier == 0) {
            initValues(child, dependency);
            return true;
        }
    
        float dVerticalScroll = dependency.getY() - mPreviousY;
        mPreviousY = dependency.getY();
    
        //going up
        if (dVerticalScroll <= 0 && child.getY() <= 0) {
            child.setY(0);
            return true;
        }
    
        //going down
        if (dVerticalScroll >= 0 && dependency.getY() <= mImageHeight)
            return false;
    
        child.setY( (int)(child.getY() + (dVerticalScroll * mYmultiplier) ) );
    
        return true;
    }
    

    complete file for backdrop Image with parallax effect

    现在结束: The Custom BottomSheet Behavior
    要首先完成3个步骤,您需要了解默认BottomSheetBehavior有5种状态: STATE_DRAGGING, STATE_SETTLING, STATE_EXPANDED, STATE_COLLAPSED, STATE_HIDDEN 对于Google Maps行为,您需要在折叠和展开之间添加中间状态: STATE_ANCHOR_POINT .
    我尝试扩展默认的bottomSheetBehavior没有成功,所以我只是复制粘贴所有代码并修改我需要的东西 .
    要实现我正在谈论的内容,请按照以下步骤操作:

    • 创建一个Java类并从 CoordinatorLayout.Behavior<V> 扩展它

    • 将默认 BottomSheetBehavior 文件中的粘贴代码复制到新文件中 .

    • 使用以下代码修改方法 clampViewPositionVertical

    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
        return constrain(top, mMinOffset, mHideable ? mParentHeight : mMaxOffset);
    }
    int constrain(int amount, int low, int high) {
        return amount < low ? low : (amount > high ? high : amount);
    }
    
    • 添加新状态

    public static final int STATE_ANCHOR_POINT = X;

    • 修改下一个方法: onLayoutChildonStopNestedScrollBottomSheetBehavior<V> from(V view)setState (可选)
    public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
        // First let the parent lay it out
        if (mState != STATE_DRAGGING && mState != STATE_SETTLING) {
            if (ViewCompat.getFitsSystemWindows(parent) &&
                    !ViewCompat.getFitsSystemWindows(child)) {
                ViewCompat.setFitsSystemWindows(child, true);
            }
            parent.onLayoutChild(child, layoutDirection);
        }
        // Offset the bottom sheet
        mParentHeight = parent.getHeight();
        mMinOffset = Math.max(0, mParentHeight - child.getHeight());
        mMaxOffset = Math.max(mParentHeight - mPeekHeight, mMinOffset);
    
        //if (mState == STATE_EXPANDED) {
        //    ViewCompat.offsetTopAndBottom(child, mMinOffset);
        //} else if (mHideable && mState == STATE_HIDDEN...
        if (mState == STATE_ANCHOR_POINT) {
            ViewCompat.offsetTopAndBottom(child, mAnchorPoint);
        } else if (mState == STATE_EXPANDED) {
            ViewCompat.offsetTopAndBottom(child, mMinOffset);
        } else if (mHideable && mState == STATE_HIDDEN) {
            ViewCompat.offsetTopAndBottom(child, mParentHeight);
        } else if (mState == STATE_COLLAPSED) {
            ViewCompat.offsetTopAndBottom(child, mMaxOffset);
        }
        if (mViewDragHelper == null) {
            mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);
        }
        mViewRef = new WeakReference<>(child);
        mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
        return true;
    }
    
    
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
        if (child.getTop() == mMinOffset) {
            setStateInternal(STATE_EXPANDED);
            return;
        }
        if (target != mNestedScrollingChildRef.get() || !mNestedScrolled) {
            return;
        }
        int top;
        int targetState;
        if (mLastNestedScrollDy > 0) {
            //top = mMinOffset;
            //targetState = STATE_EXPANDED;
            int currentTop = child.getTop();
            if (currentTop > mAnchorPoint) {
                top = mAnchorPoint;
                targetState = STATE_ANCHOR_POINT;
            }
            else {
                top = mMinOffset;
                targetState = STATE_EXPANDED;
            }
        } else if (mHideable && shouldHide(child, getYVelocity())) {
            top = mParentHeight;
            targetState = STATE_HIDDEN;
        } else if (mLastNestedScrollDy == 0) {
            int currentTop = child.getTop();
            if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) {
                top = mMinOffset;
                targetState = STATE_EXPANDED;
            } else {
                top = mMaxOffset;
                targetState = STATE_COLLAPSED;
            }
        } else {
            //top = mMaxOffset;
            //targetState = STATE_COLLAPSED;
            int currentTop = child.getTop();
            if (currentTop > mAnchorPoint) {
                top = mMaxOffset;
                targetState = STATE_COLLAPSED;
            }
            else {
                top = mAnchorPoint;
                targetState = STATE_ANCHOR_POINT;
            }
        }
        if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
            setStateInternal(STATE_SETTLING);
            ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState));
        } else {
            setStateInternal(targetState);
        }
        mNestedScrolled = false;
    }
    
    public final void setState(@State int state) {
        if (state == mState) {
            return;
        }
        if (mViewRef == null) {
            // The view is not laid out yet; modify mState and let onLayoutChild handle it later
            /**
             * New behavior (added: state == STATE_ANCHOR_POINT ||)
             */
            if (state == STATE_COLLAPSED || state == STATE_EXPANDED ||
                    state == STATE_ANCHOR_POINT ||
                    (mHideable && state == STATE_HIDDEN)) {
                mState = state;
            }
            return;
        }
        V child = mViewRef.get();
        if (child == null) {
            return;
        }
        int top;
        if (state == STATE_COLLAPSED) {
            top = mMaxOffset;
        } else if (state == STATE_ANCHOR_POINT) {
            top = mAnchorPoint;
        } else if (state == STATE_EXPANDED) {
            top = mMinOffset;
        } else if (mHideable && state == STATE_HIDDEN) {
            top = mParentHeight;
        } else {
            throw new IllegalArgumentException("Illegal state argument: " + state);
        }
        setStateInternal(STATE_SETTLING);
        if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
            ViewCompat.postOnAnimation(child, new SettleRunnable(child, state));
        }
    }
    
    
    public static <V extends View> BottomSheetBehaviorGoogleMapsLike<V> from(V view) {
        ViewGroup.LayoutParams params = view.getLayoutParams();
        if (!(params instanceof CoordinatorLayout.LayoutParams)) {
            throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
        }
        CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params)
                .getBehavior();
        if (!(behavior instanceof BottomSheetBehaviorGoogleMapsLike)) {
            throw new IllegalArgumentException(
                    "The view is not associated with BottomSheetBehaviorGoogleMapsLike");
        }
        return (BottomSheetBehaviorGoogleMapsLike<V>) behavior;
    }
    

    link to the hole project在其中您可以看到所有自定义行为

    注3:下次添加评论,以礼貌的方式询问答案的变化,或者问为什么这个答案比其他人在关闭相同主题之前的答案还有其他答案,或者标记为重复 .

    And here is how its looks like:
    [
    CustomBottomSheetBehavior
    ]

  • 1

    您可以使用协调器布局行为来实现此效果 . 您将需要扩展一个CoordinatorLayout.Behaviour类,并在协调器布局中的一个视图上编写一个依赖项,保持您的图像包含视图作为子项 . 使其简单您需要附加自定义写入对包含视图的图像的行为 . 有关编写自定义行为的帮助,请点击链接Writing custom behaviours

相关问题