1. 从0到1:Android自定义View打造沉浸式FM收音机刻度滑动控件

211 阅读14分钟

在新能源车载APP中,收音机频率调节是一个常见需求。

本文将带你实现一个高度定制化的收音机刻度滑动控件,支持流畅滑动、惯性滚动、边界回弹等特性。

1.效果图

Screenrecording_20250721_155636_20250721_155738.gif

2.功能需求

显示87.5MHz到108.0MHz的频率刻度,并将当前频率指示器居中显示

  1. 显示87.5MHz到108.0MHz的频率范围
  2. 支持手势滑动调节频率
  3. 当前频率指示器始终居中
  4. 支持惯性滑动
  5. 边界回弹效果
  6. 自定义刻度样式

3.实现思路

3.1 分析思路,分为5步,实现4个版本

第一步: 测量
第二步: 绘制逻辑

通过上面实现第一个版本MINI

第三步: 触摸事件处理

通过上面实现第而个版本,局部正常功能的 Normal

第四步 : 添加惯性滑动

有了第3个版本 Plus

第五步: 边界回弹效果

有了第4个版本 Utral

3.2 具体的分析

3.2.1 测量

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 获取父容器建议的宽度值(不包含模式信息)
    int width = MeasureSpec.getSize(widthMeasureSpec);
    
    // 将默认高度80dp转换为像素值
    int height = dpToPx(80); // 默认高度80dp

    /**
     * 处理高度测量规格:
     * 
     * MeasureSpec.AT_MOST 对应 XML 中的 wrap_content 模式
     * 当父容器指定高度为 wrap_content 时,我们需要确保视图高度不会超过父容器允许的最大高度
     * 
     * 这里取默认高度和父容器允许的最大高度中的较小值
     */
    if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
        // 获取父容器允许的最大高度值
        int maxAllowedHeight = MeasureSpec.getSize(heightMeasureSpec);
        // 使用默认高度和允许高度的较小值
        height = Math.min(height, maxAllowedHeight);
    }
    
    /**
     * 为什么不处理 EXACTLY 模式?
     * 
     * 当测量模式是 MeasureSpec.EXACTLY 时(对应 match_parent 或具体数值)
     * 父容器已经明确指定了视图高度,我们不需要额外处理
     * 
     * 例如:layout_height="100dp" 或 layout_height="match_parent"
     * 系统会自动使用 MeasureSpec.getSize() 的值
     */

    /**
     * 设置最终测量的视图尺寸
     * 
     * 宽度:使用父容器建议的宽度(match_parent 或具体数值)
     * 高度:根据上述逻辑计算出的高度值
     */
    setMeasuredDimension(width, height);
}
  • 默认高度:80dp 转换为像素值

  • AT_MOST 模式(对应 wrap_content):

    • 获取父容器允许的最大高度
    • 取默认高度和允许高度的较小值
  • EXACTLY 模式match_parent 或具体数值):

    • 不需要额外处理,系统会使用 MeasureSpec.getSize() 的值

3.2.2 绘制逻辑

