跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
Kotlin大前端java

Android View 滑动的几种实现方式与原理详解

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

SecGuard发布于 2025/2/7更新于 2026/5/812 浏览
Android View 滑动的几种实现方式与原理详解

Android View 滑动实现的几种常用方式

前言

在 Android 开发中,流畅的交互体验至关重要。我们经常会遇到需要自定义可滑动组件的场景,例如实现侧滑菜单、图片浏览、或者复杂的列表交互。虽然系统提供了 ScrollView、NestedScrollView、RecyclerView 等现成控件,但在某些特殊需求下,我们需要深入了解 View 滑动的底层机制来定制行为。

本文旨在系统性地讲解让 View 产生位移的几种核心方式,分析其原理、性能差异及适用场景,并提供完整的代码示例。最后将深入探讨 Scroller 类在惯性滑动中的应用,帮助开发者构建更专业的自定义滚动控件。

基础知识准备

Android 坐标系体系

理解滑动的前提是明确坐标系的定义。Android 中的坐标主要涉及屏幕坐标系和视图坐标系。

  1. 屏幕坐标系

    • 原点位于屏幕左上角 (0, 0)。
    • X 轴向右增加,Y 轴向下增加。
    • 对应 MotionEvent.getRawX() 和 getRawY(),表示触摸点距离屏幕边缘的距离。
  2. 视图坐标系

    • 每个 View 都有自己的局部坐标系。
    • View 的 left, top, right, bottom 属性是相对于父容器的。
    • MotionEvent.getX() 和 getY() 表示触摸点在当前 View 内部的相对位置。
  3. 父容器坐标系

    • 当 View 嵌套时,子 View 的坐标转换需考虑父容器的偏移量。

在实际滑动计算中,通常使用 getX() 获取当前触摸点相对于 View 的位置,结合上一次触摸点计算位移量 dx 和 dy。

View 滑动方式概览

Android View 实现滑动效果主要通过改变 View 的位置属性或画布偏移来实现。常见的方法包括:

  1. layout(): 直接修改 View 的布局参数,触发重绘和测量流程。
  2. offsetLeftAndRight() / offsetTopAndBottom(): 辅助方法,修改 left/top 值并触发重绘。
  3. translationX / translationY: 修改视觉偏移,不影响实际布局参数,适合动画。
  4. setX() / setY(): 间接设置 translation 值。
  5. scrollTo() / scrollBy(): 修改内部画布偏移,用于内容滚动而非 View 整体移动。

大多数情况下,这些操作都需要配合 onTouchEvent 中的 ACTION_MOVE 事件来计算位移量,并调用 invalidate() 刷新界面。

1. layout() 方法

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;
}

实现步骤:

  1. 在 onTouchEvent 的 ACTION_DOWN 记录初始触摸坐标。
  2. 在 ACTION_MOVE 计算 dx 和 dy。
  3. 调用 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)
    }
}

2. offsetLeftAndRight() / offsetTopAndBottom()

这两个方法是 layout 的便捷封装,专门用于调整 View 的位置而不改变其宽高。

源码逻辑:

public void offsetLeftAndRight(int offset) {
    // ... 省略
    mLeft += offset;
    mRight += offset;
    mRenderNode.offsetLeftAndRight(offset);
    // ... 省略
}

用法: 直接将上述 layout 调用替换为:

offsetLeftAndRight(dx)
offsetTopAndBottom(dy)

特点: 同样会触发布局流程,但语义更清晰,代码更简洁。适用于不需要频繁触发布局测量的场景。

3. translationX / translationY

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)
}

4. setX() / setY()

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 效果一致,但语义上更接近绝对坐标设定。

5. scrollTo() / scrollBy()

这是最常用的滚动方式,主要用于 ViewGroup 或支持滚动的 View(如 TextView)。

原理:

  • scrollTo(x, y):将内容滚动到指定坐标。
  • scrollBy(x, y):基于当前位置增量滚动。
  • 内部维护 mScrollX 和 mScrollY 变量。
  • 实际上是通过平移 Canvas 画布来实现的,View 本身的 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 的绘制位置。
  • 在自定义 View 中,如果想让整个 View 跟随手指移动,通常需要操作父容器或使用 offset 系列方法;如果是内部内容滚动,则使用 scrollTo。

实现:

// 假设这是一个可以滚动的容器
(parent as View).scrollBy(-dx, -dy)

