自定义AppBarLayout.Behavior实现惯性(fling)传递

FlingBehavior

AppBarLayout 惯性传递是指在滚动 AppBarLayout 时,AppBarLayout 会将惯性传递给其子 View,从而实现子 View 的滚动效果。具体来说,当用户在滚动 AppBarLayout 时,AppBarLayout 会根据滚动方向和速度计算出一个惯性滚动值,并将该值传递给其子 View。子 View 可以根据该惯性滚动值进行相应的滚动操作,从而实现与 AppBarLayout 的联动效果。

例如,在一个包含 RecyclerView 和 CollapsingToolbarLayout 的布局中,当用户向下滚动 RecyclerView 时,AppBarLayout 会根据滚动速度和方向计算出一个惯性滚动值,并将该值传递给 CollapsingToolbarLayout。CollapsingToolbarLayout 可以根据该惯性滚动值进行相应的折叠操作,从而实现与 RecyclerView 的联动效果。

总的来说,AppBarLayout 惯性传递是实现 AppBarLayout 与其子 View 真正联动的关键。通过惯性传递,AppBarLayout 可以将自身的滚动效果传递给子 View,从而实现整体联动的效果。

实现说明:
在 onInterceptTouchEvent 方法中,用于拦截触摸事件,判断是否需要进行拖拽,它会判断当前触摸事件是否在 AppBarLayout 区域内,并记录下当前触摸点的坐标和 id。然后根据触摸点的移动距离和 touchSlop 值判断是否开始拖拽,并记录下 isBeingDragged 标志位。如果当前触摸事件需要分发给下层 View,则将事件传递给与 AppBarLayout 关联的可滚动 View,并返回 true。

在 onTouchEvent 方法中,用于处理拖拽事件,计算滑动距离并将事件分发给下层 View。它会根据 isBeingDragged 标志位判断是否需要将触摸事件传递给可滚动 View。如果需要,则将事件进行偏移,以保证传递给可滚动 View 后,事件的坐标正确。然后将事件传递给可滚动 View,并返回 true。

在 onLayoutChild 方法中,它会查找与 AppBarLayout 关联的可滚动 View,并记录下来。这样,在后续的处理中,它就可以将触摸事件传递给该 View 了。
拿走即用,如果帮你解决了问题,记得点个赞呦!
在代码中有详细说明:

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;

import com.google.android.material.appbar.AppBarLayout;

/**
 * 自定义的 Behavior 类,用于实现 AppBarLayout 的 fling 效果。
 * 在滑动 AppBarLayout 时,它会将滑动事件传递给与 AppBarLayout 关联的可滚动 View,以实现 fling 效果
 *
 * @author HuKui
 * @date 2022/11/24 下午 03:02
 */
public class FlingBehavior extends AppBarLayout.Behavior {

    /**
     * 表示无效的触摸点
     **/
    private static final int INVALID_POINTER = -1;
    /**
     * 是否正在被拖拽
     **/
    private boolean isBeingDragged;
    /**
     * 当前活动的触摸点的 id
     **/
    private int activePointerId = INVALID_POINTER;
    /**
     * 上一次触摸事件的Y坐标
     **/
    private int lastMotionY;
    /**
     * 上一次触摸事件的X坐标
     **/
    private int lastMotionX;
    /**
     * 触摸的最小滑动距离
     **/
    private final int touchSlop;
    /**
     * 当前触摸事件
     **/
    private MotionEvent currentMotionEvent;
    /**
     * 是否需要将事件分发给下层 View
     **/
    private boolean needDispatchDown;
    /**
     * 与 AppBarLayout 关联的可滚动 View
     **/
    private View scrollViewBehaviorView;