通过上面实现第一个版本MINI

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // 获取视图尺寸
    int width = getWidth();
    int height = getHeight();
    float centerY = height / 2f;  // 垂直中心线位置

    // 1. 背景绘制
    canvas.drawColor(Color.WHITE);  // 设置白色背景

    // 2. 计算基础偏移量(实现频率滑动效果)
    float frequencyRange = MAX_FREQUENCY - MIN_FREQUENCY;
    // 计算当前频率在总范围内的比例(0.0~1.0)
    float frequencyRatio = (currentFrequency - MIN_FREQUENCY) / frequencyRange;
    // 核心计算:通过频率比例计算画布偏移量,使当前频率点位于屏幕中央
    float baseOffset = width / 2f - frequencyRatio * width;

    // 3. 处理回弹效果
    float totalOffset = baseOffset;  // 最终使用的总偏移量
    if (overscrollDistance != 0) {
        // 将回弹距离转换为像素偏移(保持与频率比例一致)
        totalOffset += overscrollDistance / frequencyRange * width;

        // 绘制回弹边界指示条
        if (overscrollDistance < 0) {
            // 左边界回弹:在左侧绘制10dp宽度的提示条
            canvas.drawRect(0, 0, dpToPx(10), height, overscrollPaint);
        } else {
            // 右边界回弹:在右侧绘制提示条
            canvas.drawRect(width - dpToPx(10), 0, width, height, overscrollPaint);
        }
    }

    // 4. 绘制主刻度线(1MHz间隔)
    float majorStep = 1.0f;
    for (float freq = MIN_FREQUENCY; freq <= MAX_FREQUENCY; freq += majorStep) {
        // 计算刻度线X坐标:基于总偏移量 + 频率位置比例
        float x = totalOffset + (freq - MIN_FREQUENCY) / frequencyRange * width;

        // 仅绘制可见区域内的刻度(左右各扩展100px缓冲区)
        if (x >= -100 && x <= width + 100) {
            // 绘制主刻度线(中心线上下对称延伸)
            canvas.drawLine(x, centerY - dpToPx(majorLineHeight/2),
                    x, centerY + dpToPx(majorLineHeight/2), linePaint);

            // 绘制主刻度标签(位于刻度线下方)
            String label = String.format("%.0f", freq);  // 整数MHz
            canvas.drawText(label, x, centerY + dpToPx(majorLineHeight/2 + textMargin), textPaint);
        }
    }

    // 5. 绘制次刻度线(0.1MHz间隔)
    float minorStep = 0.1f;
    for (float freq = MIN_FREQUENCY; freq <= MAX_FREQUENCY; freq += minorStep) {
        // 跳过主刻度位置(避免重复绘制)
        if (freq % majorStep == 0) continue;

        float x = totalOffset + (freq - MIN_FREQUENCY) / frequencyRange * width;
        
        if (x >= -100 && x <= width + 100) {
            // 绘制更短的次刻度线
            canvas.drawLine(x, centerY - dpToPx(minorLineHeight/2),
                    x, centerY + dpToPx(minorLineHeight/2), linePaint);
        }
    }

    // 6. 绘制中央指示器(始终位于屏幕中心)
    float indicatorX = width / 2f;
    // 绘制比主刻度更长的指示线
    canvas.drawLine(indicatorX, centerY - dpToPx(indicatorHeight/2),
            indicatorX, centerY + dpToPx(indicatorHeight/2), indicatorPaint);

    // 7. 绘制当前频率值(位于指示器上方)
    String currentText = String.format("%.1f MHz", currentFrequency);  // 带一位小数的MHz
    canvas.drawText(currentText, indicatorX, centerY - dpToPx(indicatorHeight/2 + textMargin), textPaint);
}

核心计算逻辑解析

1).频率-位置映射原理

x = totalOffset + (freq - MIN_FREQUENCY) / frequencyRange * width
  • 将频率值freq转换为画布上的X坐标 - (freq - MIN_FREQUENCY) / frequencyRange 计算当前频率在总范围内的归一化位置(0.0~1.0) - 乘以width得到在画布上的理论位置 - totalOffset实现整体滑动效果

2).滑动偏移计算,核心原理:(为了当前频率能够居中显示)

   baseOffset = width/2f - frequencyRatio * width

baseOffset 决定了这根标尺的哪一部分显示在屏幕上

  • 核心算法:通过负偏移使当前频率点frequencyRatio始终位于屏幕中心 - 当frequencyRatio=0.5(中点)时,偏移量为0 - 当frequencyRatio=0(最小值)时,偏移量为width/2,将最小频率点推到屏幕中心
  • frequencyRatio * width
    表示当前频率在标尺上的位置(从标尺起点开始计算
  • width / 2f
    表示屏幕中心位置(我们希望当前频率显示在这里)

总结:baseOffset 实现了频率标尺的动态定位,使当前选中的频率始终保持在视图中心,同时允许用户滑动浏览整个频率范围。

为什么这样设计?

这种设计实现了:

中心锁定:当前频率始终在屏幕中心

自然滑动:滑动时频率刻度从两侧进出屏幕

比例一致:不同频率范围使用相同逻辑

边界处理:轻松实现回弹效果

3).frequencyRatio:

是一个归一化的位置比例值,表示当前频率在总频率范围内的相对位置。它的计算方式和取值范围如下:

float frequencyRatio = (currentFrequency - MIN_FREQUENCY) / (MAX_FREQUENCY - MIN_FREQUENCY);

3.2.3. 触摸事件处理

