绘制K线第三章:拖拽功能实现

绘制K线第三章:拖拽功能实现

在第二章的基础上,我们添加拖拽功能,让用户可以左右滑动查看不同时间段的K线数据。

一、设计思路

新增基类处理拖拽逻辑

为了不影响原先K线的绘制逻辑,我们创建一个新的基类 ScrollableKLineView 来专门处理拖拽功能。

设计原则

  • 基类只负责拖拽和滚动逻辑
  • 子类继承基类,实现具体的绘制逻辑
  • 子类通过实现 getMinScrollX()getMaxScrollX() 来定义滚动边界

优势

  • 拖拽逻辑与绘制逻辑分离

二、模型说明

类图

ScrollableKLineView 类结构

核心组件

  • scrollX: Float:当前滚动偏移量(X方向)
    • scrollX = 0:显示最新数据(最右边)
    • scrollX < 0:向左滚动,显示更早的数据
  • GestureDetector:检测拖动手势
  • OverScroller:处理快速滑动(惯性滚动)
  • isTouching: Boolean:是否正在触摸

关键方法

  • getMinScrollX(): Float:获取最小滚动位置(子类实现)
  • getMaxScrollX(): Float:获取最大滚动位置(子类实现)
  • scrollTo(newScrollX: Float):滚动到指定位置

滚动边界

  • scrollX 的范围:[getMinScrollX(), getMaxScrollX()]
  • minScrollX:最左边的边界(负值)
  • maxScrollX:最右边的边界(0,显示最新数据)

三、核心逻辑

1. 拖动处理(onScroll)

触发时机:用户手指在屏幕上拖动

处理流程

scss 复制代码
用户拖动手指
  ↓
onScroll() 被调用
  ↓
scrollTo(scrollX + distanceX) - 计算新的滚动位置并更新
  ↓
postInvalidateOnAnimation() - 触发重绘

代码实现

kotlin 复制代码
override fun onScroll(
    e1: MotionEvent?,
    e2: MotionEvent,
    distanceX: Float,
    distanceY: Float
): Boolean {
    // distanceX > 0:手指向右拖动,数据向右移动,scrollX 增大
    // distanceX < 0:手指向左拖动,数据向左移动,scrollX 减小
    scrollTo(scrollX + distanceX)
    return true
}

private fun scrollTo(newScrollX: Float) {
    val minScrollX = getMinScrollX()
    val maxScrollX = getMaxScrollX()
    
    val oldScrollX = scrollX
    scrollX = newScrollX
    
    // 边界检查和限制
    if (scrollX < minScrollX) {
        scrollX = minScrollX
        scroller.forceFinished(true)
    } else if (scrollX > maxScrollX) {
        scrollX = maxScrollX
        scroller.forceFinished(true)
    }
    
    if (scrollX != oldScrollX) {
        postInvalidateOnAnimation()
    }
}

关键点

  • distanceX 是手指移动的距离,向右为正,向左为负
  • 滚动方向跟随手指 :手指向右拖动,数据向右移动,scrollX 增大
  • onScroll 中直接调用 scrollTo(scrollX + distanceX),简化代码逻辑
  • 边界检查在 scrollTo 中统一处理,避免回弹

2. 快速滑动处理(onFling)

触发时机:用户快速滑动后抬起手指

处理流程

scss 复制代码
用户快速滑动后抬起手指
  ↓
onFling() 被调用
  ↓
检查边界
  ↓
scroller.fling() - 启动惯性滚动
  ↓
computeScroll() - 持续更新滚动位置
  ↓
scrollTo() - 更新位置并检查边界
  ↓
postInvalidateOnAnimation() - 继续动画

代码实现

kotlin 复制代码
override fun onFling(
    e1: MotionEvent?,
    e2: MotionEvent,
    velocityX: Float,
    velocityY: Float
): Boolean {
    if (!isTouching) {
        val minScrollX = getMinScrollX()
        val maxScrollX = getMaxScrollX()
        
        // velocityX > 0:向右快速滑动,数据向右移动,scrollX 增大
        // velocityX < 0:向左快速滑动,数据向左移动,scrollX 减小
        // 边界检查:如果已经到达边界且继续向边界方向滑动,则不触发快速滑动
        if ((scrollX <= minScrollX && velocityX < 0) || (scrollX >= maxScrollX && velocityX > 0)) {
            scroller.forceFinished(true)
            return false
        }
        
        // velocityX 需要取反,以匹配我们的滚动方向
        scroller.fling(
            scrollX.toInt(), 0,
            (-velocityX).toInt(), 0,
            minScrollX.toInt(), maxScrollX.toInt(),
            0, 0
        )
        invalidate()
    }
    return true
}

关键点

  • velocityX 是滑动速度,向右为正,向左为负
  • velocityX 需要取反:因为我们的滚动方向与标准实现相反
  • 使用 OverScroller 实现平滑的惯性滚动动画

3. 滚动更新(computeScroll)

触发时机:系统自动调用,用于更新滚动动画

处理流程

scss 复制代码
系统自动调用 computeScroll()
  ↓
scroller.computeScrollOffset() - 计算下一帧位置
  ↓
scrollTo(scroller.currX) - 更新位置
  ↓
postInvalidateOnAnimation() - 继续下一帧
  ↓
重复直到 scroller 完成

代码实现

kotlin 复制代码
override fun computeScroll() {
    if (scroller.computeScrollOffset()) {
        if (!isTouching) {
            // 直接使用 scroller.getCurrX()
            // scrollTo 内部会进行边界检查和限制
            scrollTo(scroller.currX.toFloat())
            // 继续动画,直到 scroller 完成
            postInvalidateOnAnimation()
        } else {
            // 用户开始触摸,停止滚动
            scroller.forceFinished(true)
        }
    }
}

