NumberPicker分析(三)

news2024/11/18 6:44:39

NumberPicker分析(三)

这一节主要用来分析NumberPicker的事件处理及滚动
NumberPicker继承自LinearLayout,是一个ViewGroupViewGroup事件处理的顺序大致如下:

  1. dispatchTouchEvent
  2. onInterceptTouchEvent
  3. onTouchEvent

事件处理顺序
另外,源码中实现滚轮的滚动,使用到了Scroller ,以及 View的scrollToscrollBy方法,也需要对其有一定的了解

View的scrollTo和scrollBy

scrollBy方法

public void scrollBy(int x, int y)

scrollBy是在现有位置的基础上移动

scrollTo方法

public void scrollTo(int x, int y)

scrollTo则是在初始位置的基础上移动

scrollToscrollBy移动的时候,没有动画,要实现动画的过程,可借助Scroller

Scroller

Scroller是专门处理滚动效果的工具类

其使用方式是:
1.初始化

NumberPicker中的mFlingScrollermAdjustScroller

// create the fling and adjust scrollers
mFlingScroller = new Scroller(getContext(), null, true);
mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f));

2.调用startScroll

public void startScroll(int startX, int startY, int dx, int dy, int duration
  • startX, startY - 开始移动时的x,y坐标
  • dx - 沿x轴移动的距离
  • dy - 沿y轴移动的距离
  • duration - 整个移动过程所耗费的时间

该方法,根据插值器和起始、终止位置来计算当前应该移动到的位置,并反馈给用户,其只做数值计算,不会真正的移动View

需要注意的是,在调用startScroll函数后,需要调用invalidate函数来重绘View。由此可见,Scroller类只能在自定义的ViewViewGroup中 使用,因为只有它们有invalidate函数

3.在computeScroll(computeScrollView类中函数)中处理计算出的数值

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

在调用startScroll函数后,就会在Scroller内部用一个线程来计算,从起始位置沿X轴移动dx,沿Y轴方向移动dy,每毫秒控件应该在的位置。用户可以通过scroller.getCurrXscroller.getCurrY函数来获取当前计算得到的位置信息。
computeScrollOffset()方法,当Scroller还在计算中,表示当前控件还在滚动中,就会返回true。当Scroller计算结束,就会返回false
要想移动控件,就必须使用scrollTo函数,所以要每计算出一个新位置就让View重绘一次。所以步骤3也要调用invalidate函数

另外还用到了其fling方法:

public void fling(int startX, int startY, int velocityX, int velocityY,
                      int minX, int maxX, int minY, int maxY)

用于带速度的滑动,行进的距离将取决于投掷的初始速度。可以用于实现类似 RecycleView 的滑动效果

  • startX - 开始滑动点的x坐标
  • startY - 开始滑动点的y坐标
  • velocityX - 水平方向的初始速度,单位为每秒多少像素(px/s)
  • velocityY - 垂直方向的初始速度,单位为每秒多少像素(px/s)
  • minX - x坐标最小的值,最后的结果不会低于这个值;
  • maxX - x坐标最大的值,最后的结果不会超过这个值;
  • minY - y坐标最小的值,最后的结果不会低于这个值;
  • maxY - y坐标最大的值,最后的结果不会超过这个值;

VelocityTracker

VelocityTracker 是一个跟踪触摸事件滑动速度的帮助类,用于实现flinging以及其它类似的手势。它的原理是把触摸事件 MotionEvent 对象传递给VelocityTrackeraddMovement(MotionEvent)方法,然后分析MotionEvent 对象在单位时间类发生的位移来计算速度。你可以使用getXVelocity()getXVelocity()获得横向和竖向的速率,但是使用它们之前请先调用computeCurrentVelocity(int)来初始化速率的单位 。

对上面的知识有基本了解后,继续分析

滚动事件分析

暂时把dispatchTouchEventonInterceptTouchEvent 放一旁,从onTouchEvent方法入手,可能比较易懂点

onTouchEvent方法

先看MotionEvent.ACTION_MOVE这个Action

MotionEvent.ACTION_MOVE

            case MotionEvent.ACTION_MOVE: {
                if (mIgnoreMoveEvents) {
                    break;
                }
                float currentMoveY = event.getY();
                if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
                    int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
                    if (deltaDownY > mTouchSlop) {
                        removeAllCallbacks();
                        // Scroll State变化了
                        onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
                    }
                } else {
                    int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY));
                    // 滚动一段距离
                    scrollBy(0, deltaMoveY);
                    // 重绘
                    invalidate();
                }
                mLastDownOrMoveEventY = currentMoveY;
            } break;

