"城市交通调度"之CoordinatorLayout浅析

0 阅读4分钟

用一个 "城市交通调度" 的故事,结合代码来解释 CoordinatorLayout 的工作原理。这个解释适合零基础小白,保证有趣又好懂!

🌆 故事背景:城市交通调度中心

想象一下,你是一座超级大都市的 "交通调度中心主任"。这座城市有:

  • 主干道:允许各种车辆高速行驶(对应 CoordinatorLayout 中的滑动控件,如 RecyclerView)

  • 特殊车辆:比如救护车、消防车,它们需要特殊通行权(对应 FloatingActionButton、AppBarLayout 等需要响应滑动的组件)

  • 交通规则:当主干道上的车辆流动时,特殊车辆需要根据情况调整自己的位置(比如避让、跟随或保持距离)

这就是 CoordinatorLayout 的核心场景:当一个 View 滑动时,其他 View 需要做出相应的动作

🚦 CoordinatorLayout 的 "交通调度规则"

CoordinatorLayout 就像这个城市的 "智能交通系统",它通过以下三个步骤解决滑动冲突:

  1. 安装传感器:在主干道上安装传感器,实时监测车辆流动(监听滑动事件)
  2. 分配调度员:为每辆特殊车辆分配一个调度员,告诉他如何响应主干道的变化(Behavior)
  3. 执行调度方案:当主干道有情况时,调度员指挥特殊车辆做出相应调整(处理滑动冲突)

👨💻 代码演示:CoordinatorLayout 的 "智能调度系统"

下面是 CoordinatorLayout 的核心代码简化版,展示了它如何处理滑动冲突:

java

public class CoordinatorLayout extends ViewGroup {
    // 存储所有特殊车辆的调度员
    private SparseArray<Behavior> mBehaviors = new SparseArray<>();

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        // 当主干道(View)开始滑动时,通知所有调度员
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            Behavior behavior = getBehavior(view);
            if (behavior != null) {
                // 询问调度员:你是否关心这个滑动事件?
                if (behavior.onStartNestedScroll(this, view, child, target, nestedScrollAxes)) {
                    // 如果关心,标记这个view需要响应滑动
                    view.setTag(R.id.tag_needs_offset, true);
                }
            }
        }
        return nestedScrollAxes != 0;
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        // 主干道滑动前,让调度员先处理(比如让特殊车辆先避让)
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            if (view.getTag(R.id.tag_needs_offset) == Boolean.TRUE) {
                Behavior behavior = getBehavior(view);
                if (behavior != null) {
                    // 调度员指挥特殊车辆移动
                    behavior.onNestedPreScroll(this, view, target, dx, dy, consumed);
                }
            }
        }
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
                               int dxUnconsumed, int dyUnconsumed) {
        // 主干道滑动时,让调度员继续处理
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            if (view.getTag(R.id.tag_needs_offset) == Boolean.TRUE) {
                Behavior behavior = getBehavior(view);
                if (behavior != null) {
                    // 继续指挥特殊车辆移动
                    behavior.onNestedScroll(this, view, target, 
                                           dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
                }
            }
        }
    }

    // 获取view对应的调度员(Behavior)
    private Behavior getBehavior(View view) {
        LayoutParams lp = (LayoutParams) view.getLayoutParams();
        return lp.getBehavior();
    }
}

🚑 自定义调度员:让 FAB 按钮 "聪明" 起来

现在,让我们为城市中的 "救护车"(FloatingActionButton) 编写一个调度员 (Behavior),让它在主干道滑动时 "聪明" 地避让:

java

public class FabScrollBehavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
    public FabScrollBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout parent, 
                                      FloatingActionButton child,
                                      View directTargetChild, View target, 
                                      int nestedScrollAxes) {
        // 只关心垂直方向的滑动
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
    }

    @Override
    public void onNestedScroll(CoordinatorLayout parent, FloatingActionButton child,
                              View target, int dxConsumed, int dyConsumed,
                              int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(parent, child, target, dxConsumed, dyConsumed,
                            dxUnconsumed, dyUnconsumed);
        
        if (dyConsumed > 0) {
            // 主干道向上滑动(用户向下滚动),让FAB隐藏
            CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) 
                                                         child.getLayoutParams();
            int size = child.getHeight() + layoutParams.bottomMargin;
            child.animate().translationY(size).setDuration(300).start();
        } else if (dyConsumed < 0) {
            // 主干道向下滑动(用户向上滚动),让FAB显示
            child.animate().translationY(0).setDuration(300).start();
        }
    }
}

📱 如何使用这个 "调度系统"

在布局文件中,你只需要这样写,就能让 FAB 按钮根据 RecyclerView 的滑动自动隐藏 / 显示:

xml

<androidx.coordinatorlayout.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:src="@drawable/ic_add"
        app:layout_behavior=".FabScrollBehavior"/> <!-- 指定调度员 -->
</androidx.coordinatorlayout.widget.CoordinatorLayout>

🧠 核心原理总结:三个关键点

  1. Behavior 机制:每个需要响应滑动的 View 都可以绑定一个 Behavior(调度员),它负责定义 View 如何响应滑动。
  2. 事件分发:CoordinatorLayout 通过onStartNestedScrollonNestedPreScrollonNestedScroll等方法,将滑动事件分发给所有绑定了 Behavior 的 View。
  3. 自定义逻辑:在 Behavior 中,你可以根据滑动的方向和距离,自定义 View 的响应方式,比如移动、缩放、显示 / 隐藏等。

🎉 现在你也是 "滑动冲突处理专家" 了!

通过这个城市交通调度的故事,你应该已经理解了:

  • CoordinatorLayout 就像交通调度中心

  • Behavior 就像每个特殊车辆的专属调度员

  • 滑动事件就像主干道上的车流

  • 特殊 View(如 FAB、AppBar)就像需要特殊通行权的车辆

当主干道上的车流发生变化时,调度中心会通知所有调度员,调度员再指挥各自的车辆做出相应调整,从而避免了交通混乱(滑动冲突)。

是不是很简单?下次遇到滑动冲突问题,记得你也是一位合格的 "交通调度员" 啦! 🚗🚑🚒