BottomSheetBehavior+ViewPager+多RecyclerView 滑动冲突(滑动弹出隐藏)终极解决方案

最新示例见:https://github.com/ZYF99/BottomSheetTest.git

示例新增解决方法+弹窗回弹+回弹速度调节,本文暂未对示例内容进行更新,敬请期待!

最终效果

滑动冲突_1.gif

滑动冲突_2.gif

使用BottomSheetBehavior引发的问题

问题1:BottomSheetBehavior+ViewPager+多页RecyclerView组合,只有第一页列表可滑动

CoordinatorLayout中对弹出的ViewGroup直接使用 com.google.android.material.bottomsheet.BottomSheetBehavior,本身是没有问题的,但当我们嵌套了ViewPager+多页RecyclerView这个组合,就会导致只有第一页RecyclerView能滑动,其余页的滑动事件全部被behavior处理掉了,也就是滑动全部成为了弹出隐藏你得弹出框,很显然,列表是需要先于弹出框消费滑动的,于是我们看看BottomSheetBehavior的源码,如下:

  @Nullable
  @VisibleForTesting
  View findScrollingChild(View view) {
    if (ViewCompat.isNestedScrollingEnabled(view)) {
      return view;
    }
    if (view instanceof ViewGroup) {
      ViewGroup group = (ViewGroup) view;
      for (int i = 0, count = group.getChildCount(); i < count; i++) {
        View scrollingChild = findScrollingChild(group.getChildAt(i));
        if (scrollingChild != null) {
          return scrollingChild;
        }
      }
    }
    return null;
  }

关键的一段代码,在寻找滑动View的时候,对ViewGroup子View进行了遍历,再递归的寻找子View下可滑动的控件,当找到第一个可滑动控件时,将其返回,作为消费滑动事件的控件。
这里就明白了只有第一个RecyclerView能滑动的原因:无论findScrollingChild什么时机被触发,永远都只会返回ViewPager的第一页中的RecyclerView。知道了原因,修改就很简单了:将ViewPager拿出来,单独进行一波单独遍历。那么我们新建一个MyViewPagerBottomSheetBehavior.java,将BottomSheetBehavior代码拷贝,改造findScrollingChild后如下:

    @Nullable
    @VisibleForTesting
    View findScrollingChild(View view) {
        if (ViewCompat.isNestedScrollingEnabled(view)) {
            return view;
        }
        if (view instanceof ViewPager) {
            ViewPager viewPager = (ViewPager) view;
            View currentViewPagerChild = ViewPagerUtils.getCurrentView(viewPager); //通过ViewPagerUtils找到当前在界面上的页面
            View scrollingChild = findScrollingChild(currentViewPagerChild);
            if (scrollingChild != null) {
                return scrollingChild;
            }
            return currentViewPagerChild;
        }

        if (view instanceof ViewGroup) {
            ViewGroup group = (ViewGroup) view;
            for (int i = 0, count = group.getChildCount(); i < count; i++) {
                View scrollingChild = findScrollingChild(group.getChildAt(i));
                if (scrollingChild != null) {
                    return scrollingChild;
                }
            }
        }
        return null;
    }

第一个问题解决了~

问题2:滑动事件全部被 RecyclerView 消费掉,滑动弹出和关闭功能消失了

导致这个问题的原因也很简单,我们的滑动事件在 RecyclerView 加入前,都是由 BottomSheetBehavior 来消费的,当我们加入 RecyclerView 这种可滑动控件后,滑动事件都被其消费,这与 ViewPager 无关。

多数情况下我们需要 RecyclerView 消费事件(滑动),但我们同时希望当 RecyclerView 滑动到顶部时,将事件又重新交给 Behavior 消费,这样就可以做到,列表在顶部时滑动开启/关闭弹出框
实现需要分为以下几步

  • Behavior 添加 Flag ,标记列表是否滑动到最顶端
    由于需要适配ViewPager多页情况,一个Flag不能解决,需要一个HashMap,将页数与Flag关联起来
    //针对viewpager联动
    boolean isFirstFind = true; //第一次寻找联动View,为viewPager添加滑动监听
    private HashMap isScrollViewOnTopMap = new HashMap<>(); //关联页数与能否滑动flag的Map
    private View globalView;//保存下来的view,方便刷新寻找滑动控件时使用
    private int currentPagePosition = 0; //当前ViewPager的页数
    @Nullable
    @VisibleForTesting
    View findScrollingChild(View view) {
     ..........................
     }
  • 定义一个刷新滑动控件的方法,仅供 MyViewPagerBehavior 内部使用
    private void notifyScrollView() {
        nestedScrollingChildRef = new WeakReference<>(findScrollingChild(globalView));
    }
  • MyViewPagerBehavior内对外暴露一个设置Flag的方法
    public void setCurrentScrollViewOnTop(boolean scrollViewOnTop) {
        isScrollViewOnTopMap.put(currentPagePosition, scrollViewOnTop);
        Log.d("~~~~~~~~~",isScrollViewOnTopMap.toString());
        notifyScrollView();
    }
  • 更改 findScrollingChild(View view) 方法,适配列表在顶部时,滑动由 behavior 处理
    @Nullable
    @VisibleForTesting
    View findScrollingChild(View view) {
        if(view==null){
            return null;
        }
        globalView = view; //为全局view赋值
        boolean b = isScrollViewOnTopMap.getOrDefault(currentPagePosition,false );//列表是否处于顶部
        if (b) {//处于顶部,返回null,代表内部滑动控件不消费任何滑动事件,交由behavior处理
            return null;
        }

        if (ViewCompat.isNestedScrollingEnabled(view)) {
            return view;
        }
        if (view instanceof ViewPager) {
            ViewPager viewPager = (ViewPager) view;
            if (isFirstFind) {//初次寻找滑动view,添加viewpager翻页监听
                for (int i = 0; i < Objects.requireNonNull(viewPager.getAdapter()).getCount(); i++) {
                    isScrollViewOnTopMap.put(i,true); //将所有顶部标记置为false,默认不在顶部
                }
                viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
                    @Override
                    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                        //翻页后,将现在的页数更新
                        currentPagePosition = position;
                    }

                    @Override
                    public void onPageSelected(int position) {

                    }

                    @Override
                    public void onPageScrollStateChanged(int state) {

                    }
                });
                isFirstFind = false;
            }
            View currentViewPagerChild = ViewPagerUtils.getCurrentView(viewPager); //找到ViewPager当前的view
            View scrollingChild = findScrollingChild(currentViewPagerChild);
            if (scrollingChild != null) {
                return scrollingChild;
            }
            return currentViewPagerChild;
        }
  • 监听 RecyclerView 滑动,在对顶部时,通知 Behavior
    这是在Fragment或者Activity中,你得behavior一定要自己去取,可能就在你得 Fragment/Activity 中通过MyViewPagerBehavior.from()就能获取,也可能你要从其他 ActivityFragment 注入进来,具体只有你自己了解;不要忘了:
    • behavior变量类型一定要写刚才创建的 MyViewPagerBottomSheetBehavior
    • xml中更换behavior为 MyViewPagerBottomSheetBehavior
        //上拉加载
        binding.rvList.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                if (behavior != null) {
                    Log.d("!!!!!!!!!", String.valueOf(!recyclerView.canScrollVertically(-1)) + dy);
                    if (dy == 0) {
                        //指尖未真实上下滑动(针对从viewpager其他页面切换过来时),不做任何操作
                        return;
                    }
                    if (!recyclerView.canScrollVertically(-1) && dy < 0) {
                        //不可以下拉,并且手势是下拉,通知behavior已经列表已经在顶部了
                        Log.d("~~~~~", "划不动了");
                        behavior.setCurrentScrollViewOnTop(true);
                    } else {
                        //可以下拉或者手势不是下拉,通知behavior已经列表不在顶部
                        behavior.setCurrentScrollViewOnTop(false);
                    }
                }
            }
        });