注:传入负值是因为手指向下滑动时,我们希望内容向上移动,而 scrollBy 的正方向是向右/向下移动画布,导致内容向左/向上移动。

拓展:Scroller 与惯性滑动

上述基础方法仅处理了手指按下和移动的过程,当手指抬起时,View 会立即停止,缺乏惯性效果。为了模拟真实的滚动体验,我们需要引入 Scroller 类。

Scroller 的作用

Scroller 是一个滑动算法工具类,它根据给定的起始位置、目标位置和持续时间,计算出每一帧应该到达的位置。

关键参数:

  • startScrollX/Y: 起始滚动坐标。
  • deltaX/Y: 增量值,最终位置 = 起始 + 增量。
  • duration: 滑动持续时长(毫秒)。
  • interpolator: 插值器,控制速度曲线(如先快后慢)。

完整惯性滑动实现

要实现流畅的惯性滑动,需要结合 computeScroll() 方法和 Scroller。

  1. 初始化 Scroller:在构造函数中创建。
  2. 计算初速度:在 ACTION_UP 时计算手指离开时的速度。
  3. 启动 Scroller:调用 fling 或 startScroll。
  4. 循环刷新:在 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() 原理

computeScroll() 是 View 的一个回调方法,通常在 draw() 之前被调用。它的作用是告诉 View 是否需要继续渲染下一帧动画。

  • 如果 scroller.computeScrollOffset() 返回 true,说明滑动尚未结束,需要更新位置并重绘。
  • 如果返回 false,说明滑动已完成,停止刷新。

这种机制保证了动画的流畅性,同时避免了不必要的 CPU 消耗。

总结与对比

方法触发流程性能适用场景
layout()Measure + Layout低偶尔的位置调整
offsetLayout低简单的位移
translationDraw高动画、非布局干扰
scrollToDraw高内容滚动、内部偏移
ScrollerDraw + Timer高惯性滚动、平滑过渡

最佳实践建议:

  1. 优先使用 translationX/Y 进行轻量级位移,避免触发布局。
  2. 对于列表或内容区域滚动,使用 scrollTo 或继承 ScrollView。
  3. 必须实现惯性效果时,务必集成 Scroller 或 OverScroller。
  4. 注意 invalidate() 的调用频率,避免过度绘制。

通过掌握以上几种方式及其底层原理,开发者可以更灵活地控制 View 的行为,构建出符合预期的高质量交互界面。

目录

  1. Android View 滑动实现的几种常用方式
  2. 前言
  3. 基础知识准备
  4. Android 坐标系体系
  5. View 滑动方式概览
  6. 1. layout() 方法
  7. 2. offsetLeftAndRight() / offsetTopAndBottom()
  8. 3. translationX / translationY
  9. 4. setX() / setY()
  10. 5. scrollTo() / scrollBy()
  11. 拓展:Scroller 与惯性滑动
  12. Scroller 的作用
  13. 完整惯性滑动实现
  14. computeScroll() 原理
  15. 总结与对比
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Capacitor 实战指南:将 Web 项目打包为跨平台应用
  • Visual C++ 6.0 在 Windows 11 下的安装与兼容性配置
  • 企业级 Nginx 高性能部署与优化实战
  • Python 爬虫入门基础与 Requests 库使用指南
  • 大模型医疗落地现状:顶刊研究显示辅助诊疗能力与 CPU 部署实践
  • 机器人架构搭建核心准则:先论文论证后工程落地
  • CTF Web25:php_mt_seed 伪随机数爆破实战应用
  • AIGC 在现代教育技术中的应用实践
  • Java 线程终止的三种方式
  • 红黑树的底层原理及 C++ 实现
  • Java 云门诊 HIS 系统技术架构与核心功能介绍
  • 梦想与现实:人生的希望与奋斗
  • 学习大语言模型 (LLM) 应从哪个开源模型入手?
  • Moltbot 本地 AI 模型 + 完全独立部署指南
  • 基于 OpenCV 的 Python 自动扫雷实现
  • GLM-4.6V-Flash-WEB 识别珊瑚礁鱼类共生关系能力评估
  • Java 微服务入门:基于 Spring Boot 搭建用户管理系统
  • 鸿蒙金融理财全栈项目:运维监控、性能优化与安全加固
  • 多模态大模型综述:从基础模型到智能代理
  • Xilinx Vivado 付费 IP 核 License 状态解读与获取指南

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online