假设在初始状态开始缓慢的滚动NumberPicker

1.mScrollState初始值为OnScrollListener.SCROLL_STATE_IDLE,所以会进入第一个if判断里面
2.如果滑动值大于mTouchSlop(系统所能识别出的,被认为是滑动的最小距离),则进入第二个if里面
在这个if里面,会将mScrollState设置为OnScrollListener.SCROLL_STATE_TOUCH_SCROLL
3.所以,如果继续滑动话,就会进入else这个判断,开始scroll

scrollBy(0, deltaMoveY);
invalidate();

scrollBy分析

scrollByView中的方法,NumberPicker重写了scrollBy方法,如下:

    @Override
    public void scrollBy(int x, int y) {
        int[] selectorIndices = mSelectorIndices;
        int startScrollOffset = mCurrentScrollOffset;
        ...
        // mCurrentScrollOffset来时累加滚动距离
        mCurrentScrollOffset += y;
        // 处理向下滚动
        while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {
            mCurrentScrollOffset -= mSelectorElementHeight;
            decrementSelectorIndices(selectorIndices);
            setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
            ...
        }
        // 处理向上滚动
        while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) {
            mCurrentScrollOffset += mSelectorElementHeight;
            incrementSelectorIndices(selectorIndices);
            setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
            ...
        }
        if (startScrollOffset != mCurrentScrollOffset) {
            onScrollChanged(0, mCurrentScrollOffset, 0, startScrollOffset);
        }
    }

其中有2个while循环(有些类似),如何理解,以第一个while为例

1.mCurrentScrollOffset += ymCurrentScrollOffset累加移动的距离

2.如何理解 mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight
在上一节NumberPicker分析(二)中,可知最开始mCurrentScrollOffset = mInitialScrollOffset
mSelectorTextGapHeight可以理解为文字间的间距,如下图:
mSelectorTextGapHeight
所以mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight可以理解为:

mInitialScrollOffset + 累加的y - mInitialScrollOffset > mSelectorTextGapHeight

累加的y>mSelectorTextGapHeight

所以如果累计移动的距离,大于了mSelectorTextGapHeight,则会进入while循环中:

// 如果累计移动的距离,大于了mSelectorTextGapHeight,表示控件往下滑动了大于mSelectorTextGapHeight的距离
while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {
	// 调整mCurrentScrollOffset
    mCurrentScrollOffset -= mSelectorElementHeight;
    // 重新计算SelectorIndices
    decrementSelectorIndices(selectorIndices);
    // 更新当前值
    setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
    if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
        mCurrentScrollOffset = mInitialScrollOffset;
    }
}

/**
 * Decrements the <code>selectorIndices</code> whose string representations
 * will be displayed in the selector.
 */
private void decrementSelectorIndices(int[] selectorIndices) {
    for (int i = selectorIndices.length - 1; i > 0; i--) {
        selectorIndices[i] = selectorIndices[i - 1];
    }
    int nextScrollSelectorIndex = selectorIndices[1] - 1;
    //判断减1后是否小于最小值
    if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {
        nextScrollSelectorIndex = mMaxValue;
    }
    selectorIndices[0] = nextScrollSelectorIndex;
    //缓存
    ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
}

/**
 * Sets the current value of this NumberPicker.
 *
 * @param current The new value of the NumberPicker.
 * @param notifyChange Whether to notify if the current value changed.
 */
private void setValueInternal(int current, boolean notifyChange) {
    if (mValue == current) {
        return;
    }
    // Wrap around the values if we go past the start or end
    if (mWrapSelectorWheel) {
        current = getWrappedSelectorIndex(current);
    } else {
        current = Math.max(current, mMinValue);
        current = Math.min(current, mMaxValue);
    }
    int previous = mValue;
    mValue = current;
    // If we're flinging, we'll update the text view at the end when it becomes visible
    if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) {
        updateInputTextView();
    }
    if (notifyChange) {
        notifyChange(previous, current);
    }
    // 再初始化SelectorWheelIndices
    initializeSelectorWheelIndices();
    // 重绘
    invalidate();
}