贴上完整的MyViewPagerBottomSheetBehavior

package com.xxx.wordingtech.ui.widget;

import com.google.android.material.R;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;

import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Parcel;
import android.os.Parcelable;

import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.core.math.MathUtils;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import androidx.core.view.accessibility.AccessibilityViewCommand;

import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.WindowInsets;

import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams;
import androidx.customview.view.AbsSavedState;
import androidx.customview.widget.ViewDragHelper;
import androidx.viewpager.widget.ViewPager;
import androidx.viewpager.widget.ViewPagerUtils;

import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.resources.MaterialResources;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;


public class MyViewPagerBottomSheetBehavior extends CoordinatorLayout.Behavior {

    /**
     * Callback for monitoring events about bottom sheets.
     */
    public abstract static class BottomSheetCallback {

        /**
         * Called when the bottom sheet changes its state.
         *
         * @param bottomSheet The bottom sheet view.
         * @param newState    The new state. This will be one of {@link #STATE_DRAGGING}, {@link
         *                    #STATE_SETTLING}, {@link #STATE_EXPANDED}, {@link #STATE_COLLAPSED}, {@link
         *                    #STATE_HIDDEN}, or {@link #STATE_HALF_EXPANDED}.
         */
        public abstract void onStateChanged(@NonNull View bottomSheet, @State int newState);

        /**
         * Called when the bottom sheet is being dragged.
         *
         * @param bottomSheet The bottom sheet view.
         * @param slideOffset The new offset of this bottom sheet within [-1,1] range. Offset increases
         *                    as this bottom sheet is moving upward. From 0 to 1 the sheet is between collapsed and
         *                    expanded states and from -1 to 0 it is between hidden and collapsed states.
         */
        public abstract void onSlide(@NonNull View bottomSheet, float slideOffset);
    }

    /**
     * The bottom sheet is dragging.
     */
    public static final int STATE_DRAGGING = 1;

    /**
     * The bottom sheet is settling.
     */
    public static final int STATE_SETTLING = 2;

    /**
     * The bottom sheet is expanded.
     */
    public static final int STATE_EXPANDED = 3;

    /**
     * The bottom sheet is collapsed.
     */
    public static final int STATE_COLLAPSED = 4;

    /**
     * The bottom sheet is hidden.
     */
    public static final int STATE_HIDDEN = 5;

    /**
     * The bottom sheet is half-expanded (used when mFitToContents is false).
     */
    public static final int STATE_HALF_EXPANDED = 6;