关键点

  • scrollTo 方法内部统一处理边界检查和限制
  • 到达边界时自动停止滚动,避免回弹
  • 使用 postInvalidateOnAnimation() 继续动画

4. 外部如何获取滚动边界

子类实现边界计算

KLineViewCase4 中,子类需要实现 getMinScrollX()getMaxScrollX() 来定义滚动边界:

kotlin 复制代码
override fun getMinScrollX(): Float {
    if (klineData.isEmpty() || width == 0) return 0f
    val width = width.toFloat()
    val totalCandleWidth = config.getTotalCandleWidth()
    val totalWidth = klineData.size * totalCandleWidth
    
    // 如果数据总宽度小于等于屏幕宽度,不需要滚动
    if (totalWidth <= width) {
        return 0f
    }
    
    // 最小滚动位置:确保最左边的数据刚好显示在屏幕左边界
    return (width - totalWidth)
}

override fun getMaxScrollX(): Float {
    // 最大滚动位置:0(显示最新数据,即最右边)
    return 0f
}

边界计算逻辑

  • maxScrollX = 0:显示最新数据(最右边)
  • minScrollX = width - totalWidth:确保最左边的数据刚好显示在屏幕左边界
  • 如果数据总宽度小于等于屏幕宽度,不需要滚动,返回 0f

在绘制时使用滚动位置

onDraw 中,根据 scrollX 计算可见数据范围:

kotlin 复制代码
override fun onDraw(canvas: Canvas) {
    // ... 其他绘制逻辑 ...
    
    // 根据滚动位置计算可见数据的起始索引
    val baseStartIndex = (klineData.size - visibleCount).coerceAtLeast(0)
    val scrollOffset = -scrollX  // scrollX 是负值,取反得到正偏移量(像素)
    val scrollIndex = (scrollOffset / totalCandleWidth).toInt()
    val startIndex = (baseStartIndex - scrollIndex).coerceIn(0, (klineData.size - visibleCount).coerceAtLeast(0))
    
    // 获取可见的K线数据
    val visibleData = klineData.subList(startIndex, startIndex + visibleCount)
    
    // 计算像素级偏移(用于平滑滚动)
    val pixelOffset = scrollOffset % totalCandleWidth
    
    // 绘制K线时使用 pixelOffset
    visibleData.forEachIndexed { index, entity ->
        drawCandle(canvas, entity, index, minPrice, maxPrice, height, pixelOffset)
    }
}

逻辑说明

  • scrollX = 0 时,scrollOffset = 0,显示最新数据
  • scrollX < 0 时,scrollOffset > 0,显示更早的数据
  • pixelOffset 用于实现像素级平滑滚动

四、Case3 与 Case4 的区别

核心区别

功能 Case3 Case4
可见区间处理
背景网格
价格标签
时间标签
水平拖拽
快速滑动

继承关系

markdown 复制代码
ScrollableKLineView(基类,提供拖拽功能)
    └── KLineViewCase4(滚动:实现滚动边界计算)

五、性能优化

invalidate() vs postInvalidateOnAnimation()

在滚动过程中,我们需要频繁触发重绘。选择合适的重绘方法对性能至关重要。

invalidate() 的特点

工作原理

  • 立即将当前View标记为无效
  • 请求系统在下一帧重绘
  • 同步调用,会立即加入重绘队列

执行时机

  • 调用后立即加入重绘队列
  • 如果同一帧内多次调用,可能会合并为一次重绘
  • 不保证与动画帧率同步

性能影响

  • 如果滚动过程中频繁调用,可能导致:
    • 同一帧内多次重绘请求
    • 重绘频率超过屏幕刷新率(60fps)
    • 造成不必要的性能开销
postInvalidateOnAnimation() 的特点

工作原理

  • 在下一帧动画开始前将View标记为无效
  • 与系统的动画帧率同步
  • 异步调用,自动与屏幕刷新率对齐

执行时机

  • 在下一帧动画开始时执行
  • 自动与屏幕刷新率(通常60fps)同步
  • 如果同一帧内多次调用,会自动合并

性能优势

  • 与动画帧率同步,避免过度重绘
  • 自动合并同一帧内的多次调用
  • 滚动动画更流畅,性能更好
效果

``

相关推荐
fanruitian5 小时前
uniapp android开发 测试板本与发行版本
前端·javascript·uni-app
rayufo5 小时前
【工具】列出指定文件夹下所有的目录和文件
开发语言·前端·python
STCNXPARM5 小时前
Android camera之硬件架构
android·硬件架构·camera
RANCE_atttackkk5 小时前
[Java]实现使用邮箱找回密码的功能
java·开发语言·前端·spring boot·intellij-idea·idea
2501_944525547 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 支出分析页面
android·开发语言·前端·javascript·flutter
李白你好7 小时前
Burp Suite插件用于自动检测Web应用程序中的未授权访问漏洞
前端
松☆8 小时前
Dart 核心语法精讲:从空安全到流程控制(3)
android·java·开发语言
刘一说8 小时前
Vue 组件不必要的重新渲染问题解析:为什么子组件总在“无故”刷新?
前端·javascript·vue.js
徐同保9 小时前
React useRef 完全指南:在异步回调中访问最新的 props/state引言
前端·javascript·react.js
_李小白10 小时前
【Android 美颜相机】第二十三天:GPUImageDarkenBlendFilter(变暗混合滤镜)
android·数码相机