上面的代码可理解为:
a.往下滑动了大于mSelectorTextGapHeight的距离
b.后移selectorIndices数组,如最开始selectorIndices[4, 0, 1],后移一位变成[4, 4, 0] (上面的循环方法,i == 0 时暂时不处理)
c.由于是往下滑动,数组的第一个元素就必须是selectorIndices[1] - 1(原来的第一个值减去1,即当前的第二个值减去1),即变成[3, 4, 0]
d.根据最新的selectorIndices,更新当前值mValue
e.再根据当前值mValue,计算selectorIndices
f.重绘

重绘时调用onDraw方法,此时mCurrentScrollOffset累加上了移动距离,所以绘制的文字位置也发生了变化
文字位置发生了变化

MotionEvent.ACTION_UP

考虑一个问题,如果滑动结束后,滚轮中的字符串没有居中对齐,是不是还需要继续处理?
所以,在手指抬起来的MotionEvent.ACTION_UP事件中,还需要处理继续滚动。这里有大致有2个判断:

  1. 如果用户滑动的速度很快,手指抬起时,滚轮flinging,需要一个减速过程才停止下来
  2. 如果手指离开时,滚轮速度不快,也需要对齐滚轮中的字符串
            case MotionEvent.ACTION_UP: {
                ...
                // VelocityTracker追踪滑动速度
                VelocityTracker velocityTracker = mVelocityTracker;
                // 计算滑动速度
                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
                // 获取Y轴速度
                int initialVelocity = (int) velocityTracker.getYVelocity();
                // 大于mMinimumFlingVelocity,开始fling
                if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {
                    fling(initialVelocity);
                    onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                } else {
                    int eventY = (int) event.getY();
                    int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY);
                    long deltaTime = event.getEventTime() - mLastDownEventTime;
                    if (deltaMoveY <= mTouchSlop && deltaTime < ViewConfiguration.getTapTimeout()) {
                        if (mPerformClickOnTap) {
                            ...
                        } else {
                            ...
                        }
                    } else {
                    	// 调整滚轮
                        ensureScrollWheelAdjusted();
                    }
                    // 更新滚动状态
                    onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                }
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            } break;

fling

fling方法用于带初速滑动,当滚轮往下滚动时,velocityY>0,往上滚动,velocityY<0

这里使用的mFlingScroller

    /**
     * Flings the selector with the given <code>velocityY</code>.
     */
    private void fling(int velocityY) {
        mPreviousScrollerY = 0;

        if (velocityY > 0) {
            mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
        } else {
            mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
        }

        invalidate();
    }

ensureScrollWheelAdjusted

ensureScrollWheelAdjusted方法用于,调整滚轮,确保最后的状态没有偏移,且中间元素居中显示

这里使用的是mAdjustScroller

    /**
     * Ensures that the scroll wheel is adjusted i.e. there is no offset and the
     * middle element is in the middle of the widget.
     *
     * @return Whether an adjustment has been made.
     */
    private boolean ensureScrollWheelAdjusted() {
        // adjust to the closest value
        int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
        if (deltaY != 0) {
            mPreviousScrollerY = 0;
            // 如果滚动的距离大于mSelectorElementHeight / 2
            if (Math.abs(deltaY) > mSelectorElementHeight / 2) {
                deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight;
            }
            // 调整滚动
            mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
            // 重绘
            invalidate();
            return true;
        }
        return false;
    }

1.如果往下滚动,滚动的距离大于mSelectorElementHeight / 2mInitialScrollOffset - mCurrentScrollOffset得到的为负值,所以deltaY += mSelectorElementHeight
2.如果往上滚动,滚动的距离大于mSelectorElementHeight / 2mInitialScrollOffset - mCurrentScrollOffset得到的为正值,所以deltaY += -mSelectorElementHeight

computeScroll

computeScrollView中的方法,使用了Scroller,则需要重写该方法

    @Override
    public void computeScroll() {
        Scroller scroller = mFlingScroller;
        if (scroller.isFinished()) {
            scroller = mAdjustScroller;
            if (scroller.isFinished()) {
                return;
            }
        }
        // 必须调用此方法
        scroller.computeScrollOffset();
        int currentScrollerY = scroller.getCurrY();
        if (mPreviousScrollerY == 0) {
            mPreviousScrollerY = scroller.getStartY();
        }
        // 又进入了`scrollBy`方法
        scrollBy(0, currentScrollerY - mPreviousScrollerY);
        mPreviousScrollerY = currentScrollerY;
        if (scroller.isFinished()) {
            onScrollerFinished(scroller);
        } else {
        	// 重绘
            invalidate();
        }
    } 

    /**
     * Callback invoked upon completion of a given <code>scroller</code>.
     */
    private void onScrollerFinished(Scroller scroller) {
        if (scroller == mFlingScroller) {
        	// 调整位置
            ensureScrollWheelAdjusted();
            updateInputTextView();
            onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
        } else {
            if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
                updateInputTextView();
            }
        }
    }