    public FlingBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    /**
     * 这段代码的作用是判断用户是否正在拖拽 AppBarLayout。当用户按下触摸点时,记录下触摸点的坐标和触摸点的 ID;
     * 当用户移动触摸点时,计算触摸点移动的距离,并判断是否大于 touchSlop,
     * 如果是则认为正在拖拽;当用户取消或抬起触摸点时,清除拖拽状态并将事件设置为 ACTION_CANCEL
     * 。如果有关联的子 View(如 NestedScrollView),则将事件发送给子 View 处理。最后返回是否正在拖拽的状态。
     **/
    @Override
    public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull AppBarLayout child, @NonNull MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                isBeingDragged = false;
                needDispatchDown = true;
                // 获取触摸点的坐标
                int x = (int) ev.getX();
                int y = (int) ev.getY();
                // 检查触摸点是否在 AppBarLayout 中
                if (parent.isPointInChildBounds(child, x, y)) {
                    // 如果是,则记录下坐标和触摸点的 ID
                    lastMotionX = x;
                    lastMotionY = y;
                    activePointerId = ev.getPointerId(0);
                    // 回收之前的 MotionEvent
                    if (currentMotionEvent != null) {
                        currentMotionEvent.recycle();
                    }
                    // 复制当前的 MotionEvent
                    currentMotionEvent = MotionEvent.obtain(ev);
                }
                break;

            case MotionEvent.ACTION_MOVE:
                // 检查当前是否正在拖拽
                final int activePointerId = this.activePointerId;
                if (activePointerId == INVALID_POINTER) {
                    break;
                }
                // 获取触摸点的坐标
                final int pointerIndex = ev.findPointerIndex(activePointerId);
                if (pointerIndex == -1) {
                    break;
                }
                final int moveX = (int) ev.getX(pointerIndex);
                final int moveY = (int) ev.getY(pointerIndex);
                // 计算触摸点移动的距离
                final int xDiff = Math.abs(moveX - lastMotionX);
                final int yDiff = Math.abs(moveY - lastMotionY);
                // 如果垂直移动的距离大于水平移动的距离,并且大于 touchSlop,则认为正在拖拽
                if (yDiff > touchSlop && yDiff > xDiff) {
                    isBeingDragged = true;
                }
                break;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                // 取消拖拽状态
                isBeingDragged = false;
                needDispatchDown = true;
                this.activePointerId = INVALID_POINTER;
                // 清除 mCurrentMotionEvent
                currentMotionEvent = null;
                // 将事件设置为 ACTION_CANCEL
                ev.setAction(MotionEvent.ACTION_CANCEL);
                break;
            default:
                break;
        }
        return isBeingDragged;
    }


    @Override
    public boolean onTouchEvent(@NonNull CoordinatorLayout parent, @NonNull AppBarLayout child, @NonNull MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                // 是否在拖拽
                if (isBeingDragged) {
                    final int offset = child.getHeight() - child.getBottom();
                    if (needDispatchDown) {
                        needDispatchDown = false;
                        MotionEvent downEvent = MotionEvent.obtain(currentMotionEvent);
                        downEvent.offsetLocation(0, offset);
                        if (scrollViewBehaviorView != null) {
                            scrollViewBehaviorView.dispatchTouchEvent(downEvent);
                        }
                    }
                    MotionEvent moveEvent = MotionEvent.obtain(ev);
                    moveEvent.offsetLocation(0, offset);
                    if (scrollViewBehaviorView != null) {
                        scrollViewBehaviorView.dispatchTouchEvent(moveEvent);
                        return true;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if (isBeingDragged) {
                    ev.offsetLocation(0, child.getHeight() - child.getBottom());
                    if (scrollViewBehaviorView != null) {
                        scrollViewBehaviorView.dispatchTouchEvent(ev);
                        return true;
                    }
                }
                break;
            default:
                break;
        }
        // 如果事件没有被正确处理,则返回false,将事件交给父类处理
        return super.onTouchEvent(parent, child, ev);
    }

    @SuppressWarnings("rawtypes")
    @Override
    public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull AppBarLayout abl, int layoutDirection) {
        boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
        // 查找与 AppBarLayout 关联的可滚动 View。
        for (int i = 0, c = parent.getChildCount(); i < c; i++) {
            View sibling = parent.getChildAt(i);
            final CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) sibling.getLayoutParams()).getBehavior();
            if (behavior instanceof AppBarLayout.ScrollingViewBehavior) {
                scrollViewBehaviorView = sibling;

            }
        }
        return handled;
    }
}

使用方式:

    <com.google.android.material.appbar.AppBarLayout
                    style="@style/AppBarLayout"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    // FlingBehavior的文件位置
               app:layout_behavior="com.xxx.xxx.FlingBehavior">
</com.google.android.material.appbar.AppBarLayout>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值