@Override
public boolean onTouchEvent(MotionEvent event) {
    // 处理触摸事件
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 记录触摸起始点X坐标
            lastTouchX = event.getX();
            
            // 计算当前频率在视图中的位置(将频率映射到视图宽度比例)
            scaleStartPos = (currentFrequency - MIN_FREQUENCY) / (MAX_FREQUENCY - MIN_FREQUENCY) * getWidth();
            
            // 标记开始缩放/滚动操作
            isScaling = true;
            return true;

        case MotionEvent.ACTION_MOVE:
            if (isScaling) {
                // 计算手指水平移动距离(当前X坐标 - 起始X坐标)
                float dx = event.getX() - lastTouchX;
                
                /**
                 * 实现滚动效果的核心逻辑:
                 * 1. 根据移动距离调整频率位置:scaleStartPos - dx
                 * 2. 将新位置转换为频率值:
                 *    newPos / getWidth() = 在视图中的比例位置
                 *    (比例位置) * 频率范围 = 相对于最小频率的偏移量
                 *    MIN_FREQUENCY + 偏移量 = 实际频率值
                 */
                float newPos = scaleStartPos - dx;
                float newFrequency = MIN_FREQUENCY + (newPos / getWidth()) * (MAX_FREQUENCY - MIN_FREQUENCY);

                // 确保频率在合法范围内(87.5-108.0MHz)
                newFrequency = Math.max(MIN_FREQUENCY, Math.min(MAX_FREQUENCY, newFrequency));

                // 更新频率并重绘视图
                setCurrentFrequency(newFrequency);
            }
            return true;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            // 结束滚动操作
            isScaling = false;
            return true;
    }
    return super.onTouchEvent(event);
}

/**
 * 设置当前频率并刷新视图
 * @param frequency 要设置的频率值(MHz),范围必须在87.5到108.0之间
 */
public void setCurrentFrequency(float frequency) {
    // 确保频率在有效范围内
    if (frequency < MIN_FREQUENCY) {
        frequency = MIN_FREQUENCY;
    } else if (frequency > MAX_FREQUENCY) {
        frequency = MAX_FREQUENCY;
    }

    // 更新当前频率
    this.currentFrequency = frequency;

    // 请求重绘视图(触发onDraw调用)
    invalidate();
}

滚动效果实现原理

滑动的时候,会把当前的频率改变,要得到当前的频率! 然后进行重绘制!

1). 当前的坐标:坐标映射机制:X的坐标

scaleStartPos = (currentFrequency - MIN_FREQUENCY) / (MAX_FREQUENCY - MIN_FREQUENCY) * getWidth();

  • 将整个频率范围(87.5-108.0MHz)映射到视图宽度
  • 公式:屏幕位置 = (频率 - MIN) / (MAX - MIN) * 视图宽度
  • 示例:当视图宽度=300px时,87.5MHz→0px,108.0MHz→300px

2). 当前的频率:滚动计算过程

 float newFrequency = MIN + (newPos / width) * 频率范围;

    float newPos = scaleStartPos - dx;  // 核心滚动公式
     float newFrequency = MIN + (newPos / width) * 频率范围;
  • dx:手指水平移动距离(正→右移,负→左移)
  • 右滑动:dx为正 → newPos减小 → 频率减小
  • 左滑动:dx为负 → newPos增大 → 频率增大

3). 视觉滚动效果

  • 每次ACTION_MOVE都根据手指位移重新计算频率
  • 通过invalidate()触发视图重绘
  • onDraw()中根据新频率更新UI元素位置

3.2.4 添加惯性滑动

@Override
public boolean onTouchEvent(MotionEvent event) {
    // 初始化速度跟踪器(用于计算滑动速度)
    if (velocityTracker == null) {
        velocityTracker = VelocityTracker.obtain();
    }
    velocityTracker.addMovement(event);  // 添加当前触摸事件

    // 处理不同类型的触摸事件
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:  // 手指按下
            handleActionDown(event);
            return true;  // 表示消耗此事件

        case MotionEvent.ACTION_MOVE:  // 手指移动
            handleActionMove(event);
            return true;

        case MotionEvent.ACTION_UP:    // 手指抬起
        case MotionEvent.ACTION_CANCEL: // 事件取消
            handleActionUp();
            return true;
    }
    return super.onTouchEvent(event);
}

