View的滑动之​​“房屋大挪移”​​ 的奇幻冒险

14 阅读5分钟

朋友!咱们不聊枯燥的代码,今天化身“Android小镇”的镇长,带你用一场​​“房屋大挪移”​​ 的奇幻冒险,揭开View滑动的神秘面纱!🎢


🗺️ ​​第一章:Android小镇的坐标系(地基)​

想象一下,Android小镇就是你的手机屏幕:

  • ​绝对坐标系:​​ 小镇广场的​​中心雕像​​是原点 (0,0)。向右是X轴正方向,向下是Y轴正方向。event.getRawX()/getRawY() 告诉你手指离雕像有多远 。

  • ​视图坐标系:​​ 每栋房子(View)也有自己的小院子。院子的​​左上角​​是这栋房子的原点 (0,0)。event.getX()/getY() 告诉你手指离房子院门有多远 。

​📍 关键坐标员(方法):​

  • getLeft()getTop():房子左墙/屋顶离​​父院子​​左边/顶边的距离。

  • getRight()getBottom():房子右墙/地基离​​父院子​​左边/顶边的距离 。


👮‍♂️ ​​第二章:手指侦探的触控事件(触发行动)​

当你的手指(侦探🕵️‍♂️)触摸小镇房子时,系统会派发“案件卷宗”(MotionEvent):

  • ACTION_DOWN:侦探​​按下​​门铃(记录初始坐标 lastX, lastY)。

  • ACTION_MOVE:侦探在门上​​滑动​​手指(计算偏移量 offsetX = x - lastX, offsetY = y - lastY)。

  • ACTION_UP:侦探​​松开​​手指(可能触发弹性动画) 。

java
Copy
@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX(); // 相对房子院子的坐标
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            int offsetX = x - lastX; // 计算侦探滑动的距离
            int offsetY = y - lastY;
            // 这里会调用房屋移动方法(下一章揭晓)
            moveHouse(offsetX, offsetY); 
            lastX = x; // 更新侦探位置
            lastY = y;
            break;
    }
    return true;
}

🏗️ ​​第三章:四大挪移神功(实现滑动)​

现在,房子怎么动?四大工匠各显神通:

​1️⃣ Layout工匠:直接拆墙重建​

“哪儿的位置?我现画图纸!”
直接修改房子的​​四面墙​​的位置:

java
Copy
void moveHouse(int offsetX, int offsetY) {
    layout(getLeft() + offsetX, 
           getTop() + offsetY,
           getRight() + offsetX,
           getBottom() + offsetY);
}

​效果:​​ 房子瞬间闪现到新位置!⚡️(适合慢速拖动)


​2️⃣ Margin工匠:调整院子边界​

“院子大点,房子不就动了嘛~”
修改房子在​​父院子​​的边距 :

java
Copy
void moveHouse(int offsetX, int offsetY) {
    ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
    params.leftMargin = getLeft() + offsetX;
    params.topMargin = getTop() + offsetY;
    setLayoutParams(params);
}

​注意:​​ 房子必须有个“爹”(父布局)!👨‍👦


​3️⃣ Scroll工匠:移动“世界画布”​

“不是房子动,是地球在转!”  🌍
其实是移动房子的​​内容​​(比如屋里的家具)。要动整个房子?得让它的​​爹地​​(父View)动画布!
​神奇现象:​​ 想让房子右移?得让画布​​左移​​!所以参数是​​负数​​👇:

java
Copy
void moveHouse(int offsetX, int offsetY) {
    ((View) getParent()).scrollBy(-offsetX, -offsetY); // 注意负号!
}

scrollTo(x, y):直接卷动画布到某个点。
scrollBy(dx, dy):相对当前位置卷动 。


​4️⃣ Scroller大师:橡皮筋弹性动画​

“别急!我让房子滑得有惯性!”  🚀
解决scrollTo瞬移的尴尬,实现“手指松开后继续滑”的效果。
​原理三件套:​

  1. ​橡皮筋规划师:​​ Scroller 计算每一帧的位置(但不真动房子)。

  2. ​刷帧小助手:​​ invalidate() 触发重绘。

  3. ​帧执行人:​​ computeScroll() 按计划移动 。

java
Copy
// 步骤1:创建橡皮筋
Scroller mScroller = new Scroller(context);

// 步骤2:手指松开时启动弹性动画(在ACTION_UP中)
mScroller.startScroll(parentView.getScrollX(), // 当前画布位置
                      parentView.getScrollY(),
                      -targetScrollX,         // 目标偏移量(负!)
                      -targetScrollY,
                      500);                   // 动画时间(ms)
invalidate(); // 喊一声:该重画啦!

// 步骤3:重绘时执行每一帧移动
@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) { // 橡皮筋还在弹?
        parentView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        invalidate(); // 继续刷下一帧!
    }
}

🚦 ​​第四章:小镇交通冲突(滑动事件冲突)​

当两栋房子(父View和子View)都想响应滑动,咋办?

​经典车祸现场:​

  • 垂直滑动:ScrollView(父) vs ListView(子)

  • 水平滑动:ViewPager(父) vs 横向ScrollView(子)

​交警解决方案:​

  1. ​边界裁决法:​​ 在父View的边缘区域滑动归父,内部滑动归子(类似抽屉导航边缘触发)。

  2. ​方向裁决法:​​ 判断滑动的方向——水平滑动父View处理,垂直滑动子View处理(或反之)。

java
Copy
// 父View中拦截事件示例:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    int dx = Math.abs(moveX - downX); // X方向滑动距离
    int dy = Math.abs(moveY - downY); // Y方向滑动距离
    // 如果横向滑动距离更大,父View拦截事件
    if (dx > dy && dx > touchSlop) { 
        return true; // 拦截!交给我处理!
    }
    return super.onInterceptTouchEvent(ev);
}

🎬 ​​终章:一场丝滑的滑动演出(完整代码示例)​

java
Copy
public class DraggableView extends View {
    private Scroller mScroller;
    private int lastX, lastY;

    public DraggableView(Context context) {
        super(context);
        mScroller = new Scroller(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                // 方法1:直接挪房子
                // layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
                // 方法3:让父View动画布(更常用)
                ((View) getParent()).scrollBy(-offsetX, -offsetY);
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_UP:
                // 用Scroller实现弹性停止
                View parent = (View) getParent();
                mScroller.startScroll(parent.getScrollX(), 
                                     parent.getScrollY(),
                                     -parent.getScrollX(), // 弹回原点
                                     -parent.getScrollY(),
                                     500);
                invalidate();
                break;
        }
        return true;
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate(); // 持续刷帧
        }
    }
}

💎 ​​镇长总结(Key Takeaways)​

  1. ​坐标系是地基:​​ 分清 ​​绝对坐标​​(整个小镇) vs ​​视图坐标​​(自家院子) 。

  2. ​事件流是导火索:​​ DOWN -> MOVE -> UP 记录了侦探手指的轨迹 。

  3. ​四大挪移神功:​

    • layout():拆墙重建(简单粗暴)🧱
    • MarginParams:调整院子边界(需有父布局)📏
    • scrollBy/To:移动父画布(注意​​负号​​!)🎨
    • Scroller:橡皮筋弹性动画(丝滑秘诀)🎢
  4. ​冲突靠裁决:​​ 边界划分 or 方向判断,让父子View和谐共处 。

现在你已是Android小镇的“滑动魔法师”啦!✨ 下次滑动列表时,记得背后是一场精彩的房屋大冒险哦~