其它

参考:

  • 让控件如此丝滑Scroller和VelocityTracker的API讲解与实战——Android高级UI

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/445205.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ADSP21489之CCES开发笔记(十)

ADI21489定时器设计思路&#xff1a; 1、配置Power management control register. 2、定义时钟中断调用函数接口及实现。。 3、指定时钟中断间隔。 4、启用时钟timer。 demo代码实现2~4,如下代码 #include <services/int/adi_int.h> #include <stdio.h> #include &…

consul集群搭建教程 - 单机器集群

简言 1. 上一篇博客我们讲了consul多机器集群的部署&#xff0c;consul集群搭建教程 - 多机集群_YZF_Kevin的博客-CSDN博客 2. 很多同学没有多个机器&#xff0c;只想在单台机器上实验下consul集群&#xff0c;所以这篇博客我们讲单台机器的consul集群部署 3. consul的各个版…

mapreduce打包提交执行wordcount案例

文章目录 一、源代码1. WordCountMapper类2. WordCountReducer类3. WordCountDriver类4. pom.xml 二、相关操作和配置1. 项目打包2. 带参测试3. 上传打包后的jar包和测试文档4. 增大虚拟内存5.启动集群6.在hdfs上创建输入文件夹和上传测试文档Hello.txt7. 利用jar包在hdfs实现文…

TX-LCN:分布式事务框架

文章目录 概念LCN模式创建父工程parent创建子工程TxManager: 管理事务创建子工程: Eureka Server 注册中心创建子工程: book: 被远程调用方创建子工程: student: 远程调用方 TCC模式在lcn的基础上创建子工程: redistest在student 调用 redistest 概念 TX-LCN由两大模块组成&am…

设计模式:行为型模式 - 策略模式

文章目录 1.概述2.结构3.案例实现4.优缺点5.使用场景6.JDK源码解析 1.概述 先看下面的图片&#xff0c;我们去旅游选择出行模式有很多种&#xff0c;可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。 作为一个程序猿&#xff0c;开发需要选择一款开发工具&#xff0c;当然…

基于SpringBoot医养中心管理系统

有需要请私信或看评论链接哦 可远程调试 SpringBoot医养中心管理系统 一 介绍 基于SpringBoot医养中心管理系统-登录角色分为用户和管理员。用户登录后可查看个人信息/家人情况&#xff0c;生活情况和收费标准。管理员登录后台可进行账号管理&#xff0c;健康管理&#xff0c…

如何在Android面试中脱颖而出,高频Android面试题解析,帮你快速拿到Offer

Android面试就“小技巧” 了解自己的技能水平&#xff1a;在面试前&#xff0c;确保你对所面试的职位的技能要求有足够的了解&#xff0c;并检查自己的技能水平是否符合这些要求。熟悉面试流程&#xff1a;了解面试过程中可能会遇到的问题&#xff0c;并为每个问题准备好回答。…

itop-3568开发板驱动学习笔记(20)中断线程化

《【北京迅为】itop-3568开发板驱动开发指南.pdf》 学习笔记 文章目录 中断线程化简介中断线程化 API中断线程化实验 中断线程化简介 中断线程化也是中断下文的一种方式&#xff0c;与工作队列和软中断不同的是&#xff0c;中断线程只用于这个中断&#xff0c;当发生中断的时候…

Java基于POI动态合并单元格

Java使用poi导出excel 前言1.Excel和POI对象对应关系&#xff1a;2.POI创建Excel的步骤 一、引入依赖二、示例1.准备数据2.创建Excel工作簿对象3.给excel创建表头4.填充数据5.浏览器访问下载excel6.完整代码 前言 有个需求需要后端将数据导出为excel。并且excel中需要合并单元格…

linux安装java1.8