private void handleActionDown(MotionEvent event) {
    // 停止当前的惯性滑动或回弹动画
    removeCallbacks(flingRunnable);
    // 记录触摸点X坐标(用于计算移动距离)
    lastTouchX = event.getX();
    // 设置滑动标志(表示正在滑动)
    isScaling = true;
    // 重置回弹标志
    isOverscrolling = false;
}

private void handleActionMove(MotionEvent event) {
    if (isScaling) {
        // 计算X方向上的位移(当前触摸点 - 上次触摸点)
        float dx = event.getX() - lastTouchX;
        // 更新最后触摸位置
        lastTouchX = event.getX();

        // 将像素位移转换为频率变化量
        // 核心公式:频率变化量 = -位移/视图宽度 * 频率范围
        // 负号表示:手指向右滑动(dx为正)时,频率减小;手指向左滑动(dx为负)时,频率增加
        float deltaFreq = -dx / getWidth() * (MAX_FREQUENCY - MIN_FREQUENCY);
        
        // 应用频率变化(包含边界回弹处理)
        adjustFrequencyWithOverscroll(deltaFreq);
    }
}

private void handleActionUp() {
    // 结束滑动状态
    isScaling = false;

    // 计算当前滑动速度(单位:像素/秒)
    // 参数1000表示时间单位毫秒(即计算每秒移动多少像素)
    velocityTracker.computeCurrentVelocity(1000);
    // 获取X方向的速度(水平滑动速度)
    lastVelocity = velocityTracker.getXVelocity();

    // 如果存在回弹距离或速度超过阈值,则启动惯性滑动或回弹动画
    if (overscrollDistance != 0 || Math.abs(lastVelocity) > FLING_THRESHOLD) {
        post(flingRunnable);  // 启动动画循环
    }

    // 回收速度跟踪器资源
    if (velocityTracker != null) {
        velocityTracker.recycle();
        velocityTracker = null;
    }
}

private void adjustFrequencyWithOverscroll(float deltaFrequency) {
    // 计算新的频率值
    float newFrequency = currentFrequency + deltaFrequency;

    // 检查是否超出边界
    if (newFrequency < MIN_FREQUENCY) {
        // 超出左边界(最小值):计算回弹距离
        // OVERSCROLL_RATIO 控制回弹强度(例如0.5表示回弹距离是超出距离的一半)
        overscrollDistance = (newFrequency - MIN_FREQUENCY) * OVERSCROLL_RATIO;
        isOverscrolling = true;  // 设置回弹状态
    } else if (newFrequency > MAX_FREQUENCY) {
        // 超出右边界(最大值)
        overscrollDistance = (newFrequency - MAX_FREQUENCY) * OVERSCROLL_RATIO;
        isOverscrolling = true;
    } else {
        // 在正常范围内:重置回弹状态
        overscrollDistance = 0;
        isOverscrolling = false;
    }

    // 更新当前频率:
    // 1. 如果在回弹状态,频率保持在边界值 + 回弹距离
    // 2. 否则使用计算的新频率
    currentFrequency = isOverscrolling ?
            (newFrequency < MIN_FREQUENCY ? MIN_FREQUENCY : MAX_FREQUENCY) + overscrollDistance :
            newFrequency;

    // 请求重绘视图(触发onDraw)
    invalidate();
}

// 惯性滑动和回弹动画的执行器
private Runnable flingRunnable = new Runnable() {
    @Override
    public void run() {
        // 1. 优先处理回弹效果
        if (overscrollDistance != 0) {
            // 应用阻尼效果:每次减少回弹距离
            overscrollDistance *= OVERSCROLL_DAMPING;  // 例如0.8
            
            // 当回弹距离足够小时停止回弹
            if (Math.abs(overscrollDistance) < 0.1f) {
                overscrollDistance = 0;
            }
            
            // 更新频率(保持在边界值 + 回弹距离)
            currentFrequency = currentFrequency < MIN_FREQUENCY ?
                    MIN_FREQUENCY + overscrollDistance :
                    MAX_FREQUENCY + overscrollDistance;

            // 请求重绘
            invalidate();

            // 如果还有回弹距离,继续动画
            if (overscrollDistance != 0) {
                postDelayed(this, 16); // 约60帧/秒(16ms/帧)
            }
            return;
        }

        // 2. 处理惯性滑动
        if (Math.abs(lastVelocity) < 50) { // 速度过小则停止
            return;
        }

        // 将速度转换为频率变化量
        // 公式:频率变化量 = -速度 / 60 / 视图宽度 * 频率范围
        // 解释:
        // 1. 除以60:将每秒速度转换为每帧速度(假设60帧/秒)
        // 2. 除以视图宽度:将像素速度转换为比例速度
        // 3. 乘以频率范围:将比例转换为实际频率变化
        // 4. 负号:速度方向与频率变化方向相反
        adjustFrequencyWithOverscroll(-lastVelocity / 60 / getWidth() * (MAX_FREQUENCY - MIN_FREQUENCY));

        // 应用减速效果:每次减少速度
        lastVelocity *= DECELERATION_RATE;  // 例如0.95(5%的减速)

        // 继续下一帧动画
        postDelayed(this, 16);
    }
};

