朋友!咱们不聊枯燥的代码,今天化身“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
瞬移的尴尬,实现“手指松开后继续滑”的效果。
原理三件套:
橡皮筋规划师:
Scroller
计算每一帧的位置(但不真动房子)。刷帧小助手:
invalidate()
触发重绘。帧执行人:
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
(父) vsListView
(子)水平滑动:
ViewPager
(父) vs 横向ScrollView
(子)
交警解决方案:
-
边界裁决法: 在父View的边缘区域滑动归父,内部滑动归子(类似抽屉导航边缘触发)。
-
方向裁决法: 判断滑动的方向——水平滑动父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)
-
坐标系是地基: 分清 绝对坐标(整个小镇) vs 视图坐标(自家院子) 。
-
事件流是导火索:
DOWN -> MOVE -> UP
记录了侦探手指的轨迹 。 -
四大挪移神功:
layout()
:拆墙重建(简单粗暴)🧱MarginParams
:调整院子边界(需有父布局)📏scrollBy/To
:移动父画布(注意负号!)🎨Scroller
:橡皮筋弹性动画(丝滑秘诀)🎢
-
冲突靠裁决: 边界划分 or 方向判断,让父子View和谐共处 。
现在你已是Android小镇的“滑动魔法师”啦!✨ 下次滑动列表时,记得背后是一场精彩的房屋大冒险哦~