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>