惯性滑动(fling)实现原理

  1. 将当前触摸事件添加到速度跟踪器中
velocityTracker.addMovement(event);

2. 速度跟踪: 在 ACTION_UP 时计算出手指离开时的速度(像素/秒)

*   使用 `VelocityTracker` 记录触摸事件
// 在ACTION_UP事件中
velocityTracker.computeCurrentVelocity(1000); // 计算每1000ms(1秒)的速度
lastVelocity = velocityTracker.getXVelocity(); // 获取X轴方向速度(像素/秒)

3.启动条件判断

if (overscrollDistance != 0 || Math.abs(lastVelocity) > FLING_THRESHOLD) {
    post(flingRunnable);
}

4. 速度转换, 速度转换(核心算法)

  • 物理意义:将像素速度转换为频率变化量 float deltaFreq = -lastVelocity / 60 / getWidth() * (MAX_FREQUENCY - MIN_FREQUENCY)

  • 分解计算

1). lastVelocity / 60`:将每秒速度转换为每帧速度(假设60FPS)

2). / getWidth():将像素速度转换为屏幕比例速度

3). * (MAX_FREQUENCY - MIN_FREQUENCY):将比例速度转换为实际频率变化量

4). -负号:反转方向(手指向右滑动→频率减小)

  1. 动画循环
private Runnable flingRunnable = new Runnable() {
    @Override
    public void run() {
        // 1. 处理回弹(优先级更高)
        if (overscrollDistance != 0) {
            // ... 回弹处理逻辑
            postDelayed(this, 16); // 约60FPS
            return;
        }
        
        // 2. 处理惯性滑动
        if (Math.abs(lastVelocity) < 50) return; // 停止条件
        
        // 应用频率变化
        adjustFrequencyWithOverscroll(-lastVelocity / 60 / getWidth() * freqRange);
        
        // 速度衰减
        lastVelocity *= DECELERATION_RATE; // 例如0.95
        
        // 继续下一帧
        postDelayed(this, 16); // 约60FPS
    }
};
  • 使用 postDelayed(this, 16) 实现60fps动画
  • 每帧应用频率变化并重绘视图
  • 应用减速效果:lastVelocity *= DECELERATION_RATE

5. 速度衰减模型

lastVelocity *= DECELERATION_RATE; // 0.95表示每帧减速5%
  • 物理模型:模拟摩擦力作用
  • 数学原理:等比数列衰减 v = v0 * (decay)^n
  1. 停止条件
if (Math.abs(lastVelocity) < 50) return;
  • 速度衰减到阈值以下(Math.abs(lastVelocity) < 50
  • 或遇到边界转为回弹状态

3.2.5 边界回弹效果

1. 边界检测

// 在 adjustFrequencyWithOverscroll 方法中
if (newFrequency < MIN_FREQUENCY) {
    // 左边界处理
    overscrollDistance = (newFrequency - MIN_FREQUENCY) * OVERSCROLL_RATIO;
    isOverscrolling = true;
} else if (newFrequency > MAX_FREQUENCY) {
    // 右边界处理
    overscrollDistance = (newFrequency - MAX_FREQUENCY) * OVERSCROLL_RATIO;
    isOverscrolling = true;
} else {
    // 正常范围内
    overscrollDistance = 0;
    isOverscrolling = false;
}

这里 OVERSCROLL_RATIO 控制回弹强度(如0.5表示回弹距离是超出距离的一半) 2. 边界状态管理

  • MIN_FREQUENCY:频率范围最小值
  • MAX_FREQUENCY:频率范围最大值
  • isOverscrolling:标识当前是否处于回弹状态
  • overscrollDistance:记录超出边界的距离(负值表示左边界,正值表示右边界)

3. 边界更新逻辑

currentFrequency = isOverscrolling ?
        (newFrequency < MIN_FREQUENCY ? 
            MIN_FREQUENCY + overscrollDistance : 
            MAX_FREQUENCY + overscrollDistance) :
        newFrequency;

4. 边界更新UI逻辑

// 应用回弹偏移
float totalOffset = baseOffset;
if (overscrollDistance != 0) {
    totalOffset += overscrollDistance / frequencyRange * width;

    // 绘制回弹指示
    if (overscrollDistance < 0) {
        // 左边界回弹
        canvas.drawRect(0, 0, dpToPx(10), height, overscrollPaint);
    } else {
        // 右边界回弹
        canvas.drawRect(width - dpToPx(10), 0, width, height, overscrollPaint);
    }
}

4.整体的架构图

deepseek_mermaid_20250721_5499a0.png

5.总结:

5.1 VelocityTracker:

VelocityTracker 是 Android SDK 中的一个工具类,专门用于跟踪触摸事件的速度(包括水平和垂直方向的速度)。它在实现流畅的交互效果(特别是滚动和惯性滑动)时非常关键。

主要功能和用途:

功能说明
速度检测计算手指在屏幕上的移动速度(像素/秒)
方向判断区分水平(X轴)和垂直(Y轴)方向的速度
惯性滚动支持为 Fling 手势提供速度数据
手势识别帮助区分单击、长按和滑动等手势

关键方法详解:

  1. obtain()
    获取 VelocityTracker 实例(优先从对象池复用)

  2. addMovement(MotionEvent)
    添加触摸事件用于速度计算

  3. computeCurrentVelocity(int units)
    计算当前速度:

    • 参数 units = 时间单位(毫秒),常用 1000 表示"像素/秒"
    • 必须在获取速度值前调用
  4. getXVelocity() / getYVelocity()
    获取 X/Y 轴速度(单位由 computeCurrentVelocity 决定)

  5. recycle()
    释放实例回对象池(API 21+ 已废弃,改用 clear() + 复用)

5.2 惯性滑动的优化 Scroller

主要的差异点:flingRunnable,多了个computeScroll()方法,需要是实现 启动惯性滑动或回弹动画

private void handleActionUp() {
    // 计算速度并启动fling
    if (Math.abs(velocityX) > FLING_THRESHOLD) {
        float frequencyRange = MAX_FREQUENCY - MIN_FREQUENCY;
        int scrollRange = (int) (frequencyRange * getWidth());
        
        scroller.fling(
            currentScroll, 0, 
            (int) -velocityX, 0,
            -scrollRange, scrollRange, 
            0, 0,
            OVER_SCROLL_DISTANCE, 0
        );
        
        postInvalidateOnAnimation();
    }
}

实现computeScroll方法

@Override
public void computeScroll() {
    if (scroller.computeScrollOffset()) {
        int scrollX = scroller.getCurrX();
        float frequencyRange = MAX_FREQUENCY - MIN_FREQUENCY;
        float deltaFrequency = (float) scrollX / getWidth();
        
        float newFrequency = currentFrequency + deltaFrequency;
        
        // 边界检查
        if (newFrequency < MIN_FREQUENCY) {
            newFrequency = MIN_FREQUENCY;
        } else if (newFrequency > MAX_FREQUENCY) {
            newFrequency = MAX_FREQUENCY;
        }
        
        currentFrequency = newFrequency;
        postInvalidateOnAnimation();
    }
}

优势与对比

  1. 更流畅的滚动体验

    • 使用Android内置的Scroller实现物理滚动
    • 支持硬件加速的流畅动画
  2. 更自然的回弹效果

    • OverScroller提供内置的回弹效果
    • 支持自定义回弹距离和阻尼
  3. 简化的代码结构

    • 移除了手动实现的flingRunnable
    • 简化了边界处理和回弹逻辑
    • 减少了约40行代码
  4. 更好的性能

    • 利用系统级动画优化
    • 减少对象创建和GC压力
  5. 更准确的物理模拟

    • 精确的速度传递和衰减模型
    • 与Android系统滚动行为一致

6.源码

手写的VIew项目地址: github.com/pengcaihua1…