Android View 滑动的几种实现方式与原理详解
详细解析了 Android View 滑动的五种核心实现方式,包括 layout、offset、translation、setX/setY 以及 scrollTo/scrollBy。文章阐述了各方法的底层原理、性能差异及适用场景,重点介绍了如何利用 Scroller 类实现惯性滑动效果,并提供了完整的 Kotlin 代码示例。内容涵盖坐标系基础、源码分析及最佳实践对比,帮助开发者深入理解 View 位移机制,从而构建流畅的自定义滚动控件。

详细解析了 Android View 滑动的五种核心实现方式,包括 layout、offset、translation、setX/setY 以及 scrollTo/scrollBy。文章阐述了各方法的底层原理、性能差异及适用场景,重点介绍了如何利用 Scroller 类实现惯性滑动效果,并提供了完整的 Kotlin 代码示例。内容涵盖坐标系基础、源码分析及最佳实践对比,帮助开发者深入理解 View 位移机制,从而构建流畅的自定义滚动控件。

在 Android 开发中,流畅的交互体验至关重要。我们经常会遇到需要自定义可滑动组件的场景,例如实现侧滑菜单、图片浏览、或者复杂的列表交互。虽然系统提供了 ScrollView、NestedScrollView、RecyclerView 等现成控件,但在某些特殊需求下,我们需要深入了解 View 滑动的底层机制来定制行为。
本文旨在系统性地讲解让 View 产生位移的几种核心方式,分析其原理、性能差异及适用场景,并提供完整的代码示例。最后将深入探讨 Scroller 类在惯性滑动中的应用,帮助开发者构建更专业的自定义滚动控件。
理解滑动的前提是明确坐标系的定义。Android 中的坐标主要涉及屏幕坐标系和视图坐标系。
屏幕坐标系
MotionEvent.getRawX() 和 getRawY(),表示触摸点距离屏幕边缘的距离。视图坐标系
left, top, right, bottom 属性是相对于父容器的。MotionEvent.getX() 和 getY() 表示触摸点在当前 View 内部的相对位置。父容器坐标系
在实际滑动计算中,通常使用 getX() 获取当前触摸点相对于 View 的位置,结合上一次触摸点计算位移量 dx 和 dy。
Android View 实现滑动效果主要通过改变 View 的位置属性或画布偏移来实现。常见的方法包括:
大多数情况下,这些操作都需要配合 onTouchEvent 中的 ACTION_MOVE 事件来计算位移量,并调用 invalidate() 刷新界面。
layout(int l, int t, int r, int b) 是 View 定位的核心方法。它通过重新设置 View 的四边边界来改变位置。
源码逻辑简述:
public void layout(int l, int t, int r, int b) {
// ... 省略部分代码
setFrame(l, t, r, b);
// ... 省略部分代码
}
protected boolean setFrame(int left, int top, int right, int bottom) {
// 计算旧尺寸和新尺寸
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
// 判断大小是否变化
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// 赋值新的边界
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
// 更新 RenderNode 记录
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
return true;
}
实现步骤:
onTouchEvent 的 ACTION_DOWN 记录初始触摸坐标。ACTION_MOVE 计算 dx 和 dy。layout(left + dx, top + dy, right + dx, bottom + dy)。缺点: 每次调用 layout 都会触发一次完整的布局流程(measure -> layout),性能开销较大,不适合高频滚动的场景。
Kotlin 示例:
class ScrollableView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
private var lastX = 0f
private var lastY = 0f
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastX = event.x
lastY = event.y
return true
}
MotionEvent.ACTION_MOVE -> {
val dx = (event.x - lastX).toInt()
val dy = (event.y - lastY).toInt()
// 注意:这里会触发布局重排,性能较低
layout(left + dx, top + dy, right + dx, bottom + dy)
lastX = event.x
lastY = event.y
invalidate()
}
}
return super.onTouchEvent(event)
}
}
这两个方法是 layout 的便捷封装,专门用于调整 View 的位置而不改变其宽高。
源码逻辑:
public void offsetLeftAndRight(int offset) {
// ... 省略
mLeft += offset;
mRight += offset;
mRenderNode.offsetLeftAndRight(offset);
// ... 省略
}
用法:
直接将上述 layout 调用替换为:
offsetLeftAndRight(dx)
offsetTopAndBottom(dy)
特点: 同样会触发布局流程,但语义更清晰,代码更简洁。适用于不需要频繁触发布局测量的场景。
translationX 和 translationY 属于 View 的属性动画范畴,它们不会改变 View 的实际布局位置(即 left, top 不变),仅影响绘制时的偏移。
优点:
layout。缺点:
getLocationOnScreen 获取真实物理位置。实现:
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_MOVE -> {
val dx = (event.x - lastX).toInt()
val dy = (event.y - lastY).toInt()
translationX += dx
translationY += dy
lastX = event.x
lastY = event.y
invalidate()
}
}
return super.onTouchEvent(event)
}
setX(float x) 和 setY(float y) 本质上是设置 translationX 和 translationY 的快捷方式。
源码:
public void setX(float x) {
setTranslationX(x - mLeft);
}
用法:
x += dx
y += dy
setX(x)
setY(y)
这与直接修改 translationX 效果一致,但语义上更接近绝对坐标设定。
这是最常用的滚动方式,主要用于 ViewGroup 或支持滚动的 View(如 TextView)。
原理:
scrollTo(x, y):将内容滚动到指定坐标。scrollBy(x, y):基于当前位置增量滚动。mScrollX 和 mScrollY 变量。left/top 不变。源码:
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
// ... 触发重绘
}
}
注意事项:
View,scrollTo 移动的是 View 内部的内容(如 TextView 的文字)。如果 View 本身没有子元素且背景固定,调用 scrollTo 可能看不到明显效果,除非重写 draw 方法。ViewGroup,它会移动所有子 View 的绘制位置。offset 系列方法;如果是内部内容滚动,则使用 scrollTo。实现:
// 假设这是一个可以滚动的容器
(parent as View).scrollBy(-dx, -dy)
注:传入负值是因为手指向下滑动时,我们希望内容向上移动,而 scrollBy 的正方向是向右/向下移动画布,导致内容向左/向上移动。
上述基础方法仅处理了手指按下和移动的过程,当手指抬起时,View 会立即停止,缺乏惯性效果。为了模拟真实的滚动体验,我们需要引入 Scroller 类。
Scroller 是一个滑动算法工具类,它根据给定的起始位置、目标位置和持续时间,计算出每一帧应该到达的位置。
关键参数:
startScrollX/Y: 起始滚动坐标。deltaX/Y: 增量值,最终位置 = 起始 + 增量。duration: 滑动持续时长(毫秒)。interpolator: 插值器,控制速度曲线(如先快后慢)。要实现流畅的惯性滑动,需要结合 computeScroll() 方法和 Scroller。
ACTION_UP 时计算手指离开时的速度。fling 或 startScroll。computeScroll() 中检查 scroller.computeScrollOffset(),若未结束则更新位置并 invalidate()。完整代码示例:
class InertialScrollableView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
private var lastX = 0f
private var lastY = 0f
private var velocityTracker: VelocityTracker? = null
private val scroller: Scroller
init {
scroller = Scroller(context)
// 配置过冲效果
overScrollMode = OVER_SCROLL_NEVER
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
velocityTracker?.recycle()
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain()
}
velocityTracker?.addMovement(event)
when (event.action) {
MotionEvent.ACTION_DOWN -> {
lastX = event.x
lastY = event.y
scroller.forceFinished(true)
return true
}
MotionEvent.ACTION_MOVE -> {
val dx = (event.x - lastX).toInt()
val dy = (event.y - lastY).toInt()
// 使用 offset 方法移动整个 View
offsetLeftAndRight(-dx)
offsetTopAndBottom(-dy)
lastX = event.x
lastY = event.y
invalidate()
}
MotionEvent.ACTION_UP -> {
velocityTracker?.computeCurrentVelocity(1000)
val vx = velocityTracker?.xVelocity ?: 0f
val vy = velocityTracker?.yVelocity ?: 0f
// 启动惯性滑动
scroller.fling(
scrollX, scrollY,
vx.toInt(), vy.toInt(),
Integer.MIN_VALUE, Integer.MAX_VALUE,
Integer.MIN_VALUE, Integer.MAX_VALUE
)
invalidate()
}
}
return true
}
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.currX, scroller.currY)
invalidate()
}
}
}
computeScroll() 是 View 的一个回调方法,通常在 draw() 之前被调用。它的作用是告诉 View 是否需要继续渲染下一帧动画。
scroller.computeScrollOffset() 返回 true,说明滑动尚未结束,需要更新位置并重绘。false,说明滑动已完成,停止刷新。这种机制保证了动画的流畅性,同时避免了不必要的 CPU 消耗。
| 方法 | 触发流程 | 性能 | 适用场景 |
|---|---|---|---|
| layout() | Measure + Layout | 低 | 偶尔的位置调整 |
| offset | Layout | 低 | 简单的位移 |
| translation | Draw | 高 | 动画、非布局干扰 |
| scrollTo | Draw | 高 | 内容滚动、内部偏移 |
| Scroller | Draw + Timer | 高 | 惯性滚动、平滑过渡 |
最佳实践建议:
translationX/Y 进行轻量级位移,避免触发布局。scrollTo 或继承 ScrollView。Scroller 或 OverScroller。invalidate() 的调用频率,避免过度绘制。通过掌握以上几种方式及其底层原理,开发者可以更灵活地控制 View 的行为,构建出符合预期的高质量交互界面。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online