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

另外,源码中实现滚轮的滚动,使用到了Scroller ,以及 View的scrollTo、scrollBy方法,也需要对其有一定的了解
View的scrollTo和scrollBy
scrollBy方法
public void scrollBy(int x, int y)
scrollBy是在现有位置的基础上移动
scrollTo方法
public void scrollTo(int x, int y)
scrollTo则是在初始位置的基础上移动
scrollTo和scrollBy移动的时候,没有动画,要实现动画的过程,可借助Scroller
Scroller
Scroller是专门处理滚动效果的工具类
其使用方式是:
1.初始化
如NumberPicker中的mFlingScroller和mAdjustScroller
// 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类只能在自定义的View或ViewGroup中 使用,因为只有它们有invalidate函数
3.在computeScroll(computeScroll是View类中函数)中处理计算出的数值
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
在调用
startScroll函数后,就会在Scroller内部用一个线程来计算,从起始位置沿X轴移动dx,沿Y轴方向移动dy,每毫秒控件应该在的位置。用户可以通过scroller.getCurrX、scroller.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 对象传递给VelocityTracker的addMovement(MotionEvent)方法,然后分析MotionEvent 对象在单位时间类发生的位移来计算速度。你可以使用getXVelocity() 或getXVelocity()获得横向和竖向的速率,但是使用它们之前请先调用computeCurrentVelocity(int)来初始化速率的单位 。
对上面的知识有基本了解后,继续分析
滚动事件分析
暂时把dispatchTouchEvent 和 onInterceptTouchEvent 放一旁,从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分析
scrollBy是View中的方法,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 += y,mCurrentScrollOffset累加移动的距离
2.如何理解 mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight?
在上一节NumberPicker分析(二)中,可知最开始mCurrentScrollOffset = mInitialScrollOffset
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个判断:
- 如果用户滑动的速度很快,手指抬起时,滚轮flinging,需要一个减速过程才停止下来
- 如果手指离开时,滚轮速度不快,也需要对齐滚轮中的字符串
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 / 2,mInitialScrollOffset - mCurrentScrollOffset得到的为负值,所以deltaY += mSelectorElementHeight
2.如果往上滚动,滚动的距离大于mSelectorElementHeight / 2,mInitialScrollOffset - mCurrentScrollOffset得到的为正值,所以deltaY += -mSelectorElementHeight
computeScroll
computeScroll是View中的方法,使用了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



