前言 安装java1.8是为了适配pyspark&#xff0c; 出现错误&#xff1a;pyspark.sql.utils.IllegalArgumentException: Unsupported class file major version 55\56\57\60 通过“java -version”看一下java版本&#xff0c;发现版本是java11&#xff0c;应该安装1.8才对 1、…

GaussDB工作级开发者认证—第二章GaussDB数据库应用程序开发指引

一. 驱动概述 GaussDB客户端接入认证&#xff0c;GaussDB支持以下三种认证方式&#xff1a;基于主机的认证口令认证SSL加密 二. JDBC接口介绍 1. JDBC概述 Java数据库连接&#xff08;JDBC&#xff09;是Java标准&#xff0c;它提供了从Java连接到关系数 据库的接口&#x…

C++智能指针shared_ptr详解

智能指针shared_ptr详解 一、简介二、底层原理2.1、引用计数2.2、shared_ptr的构造和析构2.3、shared_ptr的共享和拷贝2.4、循环引用问题 三、shared_ptr的使用3.1、创建一个shared_ptr3.2、共享一个shared_ptr3.3、使用删除器3.4、解除关联 四、使用示例总结 一、简介 C智能指…

软件测试拿了几个20K offer,分享一波面经

1、你的测试职业发展是什么?  测试经验越多&#xff0c;测试能力越高。所以我的职业发展是需要时间积累的&#xff0c;一步步向着高级测试工程师奔去。而且我也有初步的职业规划&#xff0c;前3年积累测试经验&#xff0c;按如何做好测试工程师的要点去要求自己&#xff0c;不…

MySQL笔记-函数,约束

本文标签 : 数据库函数 约束 目录 一、函数 1.字符串函数. 2.数值函数. 3.日期函数. 4.流程函数 二、约束 1.概述 2.约束演示 3.外键约束 1.概念 : 2. 实现: 3.删除/更新行为: 三、总结 一、函数 1.字符串函数. 实现: -- 函数演示 ---- 语法: select 函数(参数);-- …

跨域和网关通俗小白理解

跨域 跨域就是协议域名端口不同的服务器不能互相请求&#xff0c;企业级解决办法一般是通过Nginx反向代理实现 我们服务&#xff0c;线上都是通过S3服务器的Nginx反向代理解决跨域问题&#xff0c;因为Nginx和服务端沟通属于服务器之间的问题&#xff0c;不像浏览器有同源策略…

哇塞,炫云的智能优化太厉害啦!渲染费用竟然大幅降低了!

你有没有遇到过因为设置参数错误而导致云渲染费用突然飙升的情况呢&#xff1f;或者不知道自己设置的参数是否过高&#xff1f;现在&#xff0c;这些问题都可以轻松解决了&#xff0c;因为炫云的渲染质量功能非常智能和人性化。根据不同用户需求&#xff0c;它将参数优化分为五…

二进制部署nacos、docker部署nacos、k8s部署nacos、helm部署nacos

目录 前言Nacos支持三种部署模式官方文档二进制部署nacos&#xff08;单机模式&#xff09;安装jdk创建数据库及用户名下载安装包并解压导入nacos的表结构修改配置文件&#xff0c;启动nacos&#xff0c;登录nacos 二进制部署nacos&#xff08;cluster模式&#xff09;安装jdk&…

干货 | 什么是高频电解电容,它有普通电解电容有什么区别?

高频电解电容是一种常见的电容器&#xff0c;它在高频电路中发挥着重要的作用。与普通电解电容不同&#xff0c;高频电解电容能够更好地适应高频电路的需求&#xff0c;具有更高的频率响应和更低的ESR&#xff08;等效串联电阻&#xff09;。 电解电容重要性&#xff1a;电解电…

射频功率放大器在超声换能器声场特性校准中的应用

实验名称&#xff1a;基于水听器法的超声换能器声场特性校准技术的研究 研究方向&#xff1a;超声换能器 测试目的&#xff1a; 超声无损检测是无损检测领域重要的技术之一&#xff0c;而换能器作为超声检测中的关键部件&#xff0c;广泛应用于工业检测和医用超声成像领域。其性…

opencv配置安装

opencv配置安装 1、安装方式 https://blog.csdn.net/qq_45022687/article/details/120241068 根据这个网址的配置进行安装 2、解编译 mutex/thread等 似乎因为mingw的问题,#include 等直接引入线程无法直接引用&#xff0c;这导致了原有代码中直接使用mutex/thread的部分需要…