    /**
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    @IntDef({
            STATE_EXPANDED,
            STATE_COLLAPSED,
            STATE_DRAGGING,
            STATE_SETTLING,
            STATE_HIDDEN,
            STATE_HALF_EXPANDED
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface State {
    }

    /**
     * Peek at the 16:9 ratio keyline of its parent.
     *
     * 

This can be used as a parameter for {@link #setPeekHeight(int)}. {@link #getPeekHeight()} * will return this when the value is set. */ public static final int PEEK_HEIGHT_AUTO = -1; /** * This flag will preserve the peekHeight int value on configuration change. */ public static final int SAVE_PEEK_HEIGHT = 0x1; /** * This flag will preserve the fitToContents boolean value on configuration change. */ public static final int SAVE_FIT_TO_CONTENTS = 1 << 1; /** * This flag will preserve the hideable boolean value on configuration change. */ public static final int SAVE_HIDEABLE = 1 << 2; /** * This flag will preserve the skipCollapsed boolean value on configuration change. */ public static final int SAVE_SKIP_COLLAPSED = 1 << 3; /** * This flag will preserve all aforementioned values on configuration change. */ public static final int SAVE_ALL = -1; /** * This flag will not preserve the aforementioned values set at runtime if the view is destroyed * and recreated. The only value preserved will be the positional state, e.g. collapsed, hidden, * expanded, etc. This is the default behavior. */ public static final int SAVE_NONE = 0; /** * @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef( flag = true, value = { SAVE_PEEK_HEIGHT, SAVE_FIT_TO_CONTENTS, SAVE_HIDEABLE, SAVE_SKIP_COLLAPSED, SAVE_ALL, SAVE_NONE, }) @Retention(RetentionPolicy.SOURCE) public @interface SaveFlags { } private static final String TAG = "BottomSheetBehavior"; @SaveFlags private int saveFlags = SAVE_NONE; private static final int SIGNIFICANT_VEL_THRESHOLD = 500; private static final float HIDE_THRESHOLD = 0.5f; private static final float HIDE_FRICTION = 0.1f; private static final int CORNER_ANIMATION_DURATION = 500; private boolean fitToContents = true; private boolean updateImportantForAccessibilityOnSiblings = false; private float maximumVelocity; /** * Peek height set by the user. */ private int peekHeight; /** * Whether or not to use automatic peek height. */ private boolean peekHeightAuto; /** * Minimum peek height permitted. */ private int peekHeightMin; /** * True if Behavior has a non-null value for the @shapeAppearance attribute */ private boolean shapeThemingEnabled; private MaterialShapeDrawable materialShapeDrawable; private boolean gestureInsetBottomIgnored; /** * Default Shape Appearance to be used in bottomsheet */ private ShapeAppearanceModel shapeAppearanceModelDefault; private boolean isShapeExpanded; private SettleRunnable settleRunnable = null; @Nullable private ValueAnimator interpolatorAnimator; private static final int DEF_STYLE_RES = R.style.Widget_Design_BottomSheet_Modal; int expandedOffset; int fitToContentsOffset; int halfExpandedOffset; float halfExpandedRatio = 0.5f; int collapsedOffset; float elevation = -1; boolean hideable; private boolean skipCollapsed; private boolean draggable = true; @State int state = STATE_COLLAPSED; @Nullable ViewDragHelper viewDragHelper; private boolean ignoreEvents; private int lastNestedScrollDy; private boolean nestedScrolled; int parentWidth; int parentHeight; @Nullable WeakReference viewRef; @Nullable WeakReference nestedScrollingChildRef; @NonNull private final ArrayList callbacks = new ArrayList<>(); @Nullable private VelocityTracker velocityTracker; int activePointerId; private int initialY; boolean touchingScrollingChild; @Nullable private Map importantForAccessibilityMap; public MyViewPagerBottomSheetBehavior() { } public MyViewPagerBottomSheetBehavior(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BottomSheetBehavior_Layout); this.shapeThemingEnabled = a.hasValue(R.styleable.BottomSheetBehavior_Layout_shapeAppearance); boolean hasBackgroundTint = a.hasValue(R.styleable.BottomSheetBehavior_Layout_backgroundTint); if (hasBackgroundTint) { @SuppressLint("RestrictedApi") ColorStateList bottomSheetColor = MaterialResources.getColorStateList( context, a, R.styleable.BottomSheetBehavior_Layout_backgroundTint); createMaterialShapeDrawable(context, attrs, hasBackgroundTint, bottomSheetColor); } else { createMaterialShapeDrawable(context, attrs, hasBackgroundTint); } createShapeValueAnimator(); if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { this.elevation = a.getDimension(R.styleable.BottomSheetBehavior_Layout_android_elevation, -1); } TypedValue value = a.peekValue(R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight); if (value != null && value.data == PEEK_HEIGHT_AUTO) { setPeekHeight(value.data); } else { setPeekHeight( a.getDimensionPixelSize( R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, PEEK_HEIGHT_AUTO)); } setHideable(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_hideable, false)); setGestureInsetBottomIgnored( a.getBoolean(R.styleable.BottomSheetBehavior_Layout_gestureInsetBottomIgnored, false)); setFitToContents( a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_fitToContents, true)); setSkipCollapsed( a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed, false)); setDraggable(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_draggable, true)); setSaveFlags(a.getInt(R.styleable.BottomSheetBehavior_Layout_behavior_saveFlags, SAVE_NONE)); setHalfExpandedRatio( a.getFloat(R.styleable.BottomSheetBehavior_Layout_behavior_halfExpandedRatio, 0.5f)); value = a.peekValue(R.styleable.BottomSheetBehavior_Layout_behavior_expandedOffset); if (value != null && value.type == TypedValue.TYPE_FIRST_INT) { setExpandedOffset(value.data); } else { setExpandedOffset( a.getDimensionPixelOffset( R.styleable.BottomSheetBehavior_Layout_behavior_expandedOffset, 0)); } a.recycle(); ViewConfiguration configuration = ViewConfiguration.get(context); maximumVelocity = configuration.getScaledMaximumFlingVelocity(); } @NonNull @Override public Parcelable onSaveInstanceState(@NonNull CoordinatorLayout parent, @NonNull V child) { return new SavedState(super.onSaveInstanceState(parent, child), this); } @Override public void onRestoreInstanceState( @NonNull CoordinatorLayout parent, @NonNull V child, @NonNull Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(parent, child, ss.getSuperState()); // Restore Optional State values designated by saveFlags restoreOptionalState(ss); // Intermediate states are restored as collapsed state if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) { this.state = STATE_COLLAPSED; } else { this.state = ss.state; } } @Override public void onAttachedToLayoutParams(@NonNull LayoutParams layoutParams) { super.onAttachedToLayoutParams(layoutParams); // These may already be null, but just be safe, explicitly assign them. This lets us know the // first time we layout with this behavior by checking (viewRef == null). viewRef = null; viewDragHelper = null; } @Override public void onDetachedFromLayoutParams() { super.onDetachedFromLayoutParams(); // Release references so we don't run unnecessary codepaths while not attached to a view. viewRef = null; viewDragHelper = null; } @Override public boolean onLayoutChild( @NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection) { if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child)) { child.setFitsSystemWindows(true); } if (viewRef == null) { // First layout with this behavior. peekHeightMin = parent.getResources().getDimensionPixelSize(R.dimen.design_bottom_sheet_peek_height_min); setSystemGestureInsets(parent); viewRef = new WeakReference<>(child); // Only set MaterialShapeDrawable as background if shapeTheming is enabled, otherwise will // default to android:background declared in styles or layout. if (shapeThemingEnabled && materialShapeDrawable != null) { ViewCompat.setBackground(child, materialShapeDrawable); } // Set elevation on MaterialShapeDrawable if (materialShapeDrawable != null) { // Use elevation attr if set on bottomsheet; otherwise, use elevation of child view. materialShapeDrawable.setElevation( elevation == -1 ? ViewCompat.getElevation(child) : elevation); // Update the material shape based on initial state. isShapeExpanded = state == STATE_EXPANDED; materialShapeDrawable.setInterpolation(isShapeExpanded ? 0f : 1f); } updateAccessibilityActions(); if (ViewCompat.getImportantForAccessibility(child) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); } } if (viewDragHelper == null) { viewDragHelper = ViewDragHelper.create(parent, dragCallback); } int savedTop = child.getTop(); // First let the parent lay it out parent.onLayoutChild(child, layoutDirection); // Offset the bottom sheet parentWidth = parent.getWidth(); parentHeight = parent.getHeight(); fitToContentsOffset = Math.max(0, parentHeight - child.getHeight()); calculateHalfExpandedOffset(); calculateCollapsedOffset(); if (state == STATE_EXPANDED) { ViewCompat.offsetTopAndBottom(child, getExpandedOffset()); } else if (state == STATE_HALF_EXPANDED) { ViewCompat.offsetTopAndBottom(child, halfExpandedOffset); } else if (hideable && state == STATE_HIDDEN) { ViewCompat.offsetTopAndBottom(child, parentHeight); } else if (state == STATE_COLLAPSED) { ViewCompat.offsetTopAndBottom(child, collapsedOffset); } else if (state == STATE_DRAGGING || state == STATE_SETTLING) { ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop()); } nestedScrollingChildRef = new WeakReference<>(findScrollingChild(child)); return true; } @Override public boolean onInterceptTouchEvent( @NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent event) { if (!child.isShown() || !draggable) { ignoreEvents = true; return false; } int action = event.getActionMasked(); // Record the velocity if (action == MotionEvent.ACTION_DOWN) { reset(); } if (velocityTracker == null) { velocityTracker = VelocityTracker.obtain(); } velocityTracker.addMovement(event); switch (action) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: touchingScrollingChild = false; activePointerId = MotionEvent.INVALID_POINTER_ID; // Reset the ignore flag if (ignoreEvents) { ignoreEvents = false; return false; } break; case MotionEvent.ACTION_DOWN: int initialX = (int) event.getX(); initialY = (int) event.getY(); // Only intercept nested scrolling events here if the view not being moved by the // ViewDragHelper. if (state != STATE_SETTLING) { View scroll = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null; if (scroll != null && parent.isPointInChildBounds(scroll, initialX, initialY)) { activePointerId = event.getPointerId(event.getActionIndex()); touchingScrollingChild = true; } } ignoreEvents = activePointerId == MotionEvent.INVALID_POINTER_ID && !parent.isPointInChildBounds(child, initialX, initialY); break; default: // fall out } if (!ignoreEvents && viewDragHelper != null && viewDragHelper.shouldInterceptTouchEvent(event)) { return true; } // We have to handle cases that the ViewDragHelper does not capture the bottom sheet because // it is not the top most view of its parent. This is not necessary when the touch event is // happening over the scrolling content as nested scrolling logic handles that case. View scroll = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null; return action == MotionEvent.ACTION_MOVE && scroll != null && !ignoreEvents && state != STATE_DRAGGING && !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) && viewDragHelper != null && Math.abs(initialY - event.getY()) > viewDragHelper.getTouchSlop(); } @Override public boolean onTouchEvent( @NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent event) { if (!child.isShown()) { return false; } int action = event.getActionMasked(); if (state == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) { return true; } if (viewDragHelper != null) { viewDragHelper.processTouchEvent(event); } // Record the velocity if (action == MotionEvent.ACTION_DOWN) { reset(); } if (velocityTracker == null) { velocityTracker = VelocityTracker.obtain(); } velocityTracker.addMovement(event); // The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it // to capture the bottom sheet in case it is not captured and the touch slop is passed. if (action == MotionEvent.ACTION_MOVE && !ignoreEvents) { if (Math.abs(initialY - event.getY()) > viewDragHelper.getTouchSlop()) { viewDragHelper.captureChildView(child, event.getPointerId(event.getActionIndex())); } } return !ignoreEvents; } @Override public boolean onStartNestedScroll( @NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { lastNestedScrollDy = 0; nestedScrolled = false; return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedPreScroll( @NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { if (type == ViewCompat.TYPE_NON_TOUCH) { // Ignore fling here. The ViewDragHelper handles it. return; } View scrollingChild = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null; if (target != scrollingChild) { return; } int currentTop = child.getTop(); int newTop = currentTop - dy; if (dy > 0) { // Upward if (newTop < getExpandedOffset()) { consumed[1] = currentTop - getExpandedOffset(); ViewCompat.offsetTopAndBottom(child, -consumed[1]); setStateInternal(STATE_EXPANDED); } else { if (!draggable) { // Prevent dragging return; } consumed[1] = dy; ViewCompat.offsetTopAndBottom(child, -dy); setStateInternal(STATE_DRAGGING); } } else if (dy < 0) { // Downward if (!target.canScrollVertically(-1)) { if (newTop <= collapsedOffset || hideable) { if (!draggable) { // Prevent dragging return; } consumed[1] = dy; ViewCompat.offsetTopAndBottom(child, -dy); setStateInternal(STATE_DRAGGING); } else { consumed[1] = currentTop - collapsedOffset; ViewCompat.offsetTopAndBottom(child, -consumed[1]); setStateInternal(STATE_COLLAPSED); } } } dispatchOnSlide(child.getTop()); lastNestedScrollDy = dy; nestedScrolled = true; } @Override public void onStopNestedScroll( @NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int type) { if (child.getTop() == getExpandedOffset()) { setStateInternal(STATE_EXPANDED); return; } if (nestedScrollingChildRef == null || target != nestedScrollingChildRef.get() || !nestedScrolled) { return; } int top; int targetState; if (lastNestedScrollDy > 0) { if (fitToContents) { top = fitToContentsOffset; targetState = STATE_EXPANDED; } else { int currentTop = child.getTop(); if (currentTop > halfExpandedOffset) { top = halfExpandedOffset; targetState = STATE_HALF_EXPANDED; } else { top = expandedOffset; targetState = STATE_EXPANDED; } } } else if (hideable && shouldHide(child, getYVelocity())) { top = parentHeight; targetState = STATE_HIDDEN; } else if (lastNestedScrollDy == 0) { int currentTop = child.getTop(); if (fitToContents) { if (Math.abs(currentTop - fitToContentsOffset) < Math.abs(currentTop - collapsedOffset)) { top = fitToContentsOffset; targetState = STATE_EXPANDED; } else { top = collapsedOffset; targetState = STATE_COLLAPSED; } } else { if (currentTop < halfExpandedOffset) { if (currentTop < Math.abs(currentTop - collapsedOffset)) { top = expandedOffset; targetState = STATE_EXPANDED; } else { top = halfExpandedOffset; targetState = STATE_HALF_EXPANDED; } } else { if (Math.abs(currentTop - halfExpandedOffset) < Math.abs(currentTop - collapsedOffset)) { top = halfExpandedOffset; targetState = STATE_HALF_EXPANDED; } else { top = collapsedOffset; targetState = STATE_COLLAPSED; } } } } else { if (fitToContents) { top = collapsedOffset; targetState = STATE_COLLAPSED; } else { // Settle to nearest height. int currentTop = child.getTop(); if (Math.abs(currentTop - halfExpandedOffset) < Math.abs(currentTop - collapsedOffset)) { top = halfExpandedOffset; targetState = STATE_HALF_EXPANDED; } else { top = collapsedOffset; targetState = STATE_COLLAPSED; } } } startSettlingAnimation(child, targetState, top, false); nestedScrolled = false; } @Override public void onNestedScroll( @NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) { // Overridden to prevent the default consumption of the entire scroll distance. } @Override public boolean onNestedPreFling( @NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View target, float velocityX, float velocityY) { if (nestedScrollingChildRef != null) { return target == nestedScrollingChildRef.get() && (state != STATE_EXPANDED || super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY)); } else { return false; } } /** * @return whether the height of the expanded sheet is determined by the height of its contents, * or if it is expanded in two stages (half the height of the parent container, full height of * parent container). */ public boolean isFitToContents() { return fitToContents; } /** * Sets whether the height of the expanded sheet is determined by the height of its contents, or * if it is expanded in two stages (half the height of the parent container, full height of parent * container). Default value is true. * * @param fitToContents whether or not to fit the expanded sheet to its contents. */ public void setFitToContents(boolean fitToContents) { if (this.fitToContents == fitToContents) { return; } this.fitToContents = fitToContents; // If sheet is already laid out, recalculate the collapsed offset based on new setting. // Otherwise, let onLayoutChild handle this later. if (viewRef != null) { calculateCollapsedOffset(); } // Fix incorrect expanded settings depending on whether or not we are fitting sheet to contents. setStateInternal((this.fitToContents && state == STATE_HALF_EXPANDED) ? STATE_EXPANDED : state); updateAccessibilityActions(); } /** * Sets the height of the bottom sheet when it is collapsed. * * @param peekHeight The height of the collapsed bottom sheet in pixels, or {@link * #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline. * @attr ref * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight */ public void setPeekHeight(int peekHeight) { setPeekHeight(peekHeight, false); } /** * Sets the height of the bottom sheet when it is collapsed while optionally animating between the * old height and the new height. * * @param peekHeight The height of the collapsed bottom sheet in pixels, or {@link * #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically at 16:9 ratio keyline. * @param animate Whether to animate between the old height and the new height. * @attr ref * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight */ public final void setPeekHeight(int peekHeight, boolean animate) { boolean layout = false; if (peekHeight == PEEK_HEIGHT_AUTO) { if (!peekHeightAuto) { peekHeightAuto = true; layout = true; } } else if (peekHeightAuto || this.peekHeight != peekHeight) { peekHeightAuto = false; this.peekHeight = Math.max(0, peekHeight); layout = true; } // If sheet is already laid out, recalculate the collapsed offset based on new setting. // Otherwise, let onLayoutChild handle this later. if (layout && viewRef != null) { calculateCollapsedOffset(); if (state == STATE_COLLAPSED) { V view = viewRef.get(); if (view != null) { if (animate) { settleToStatePendingLayout(state); } else { view.requestLayout(); } } } } } /** * Gets the height of the bottom sheet when it is collapsed. * * @return The height of the collapsed bottom sheet in pixels, or {@link #PEEK_HEIGHT_AUTO} if the * sheet is configured to peek automatically at 16:9 ratio keyline * @attr ref * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight */ public int getPeekHeight() { return peekHeightAuto ? PEEK_HEIGHT_AUTO : peekHeight; } /** * Determines the height of the BottomSheet in the {@link #STATE_HALF_EXPANDED} state. The * material guidelines recommended a value of 0.5, which results in the sheet filling half of the * parent. The height of the BottomSheet will be smaller as this ratio is decreased and taller as * it is increased. The default value is 0.5. * * @param ratio a float between 0 and 1, representing the {@link #STATE_HALF_EXPANDED} ratio. * @attr ref * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_halfExpandedRatio */ public void setHalfExpandedRatio(@FloatRange(from = 0.0f, to = 1.0f) float ratio) { if ((ratio <= 0) || (ratio >= 1)) { throw new IllegalArgumentException("ratio must be a float value between 0 and 1"); } this.halfExpandedRatio = ratio; // If sheet is already laid out, recalculate the half expanded offset based on new setting. // Otherwise, let onLayoutChild handle this later. if (viewRef != null) { calculateHalfExpandedOffset(); } } /** * Gets the ratio for the height of the BottomSheet in the {@link #STATE_HALF_EXPANDED} state. * * @attr ref * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_halfExpandedRatio */ @FloatRange(from = 0.0f, to = 1.0f) public float getHalfExpandedRatio() { return halfExpandedRatio; } /** * Determines the top offset of the BottomSheet in the {@link #STATE_EXPANDED} state when * fitsToContent is false. The default value is 0, which results in the sheet matching the * parent's top. * * @param offset an integer value greater than equal to 0, representing the {@link * #STATE_EXPANDED} offset. Value must not exceed the offset in the half expanded state. * @attr ref * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_expandedOffset */ public void setExpandedOffset(int offset) { if (offset < 0) { throw new IllegalArgumentException("offset must be greater than or equal to 0"); } this.expandedOffset = offset; } /** * Returns the current expanded offset. If {@code fitToContents} is true, it will automatically * pick the offset depending on the height of the content. * * @attr ref * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_expandedOffset */ public int getExpandedOffset() { return fitToContents ? fitToContentsOffset : expandedOffset; } /** * Sets whether this bottom sheet can hide when it is swiped down. * * @param hideable {@code true} to make this bottom sheet hideable. * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_hideable */ public void setHideable(boolean hideable) { if (this.hideable != hideable) { this.hideable = hideable; if (!hideable && state == STATE_HIDDEN) { // Lift up to collapsed state setState(STATE_COLLAPSED); } updateAccessibilityActions(); } } /** * Gets whether this bottom sheet can hide when it is swiped down. * * @return {@code true} if this bottom sheet can hide. * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_hideable */ public boolean isHideable() { return hideable; } /** * Sets whether this bottom sheet should skip the collapsed state when it is being hidden after it * is expanded once. Setting this to true has no effect unless the sheet is hideable. * * @param skipCollapsed True if the bottom sheet should skip the collapsed state. * @attr ref * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed */ public void setSkipCollapsed(boolean skipCollapsed) { this.skipCollapsed = skipCollapsed; } /** * Sets whether this bottom sheet should skip the collapsed state when it is being hidden after it * is expanded once. * * @return Whether the bottom sheet should skip the collapsed state. * @attr ref * com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed */ public boolean getSkipCollapsed() { return skipCollapsed; } /** * Sets whether this bottom sheet is can be collapsed/expanded by dragging. Note: When disabling * dragging, an app will require to implement a custom way to expand/collapse the bottom sheet * * @param draggable {@code false} to prevent dragging the sheet to collapse and expand * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_draggable */ public void setDraggable(boolean draggable) { this.draggable = draggable; } public boolean isDraggable() { return draggable; } /** * Sets save flags to be preserved in bottomsheet on configuration change. * * @param flags bitwise int of {@link #SAVE_PEEK_HEIGHT}, {@link #SAVE_FIT_TO_CONTENTS}, {@link * #SAVE_HIDEABLE}, {@link #SAVE_SKIP_COLLAPSED}, {@link #SAVE_ALL} and {@link #SAVE_NONE}. * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags * @see #getSaveFlags() */ public void setSaveFlags(@SaveFlags int flags) { this.saveFlags = flags; } /** * Returns the save flags. * * @attr ref com.google.android.material.R.styleable#BottomSheetBehavior_Layout_behavior_saveFlags * @see #setSaveFlags(int) */ @SaveFlags public int getSaveFlags() { return this.saveFlags; } /** * Sets a callback to be notified of bottom sheet events. * * @param callback The callback to notify when bottom sheet events occur. * @deprecated use {@link #addBottomSheetCallback(BottomSheetCallback)} and {@link * #removeBottomSheetCallback(BottomSheetCallback)} instead */ @Deprecated public void setBottomSheetCallback(BottomSheetCallback callback) { Log.w( TAG, "BottomSheetBehavior now supports multiple callbacks. `setBottomSheetCallback()` removes" + " all existing callbacks, including ones set internally by library authors, which" + " may result in unintended behavior. This may change in the future. Please use" + " `addBottomSheetCallback()` and `removeBottomSheetCallback()` instead to set your" + " own callbacks."); callbacks.clear(); if (callback != null) { callbacks.add(callback); } } /** * Adds a callback to be notified of bottom sheet events. * * @param callback The callback to notify when bottom sheet events occur. */ public void addBottomSheetCallback(@NonNull BottomSheetCallback callback) { if (!callbacks.contains(callback)) { callbacks.add(callback); } } /** * Removes a previously added callback. * * @param callback The callback to remove. */ public void removeBottomSheetCallback(@NonNull BottomSheetCallback callback) { callbacks.remove(callback); } /** * Sets the state of the bottom sheet. The bottom sheet will transition to that state with * animation. * * @param state One of {@link #STATE_COLLAPSED}, {@link #STATE_EXPANDED}, {@link #STATE_HIDDEN}, * or {@link #STATE_HALF_EXPANDED}. */ public void setState(@State int state) { if (state == this.state) { return; } if (viewRef == null) { // The view is not laid out yet; modify mState and let onLayoutChild handle it later if (state == STATE_COLLAPSED || state == STATE_EXPANDED || state == STATE_HALF_EXPANDED || (hideable && state == STATE_HIDDEN)) { this.state = state; } return; } settleToStatePendingLayout(state); } /** * Sets whether this bottom sheet should adjust it's position based on the system gesture area on * Android Q and above. * *

Note: the bottom sheet will only adjust it's position if it would be unable to be scrolled * upwards because the peekHeight is less than the gesture inset margins,(because that would cause * a gesture conflict), gesture navigation is enabled, and this {@code ignoreGestureInsetBottom} * flag is false. */ public void setGestureInsetBottomIgnored(boolean gestureInsetBottomIgnored) { this.gestureInsetBottomIgnored = gestureInsetBottomIgnored; } /** * Returns whether this bottom sheet should adjust it's position based on the system gesture area. */ public boolean isGestureInsetBottomIgnored() { return gestureInsetBottomIgnored; } private void settleToStatePendingLayout(@State int state) { final V child = viewRef.get(); if (child == null) { return; } // Start the animation; wait until a pending layout if there is one. ViewParent parent = child.getParent(); if (parent != null && parent.isLayoutRequested() && ViewCompat.isAttachedToWindow(child)) { final int finalState = state; child.post( new Runnable() { @Override public void run() { settleToState(child, finalState); } }); } else { settleToState(child, state); } } /** * Gets the current state of the bottom sheet. * * @return One of {@link #STATE_EXPANDED}, {@link #STATE_HALF_EXPANDED}, {@link #STATE_COLLAPSED}, * {@link #STATE_DRAGGING}, {@link #STATE_SETTLING}, or {@link #STATE_HALF_EXPANDED}. */ @State public int getState() { return state; } void setStateInternal(@State int state) { if (this.state == state) { return; } this.state = state; if (viewRef == null) { return; } View bottomSheet = viewRef.get(); if (bottomSheet == null) { return; } if (state == STATE_EXPANDED) { updateImportantForAccessibility(true); } else if (state == STATE_HALF_EXPANDED || state == STATE_HIDDEN || state == STATE_COLLAPSED) { updateImportantForAccessibility(false); } updateDrawableForTargetState(state); for (int i = 0; i < callbacks.size(); i++) { callbacks.get(i).onStateChanged(bottomSheet, state); } updateAccessibilityActions(); } private void updateDrawableForTargetState(@State int state) { if (state == STATE_SETTLING) { // Special case: we want to know which state we're settling to, so wait for another call. return; } boolean expand = state == STATE_EXPANDED; if (isShapeExpanded != expand) { isShapeExpanded = expand; if (materialShapeDrawable != null && interpolatorAnimator != null) { if (interpolatorAnimator.isRunning()) { interpolatorAnimator.reverse(); } else { float to = expand ? 0f : 1f; float from = 1f - to; interpolatorAnimator.setFloatValues(from, to); interpolatorAnimator.start(); } } } } private int calculatePeekHeight() { if (peekHeightAuto) { return Math.max(peekHeightMin, parentHeight - parentWidth * 9 / 16); } return peekHeight; } private void calculateCollapsedOffset() { int peek = calculatePeekHeight(); if (fitToContents) { collapsedOffset = Math.max(parentHeight - peek, fitToContentsOffset); } else { collapsedOffset = parentHeight - peek; } } private void calculateHalfExpandedOffset() { this.halfExpandedOffset = (int) (parentHeight * (1 - halfExpandedRatio)); } private void reset() { activePointerId = ViewDragHelper.INVALID_POINTER; if (velocityTracker != null) { velocityTracker.recycle(); velocityTracker = null; } } private void restoreOptionalState(@NonNull SavedState ss) { if (this.saveFlags == SAVE_NONE) { return; } if (this.saveFlags == SAVE_ALL || (this.saveFlags & SAVE_PEEK_HEIGHT) == SAVE_PEEK_HEIGHT) { this.peekHeight = ss.peekHeight; } if (this.saveFlags == SAVE_ALL || (this.saveFlags & SAVE_FIT_TO_CONTENTS) == SAVE_FIT_TO_CONTENTS) { this.fitToContents = ss.fitToContents; } if (this.saveFlags == SAVE_ALL || (this.saveFlags & SAVE_HIDEABLE) == SAVE_HIDEABLE) { this.hideable = ss.hideable; } if (this.saveFlags == SAVE_ALL || (this.saveFlags & SAVE_SKIP_COLLAPSED) == SAVE_SKIP_COLLAPSED) { this.skipCollapsed = ss.skipCollapsed; } } boolean shouldHide(@NonNull View child, float yvel) { if (skipCollapsed) { return true; } if (child.getTop() < collapsedOffset) { // It should not hide, but collapse. return false; } int peek = calculatePeekHeight(); final float newTop = child.getTop() + yvel * HIDE_FRICTION; return Math.abs(newTop - collapsedOffset) / (float) peek > HIDE_THRESHOLD; } //针对viewpager联动 boolean isFirstFind = true; //第一次寻找联动View,为viewPager添加滑动监听 private HashMap isScrollViewOnTopMap = new HashMap<>(); //关联页数与能否滑动flag的Map private int currentPagePosition = 0; //当前ViewPager的页数 public void setCurrentScrollViewOnTop(boolean scrollViewOnTop) { isScrollViewOnTopMap.put(currentPagePosition, scrollViewOnTop); Log.d("~~~~~~~~~",isScrollViewOnTopMap.toString()); notifyScrollView(); } private View globalView; private void notifyScrollView() { nestedScrollingChildRef = new WeakReference<>(findScrollingChild(globalView)); } @Nullable @VisibleForTesting View findScrollingChild(View view) { if(view==null){ return null; } globalView = view; boolean b = isScrollViewOnTopMap.getOrDefault(currentPagePosition,false ); if (b) { return null; } if (ViewCompat.isNestedScrollingEnabled(view)) { return view; } if (view instanceof ViewPager) { ViewPager viewPager = (ViewPager) view; if (isFirstFind) { for (int i = 0; i < Objects.requireNonNull(viewPager.getAdapter()).getCount(); i++) { isScrollViewOnTopMap.put(i,true); } viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { currentPagePosition = position; } @Override public void onPageSelected(int position) { } @Override public void onPageScrollStateChanged(int state) { } }); isFirstFind = false; } View currentViewPagerChild = ViewPagerUtils.getCurrentView(viewPager); View scrollingChild = findScrollingChild(currentViewPagerChild); if (scrollingChild != null) { return scrollingChild; } return currentViewPagerChild; } if (view instanceof ViewGroup) { ViewGroup group = (ViewGroup) view; for (int i = 0, count = group.getChildCount(); i < count; i++) { View scrollingChild = findScrollingChild(group.getChildAt(i)); if (scrollingChild != null) { return scrollingChild; } } } return null; } private void createMaterialShapeDrawable( @NonNull Context context, AttributeSet attrs, boolean hasBackgroundTint) { this.createMaterialShapeDrawable(context, attrs, hasBackgroundTint, null); } private void createMaterialShapeDrawable( @NonNull Context context, AttributeSet attrs, boolean hasBackgroundTint, @Nullable ColorStateList bottomSheetColor) { if (this.shapeThemingEnabled) { this.shapeAppearanceModelDefault = ShapeAppearanceModel.builder(context, attrs, R.attr.bottomSheetStyle, DEF_STYLE_RES) .build(); this.materialShapeDrawable = new MaterialShapeDrawable(shapeAppearanceModelDefault); this.materialShapeDrawable.initializeElevationOverlay(context); if (hasBackgroundTint && bottomSheetColor != null) { materialShapeDrawable.setFillColor(bottomSheetColor); } else { // If the tint isn't set, use the theme default background color. TypedValue defaultColor = new TypedValue(); context.getTheme().resolveAttribute(android.R.attr.colorBackground, defaultColor, true); materialShapeDrawable.setTint(defaultColor.data); } } } private void createShapeValueAnimator() { interpolatorAnimator = ValueAnimator.ofFloat(0f, 1f); interpolatorAnimator.setDuration(CORNER_ANIMATION_DURATION); interpolatorAnimator.addUpdateListener( new AnimatorUpdateListener() { @Override public void onAnimationUpdate(@NonNull ValueAnimator animation) { float value = (float) animation.getAnimatedValue(); if (materialShapeDrawable != null) { materialShapeDrawable.setInterpolation(value); } } }); } private void setSystemGestureInsets(@NonNull CoordinatorLayout parent) { if (VERSION.SDK_INT >= VERSION_CODES.Q && !isGestureInsetBottomIgnored()) { WindowInsets windowInsets = parent.getRootWindowInsets(); if (windowInsets != null) { int systemMandatoryInsetsBottom = windowInsets.getSystemGestureInsets().bottom; peekHeight += systemMandatoryInsetsBottom; } } } private float getYVelocity() { if (velocityTracker == null) { return 0; } velocityTracker.computeCurrentVelocity(1000, maximumVelocity); return velocityTracker.getYVelocity(activePointerId); } void settleToState(@NonNull View child, int state) { int top; if (state == STATE_COLLAPSED) { top = collapsedOffset; } else if (state == STATE_HALF_EXPANDED) { top = halfExpandedOffset; if (fitToContents && top <= fitToContentsOffset) { // Skip to the expanded state if we would scroll past the height of the contents. state = STATE_EXPANDED; top = fitToContentsOffset; } } else if (state == STATE_EXPANDED) { top = getExpandedOffset(); } else if (hideable && state == STATE_HIDDEN) { top = parentHeight; } else { throw new IllegalArgumentException("Illegal state argument: " + state); } startSettlingAnimation(child, state, top, false); } void startSettlingAnimation(View child, int state, int top, boolean settleFromViewDragHelper) { boolean startedSettling = settleFromViewDragHelper ? viewDragHelper.settleCapturedViewAt(child.getLeft(), top) : viewDragHelper.smoothSlideViewTo(child, child.getLeft(), top); if (startedSettling) { setStateInternal(STATE_SETTLING); // STATE_SETTLING won't animate the material shape, so do that here with the target state. updateDrawableForTargetState(state); if (settleRunnable == null) { // If the singleton SettleRunnable instance has not been instantiated, create it. settleRunnable = new SettleRunnable(child, state); } // If the SettleRunnable has not been posted, post it with the correct state. if (settleRunnable.isPosted == false) { settleRunnable.targetState = state; ViewCompat.postOnAnimation(child, settleRunnable); settleRunnable.isPosted = true; } else { // Otherwise, if it has been posted, just update the target state. settleRunnable.targetState = state; } } else { setStateInternal(state); } } private final ViewDragHelper.Callback dragCallback = new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(@NonNull View child, int pointerId) { if (state == STATE_DRAGGING) { return false; } if (touchingScrollingChild) { return false; } if (state == STATE_EXPANDED && activePointerId == pointerId) { View scroll = nestedScrollingChildRef != null ? nestedScrollingChildRef.get() : null; if (scroll != null && scroll.canScrollVertically(-1)) { // Let the content scroll up return false; } } return viewRef != null && viewRef.get() == child; } @Override public void onViewPositionChanged( @NonNull View changedView, int left, int top, int dx, int dy) { dispatchOnSlide(top); } @Override public void onViewDragStateChanged(int state) { if (state == ViewDragHelper.STATE_DRAGGING && draggable) { setStateInternal(STATE_DRAGGING); } } private boolean releasedLow(@NonNull View child) { // Needs to be at least half way to the bottom. return child.getTop() > (parentHeight + getExpandedOffset()) / 2; } @Override public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) { int top; @State int targetState; if (yvel < 0) { // Moving up if (fitToContents) { top = fitToContentsOffset; targetState = STATE_EXPANDED; } else { int currentTop = releasedChild.getTop(); if (currentTop > halfExpandedOffset) { top = halfExpandedOffset; targetState = STATE_HALF_EXPANDED; } else { top = expandedOffset; targetState = STATE_EXPANDED; } } } else if (hideable && shouldHide(releasedChild, yvel)) { // Hide if the view was either released low or it was a significant vertical swipe // otherwise settle to closest expanded state. if ((Math.abs(xvel) < Math.abs(yvel) && yvel > SIGNIFICANT_VEL_THRESHOLD) || releasedLow(releasedChild)) { top = parentHeight; targetState = STATE_HIDDEN; } else if (fitToContents) { top = fitToContentsOffset; targetState = STATE_EXPANDED; } else if (Math.abs(releasedChild.getTop() - expandedOffset) < Math.abs(releasedChild.getTop() - halfExpandedOffset)) { top = expandedOffset; targetState = STATE_EXPANDED; } else { top = halfExpandedOffset; targetState = STATE_HALF_EXPANDED; } } else if (yvel == 0.f || Math.abs(xvel) > Math.abs(yvel)) { // If the Y velocity is 0 or the swipe was mostly horizontal indicated by the X velocity // being greater than the Y velocity, settle to the nearest correct height. int currentTop = releasedChild.getTop(); if (fitToContents) { if (Math.abs(currentTop - fitToContentsOffset) < Math.abs(currentTop - collapsedOffset)) { top = fitToContentsOffset; targetState = STATE_EXPANDED; } else { top = collapsedOffset; targetState = STATE_COLLAPSED; } } else { if (currentTop < halfExpandedOffset) { if (currentTop < Math.abs(currentTop - collapsedOffset)) { top = expandedOffset; targetState = STATE_EXPANDED; } else { top = halfExpandedOffset; targetState = STATE_HALF_EXPANDED; } } else { if (Math.abs(currentTop - halfExpandedOffset) < Math.abs(currentTop - collapsedOffset)) { top = halfExpandedOffset; targetState = STATE_HALF_EXPANDED; } else { top = collapsedOffset; targetState = STATE_COLLAPSED; } } } } else { // Moving Down if (fitToContents) { top = collapsedOffset; targetState = STATE_COLLAPSED; } else { // Settle to the nearest correct height. int currentTop = releasedChild.getTop(); if (Math.abs(currentTop - halfExpandedOffset) < Math.abs(currentTop - collapsedOffset)) { top = halfExpandedOffset; targetState = STATE_HALF_EXPANDED; } else { top = collapsedOffset; targetState = STATE_COLLAPSED; } } } startSettlingAnimation(releasedChild, targetState, top, true); } @Override public int clampViewPositionVertical(@NonNull View child, int top, int dy) { return MathUtils.clamp( top, getExpandedOffset(), hideable ? parentHeight : collapsedOffset); } @Override public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) { return child.getLeft(); } @Override public int getViewVerticalDragRange(@NonNull View child) { if (hideable) { return parentHeight; } else { return collapsedOffset; } } }; void dispatchOnSlide(int top) { View bottomSheet = viewRef.get(); if (bottomSheet != null && !callbacks.isEmpty()) { float slideOffset = (top > collapsedOffset || collapsedOffset == getExpandedOffset()) ? (float) (collapsedOffset - top) / (parentHeight - collapsedOffset) : (float) (collapsedOffset - top) / (collapsedOffset - getExpandedOffset()); for (int i = 0; i < callbacks.size(); i++) { callbacks.get(i).onSlide(bottomSheet, slideOffset); } } } @VisibleForTesting int getPeekHeightMin() { return peekHeightMin; } /** * Disables the shaped corner {@link ShapeAppearanceModel} interpolation transition animations. * Will have no effect unless the sheet utilizes a {@link MaterialShapeDrawable} with set shape * theming properties. Only For use in UI testing. * * @hide */ @RestrictTo(LIBRARY_GROUP) @VisibleForTesting public void disableShapeAnimations() { // Sets the shape value animator to null, prevents animations from occuring during testing. interpolatorAnimator = null; } private class SettleRunnable implements Runnable { private final View view; private boolean isPosted; @State int targetState; SettleRunnable(View view, @State int targetState) { this.view = view; this.targetState = targetState; } @Override public void run() { if (viewDragHelper != null && viewDragHelper.continueSettling(true)) { ViewCompat.postOnAnimation(view, this); } else { setStateInternal(targetState); } this.isPosted = false; } } /** * State persisted across instances */ protected static class SavedState extends AbsSavedState { @State final int state; int peekHeight; boolean fitToContents; boolean hideable; boolean skipCollapsed; public SavedState(@NonNull Parcel source) { this(source, null); } public SavedState(@NonNull Parcel source, ClassLoader loader) { super(source, loader); //noinspection ResourceType state = source.readInt(); peekHeight = source.readInt(); fitToContents = source.readInt() == 1; hideable = source.readInt() == 1; skipCollapsed = source.readInt() == 1; } public SavedState(Parcelable superState, @NonNull MyViewPagerBottomSheetBehavior behavior) { super(superState); this.state = behavior.getState(); this.peekHeight = behavior.getPeekHeight(); this.fitToContents = behavior.isFitToContents(); this.hideable = behavior.isHideable(); this.skipCollapsed = behavior.getSkipCollapsed(); } @Deprecated public SavedState(Parcelable superstate, int state) { super(superstate); this.state = state; } @Override public void writeToParcel(@NonNull Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(state); out.writeInt(peekHeight); out.writeInt(fitToContents ? 1 : 0); out.writeInt(hideable ? 1 : 0); out.writeInt(skipCollapsed ? 1 : 0); } public static final Creator CREATOR = new ClassLoaderCreator() { @NonNull @Override public SavedState createFromParcel(@NonNull Parcel in, ClassLoader loader) { return new SavedState(in, loader); } @Nullable @Override public SavedState createFromParcel(@NonNull Parcel in) { return new SavedState(in, null); } @NonNull @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } /** * A utility function to get the {@link BottomSheetBehavior} associated with the {@code view}. * * @param view The {@link View} with {@link BottomSheetBehavior}. * @return The {@link BottomSheetBehavior} associated with the {@code view}. */ @NonNull @SuppressWarnings("unchecked") public static MyViewPagerBottomSheetBehavior from(@NonNull 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 MyViewPagerBottomSheetBehavior)) { throw new IllegalArgumentException("The view is not associated with BottomSheetBehavior"); } return (MyViewPagerBottomSheetBehavior) behavior; } /** * Sets whether the BottomSheet should update the accessibility status of its {@link * CoordinatorLayout} siblings when expanded. * *

Set this to true if the expanded state of the sheet blocks access to siblings (e.g., when * the sheet expands over the full screen). */ public void setUpdateImportantForAccessibilityOnSiblings( boolean updateImportantForAccessibilityOnSiblings) { this.updateImportantForAccessibilityOnSiblings = updateImportantForAccessibilityOnSiblings; } private void updateImportantForAccessibility(boolean expanded) { if (viewRef == null) { return; } ViewParent viewParent = viewRef.get().getParent(); if (!(viewParent instanceof CoordinatorLayout)) { return; } CoordinatorLayout parent = (CoordinatorLayout) viewParent; final int childCount = parent.getChildCount(); if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) && expanded) { if (importantForAccessibilityMap == null) { importantForAccessibilityMap = new HashMap<>(childCount); } else { // The important for accessibility values of the child views have been saved already. return; } } for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); if (child == viewRef.get()) { continue; } if (expanded) { // Saves the important for accessibility value of the child view. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { importantForAccessibilityMap.put(child, child.getImportantForAccessibility()); } if (updateImportantForAccessibilityOnSiblings) { ViewCompat.setImportantForAccessibility( child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); } } else { if (updateImportantForAccessibilityOnSiblings && importantForAccessibilityMap != null && importantForAccessibilityMap.containsKey(child)) { // Restores the original important for accessibility value of the child view. ViewCompat.setImportantForAccessibility(child, importantForAccessibilityMap.get(child)); } } } if (!expanded) { importantForAccessibilityMap = null; } } private void updateAccessibilityActions() { if (viewRef == null) { return; } V child = viewRef.get(); if (child == null) { return; } ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_COLLAPSE); ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_EXPAND); ViewCompat.removeAccessibilityAction(child, AccessibilityNodeInfoCompat.ACTION_DISMISS); if (hideable && state != STATE_HIDDEN) { addAccessibilityActionForState(child, AccessibilityActionCompat.ACTION_DISMISS, STATE_HIDDEN); } switch (state) { case STATE_EXPANDED: { int nextState = fitToContents ? STATE_COLLAPSED : STATE_HALF_EXPANDED; addAccessibilityActionForState( child, AccessibilityActionCompat.ACTION_COLLAPSE, nextState); break; } case STATE_HALF_EXPANDED: { addAccessibilityActionForState( child, AccessibilityActionCompat.ACTION_COLLAPSE, STATE_COLLAPSED); addAccessibilityActionForState( child, AccessibilityActionCompat.ACTION_EXPAND, STATE_EXPANDED); break; } case STATE_COLLAPSED: { int nextState = fitToContents ? STATE_EXPANDED : STATE_HALF_EXPANDED; addAccessibilityActionForState(child, AccessibilityActionCompat.ACTION_EXPAND, nextState); break; } default: // fall out } } private void addAccessibilityActionForState( V child, AccessibilityActionCompat action, final int state) { ViewCompat.replaceAccessibilityAction( child, action, null, new AccessibilityViewCommand() { @Override public boolean perform(@NonNull View view, @Nullable CommandArguments arguments) { setState(state); return true; } }); } }

你可能感兴趣的:(BottomSheetBehavior+ViewPager+多RecyclerView 滑动冲突(滑动弹出隐藏)终极解决方案)