AbsListView的setAdapter函数其实没有替换和保存新的Adapter, 也没有触发UI重绘等,只是简单的将mCheckStates/mCheckedIdStates清空, 是重点子类override函数, AbslListView中关于Item**check和select的处理逻辑还是很多的,但是这一块逻辑反而平时开发基本不会涉及(很少有需求对这些功能进行高度定制的)**
setOnScrollListener(OnScrollListener l): 除了将给的listener设置以外,还会触发一次invokeOnItemScrollListener(), 后者会触发mFastScroller/mOnScrollListener的onScroll函数: 并且还会调用一次onScrollChanged(0, 0, 0, 0)(dummy values, View’s implementation does not use these)
解释一下SmoothScrollbar这个概念:
- 如果开启了这个特性:那么scrollBar的thumb(滑块)的位置和尺寸都是基于当前可视的ItemView的像素值(pixel), 这个特性假设所有的itemView都有相同的height, 如果不能满足这个情况,会导致scrollBar在滑动时发生不应该的形变(一会儿长一会儿短), 这种情况应该关闭这个feature.
- 如果这个特性被关掉,那么thumb的位置和尺寸都仅仅取决于当前爱Adapter中item的总数和当前可见的itemView在data set中的pos, 这样就不会因为itemView的不同height造成活动中的尺寸畸变.
int computeVerticalScrollExtent(): 继承自View, 计算scroll时显示的滑块(thumb)的垂直尺寸:
* 在没有childView时, 为 0.
* 如果有childView,但是没有开启mSmoothScrollbarEnabled, 那么就是 1.
* 如果有childView并且开启了mSmoothScrollbarEnabled, 会根据child的count/height以及View的bottom等信息综得到一个合适的值.int computeVerticalScrollOffset(): 同样继承自View, 计算的是ScrollBar的thumb的在滑道(track)中的垂直偏移,其实就是thumb的垂直坐标.
- 如果没有childVIew或者mFirstPosition是无效pos,那么返回0.
- 否则:
- 如果mSmoothScrollbarEnabled: 像之前说的一样,基于Item的pixel级尺寸进行计算.
- 如果没有mSmoothScrollbarEnabled: 像之前说的一样,基于Item的总数和当前可视的item在data set中的pos决定.
int computeVerticalScrollRange(): 继承自View, 这个是计算滑道的长度. 同样的会有对SmoothScrollbarEnabled的分别处理. (这三个计算滑道与滑块的函数返回的值应该基于同一标尺)
getTopFadingEdgeStrength/getBottomFadingEdgeStrength: 继承自View, Returns the strength, or intensity, of the top faded edge.
onMeasure(int widthMeasureSpec, int heightMeasureSpec): 感觉其实没干啥实质性的工作…
void onLayout(boolean changed, int l, int t, int r, int b): 一个关键点是mInLayout开始设为true/结束时设为false, 如果changed, 那么会在当前的childView上调用forceLayout, 并将RecyclerBin里的Child也都标记为dirty(mRecycler.markChildrenDirty(), 前面分析过,其实也是调用forceLayout).如果发现ItemCount变化或者DataChanged, 那么会尝试回调FastScroller的onItemCountChanged(mItemCount), 最后调用layoutChildren(空函数,交由子类实现).
boolean setFrame(int left, int top, int right, int bottom): 继承自View,这个函数是在layout的时候被调用, 最终制定View的尺寸和位置,是一个非常重要的函数. 这里只是顺带在super.setFrame(…)返回表示change时会重新设一下mPopup的位置.
updateScrollIndicators(): 根据当前情况设置mScrollUp/mScrollDown(如果有的话)的可见性,代表这可以向上/下scroll. 其判断逻辑还是挺简单的(不过很有参考价值):
- 如果第一个/最后一个Item当前没有显示(使用mFirstPosition和childCount判断),那么显然是可以向上/下scroll的.
- 还有就是第一个/最后一个Item的上沿/下沿在AbsListView的mTop/mBottom的上/下(就是纯坐标判断了,还会考虑到padding, 如果Item的top小于padding,那么说明上沿没有显示, 同样,如果Item的Bottom已经超过了parentView的高度-padding,那么说明下沿没有显示),那么可以认为还是可以向上/下scroll的.
obtainView(int position, boolean[] isScrap): 该函数会返回一个已经绑定了数据的View, 在发现View已经不可能从recycle bin中直接重用,这时候就只能convert一个old view或者new 一个新的View.
- isScrap: 如果返回的view是重用自scrap heap, 那么这个数组的第一个成员是true.
- 先尝试从RecycleBin保存的TransientStateView取View(position作为参数),
- 如果没有取到, 那么从RecycleBin的scrapView中取一个(前面解析过这个函数,会尽量返回一个以前也在同样position的View,否则就返回最后一个)
- 如果前面的步骤取到了View,那么会将此View作为convertView参数调用Adapter的getView函数得到一个View child, 这里发现child竟然没有做null检测…, 看样子getView不能返回null了,如果发现child并不是之前传入的convertView,那么说明scrapView没有被重用, 将其放回到RecycleBin的回收池中,顺带设置new View的mCacheColorHint.
- 如果是重用了convertView,那么isScrap[0]= true. 并且将此View上的所有系统管理的暂态全部clear. 调用child的dispatchFinishTemporaryDetach().
- 如果根本没有可用的convertView,那么会直接传递null调用adapter的getVIew(…).
dispatchDraw(Canvas canvas): 如果开启了CLIP_TO_PADDING_MASK,那么就会将canvas进行clipRect, 会在之前save一下canvas得到一个svaeCount,因为最后需要将canvas restore, 这样不会绘制padding区域内的内容了. 还会根据mDrawSelectorOnTop来选择是在super.dispatchDraw()(绘制其他内容)之前还是之后draw selector. 最后会将前面被clipRect的Canvas回复(canvas.restoreToCount(saveCount)), drawSelector(Canvas canvas)会将selector的drawable setBound(mSelectorRect)来指定selector的被绘制区域.
touchModeDrawsInPressedState(): 返回是否需要在pressed的状态下绘制selector. 被shouldShowSelector()所使用,
setSelector: 就是指定一个drawable作为selector, 原来的selector drawable的Callback以及events都会被clear(mSelector.setCallback(null);unscheduleDrawable(mSelector);), 新的Drawable会设置padding,Callback(AbsListView), 最后update selector的state(updateSelectorState()). updateSelectorState的逻辑:
- 如果应该展示selector, 那么selcector的state会设置为和AbsListView一样的(getDrawableState())
- 否则设置为StateSet.NOTHING.
- 为了和AbsListView保持state同步,在AbslIstView的drawableStateChanged中,也会调用此函数.
pointToPosition(int x, int y)/pointToRowId(int x, int y): 前者获取当前坐标属于哪个postion的Item,后者在前者的基础上进一步去的Item的id. pointToPosition的实现有参考价值,挨个遍历当前child的getHitRect(frame),然后检测x/y是否在返回的frame rect中, 如果是,结合mFirstPosition返回postition,如果不属于任何一个item, 返回INVALID_POSITION.
想大多数继承了ViewGroup的类一样, AbsListView定制了自己的LayoutParams, LayoutParams extends ViewGroup.LayoutParams, 增加新的属性来支持自己的特性:
- viewType
- recycledHeaderFooter: 用来得知header/footer是否已经加入到了AbsListView中以及是否可以被recycle.
- forceAdd: 当一AbsListView被用AT_MOST来measure的时候,它需要获得child View的信息来对自己进行measure, 这样做的时候,child view没有被attach到window上,但是却已经被放进了RecycleBin并假设他们曾经attach过, 设置了这个flag,会迫使被重用的view被attach到Window而不仅仅是attach到他们的parent上.
- scrappedFromPosition: 之前说过,ScrapView在被回收remove前对应的position.
- 因为定制了自己的LayoutParams,那么generateLayoutParams(…)这一系列方法也会override来返回一个AbsListView.LayoutParams.
reclaimViews(List views): 将当前absListView所hold的view(除了header&footer)fill到views中, 包括了当前所展示的View以及RecycleBin中的View, 在这个过程中,会回调onMovedToScrapHeap(childView), RecycleBin的reclaimScrapViews也会被调用, 最后一步是removeAllViewsInLayout()(不会触发重新layout).
自定义的AdapterDataSetObserver进一步扩展了AdapterView.AdapterDataSetObserver:
- onChanged()会额外调用mFastScroller的onSectionsChanged()
- onInvalidated()会额外调用mFastScroller的onSectionsChanged()
onInterceptTouchEvent(MotionEvent ev): Touch三重门的第二关(dispatchTouchEvent没有被override):
- 首先任何的Touch都会导致mPositionScroller的stop.
- 然后check是否 isAttachedToWindow(), 没有attach直接return false放弃这一轮的Touch事务(这种情况是咋发生的?)
- 再将TouchEvent交由FastScroller的onInterceptTouchEvent处理,如果被处理了就返回(职责链模式)
- 然后按照TouchEvent的Action分类处理:
- ACTION_DOWN:
- 如果现在touchMode正处于TOUCH_MODE_OVERFLING/TOUCH_MODE_OVERSCROLL,那么此时的down会将mMotionCorrection置为0, 并返回true来将这一轮的touch事务直接交给AbsListView处理(记住,这里是onInterceptTouchEvent),
- 否则,会根据down的y坐标结合findMotionRow来得到离y最近的row的position, 如果此时不是TOUCH_MODE_FLING并且是有效的pos, 那么会获取被down到的View(用来得到mMotionViewOriginalTop), 并且TouchMode会变为**TOUCH_MODE_DOWN, mMotionX/Y会设置为这次motionEvent的坐标,motionPosition 也会设置为down到的position.
**. - 还会将motionEvent加入到VelocityTracker中来计算速度.
- ACTION_MOVE:
- 只有在TOUCH_MODE_DOWN下处理,会调用startScrollIfNeeded,如果确实可以scroll, 那么直接返回true拦截并吞掉这一轮Touch事务.
- ACTION_CANCEL/UP: 两者某种意义上是可以相同逻辑处理的, TouchMode会变为TOUCH_MODE_REST, 对VelocityTracker进行recycle(), 并且将当前scroll的state变为SCROLL_STATE_IDLE以及通知onScrollListener
- ACTION_DOWN:
startScrollIfNeeded(int y): 尝试进行scroll,如果真的成功scroll了,会返回true
- 先根据mMotionY来得到移动的distance, 并和mTouchSlop这个阈值比较
- 如果超过了阈值,那么说明可以scroll,TouchMode会变为TOUCH_MODE_SCROLL,并且mMotionCorrection会更新为mTouchSlop/-mTouchSlop,
- 既然开始了scroll,那么check是否是longPress就没有意义了,会将之前post的mPendingCheckForLongPress remove,
- 根据mMotionPosition和mFirstPosition得到一开始被down到的childView, 将其press state取消.
- 通告当前的scroll state变为了SCROLL_STATE_TOUCH_SCROLL(reportScrollStateChange): 从reportScrollStateChange的逻辑可以看到,只有在state变化时,才会回调onScrollListener.
- 为了确保scroll后这一轮的touch event**不会被parent吞掉,会调用parent的requestDisallowInterceptTouchEvent(true)**
- 最后调用scrollIfNeeded(y), 返回true.
- 只有在既没有overScroll, 并且move距离没超过TouchSlop的情况下,才会返回false.
scrollIfNeeded(int y):
- 首先根据 y, mMotionY, mMotionCorrection得到一个delatY表示这一次y坐标上的变化.
- 如果当前touchMode是TOUCH_MODE_SCROLL: ,最关键的移动函数trackMotionScroll(deltaY, incrementalDeltaY)在这里被调用了, 还有对OverScroll的检测和执行(overScrollBy()). onTouchEvent中对ACTION_MOVE的处理最后也会跑到这里.
- TOUCH_MODE_OVERSCROLL:先不管.
onTouchEvent(MotionEvent ev): Touch三重门最后一关,
- 如果AbsListView被disable,但是可以被click或者longClick,那么照样会吞下这一轮Touch事务,只是不做任何处理.
- mPositionScroller stop(正在自动scroll的话,要停掉)
- 还是交给mFastScroller的onTouchEvent处理,如果被处理,return true.
- 将这次的MotionEvent加入到VelocityTracker中.
然后根据Action:
ACTION_DOWN->onTouchDown(ev):
- 如果TouchMode是TOUCH_MODE_OVERFLING, 那么停止fling,状态变为TOUCH_MODE_OVERSCROLL, 更新mMotionX/mMotionY/mLastY/mMotionCorrection/mDirection
- 否则: 利用pointToPosition获得down到的View对应的Item的pos,如果Data没有change:
- 如果TouchMode是TOUCH_MODE_FLING,会变为TOUCH_MODE_SCROLL.
*否则,如果是有效的pos,并且对应的Item也是enable的(Adpater.isEnabled), 那么TouchMode变为TOUCH_MODE_DOWN, 会延迟post一个CheckForTap来检测是否是一次Tap.
- 如果TouchMode是TOUCH_MODE_FLING,会变为TOUCH_MODE_SCROLL.
更新mMotionX/mMotionY/mLastY.
ACTION_MOVE->onTouchMove(ev):
- 如果data变化了,那么会重新layoutChildren()(因为Scroll操作会query Adapter)
- 得到当前move到的y坐标,然后对于TouchMode是TOUCH_MODE_DOWN/TOUCH_MODE_TAP/TOUCH_MODE_DONE_WAITING的情况:
- 还是会检测TouchSlop(startScrollIfNeeded(y)), 后面跟着检测一次当前Move的位置(pointInView(x, y, mTouchSlop))是否还在AbsListView范围中, 如果已经不在了, 取消pressed state,TouchMode会变为TOUCH_MODE_DONE_WAITING, 还会更新Selector的state.
- 如果TouchMode是TOUCH_MODE_SCROLL/TOUCH_MODE_OVERSCROLL:直接调用scrollIfNeeded(y).
ACTION_UP->onTouchUp(ev):
针对当前的touchMode区分处理:- TOUCH_MODE_DOWN/TOUCH_MODE_TAP/TOUCH_MODE_DONE_WAITING: 就总结一下大概的处理流程是延迟pos一个Runnable(mTouchModeReset), delay = ViewConfiguration.getPressedStateDuration()(延迟post是为了检测这是一次有效的点击,有可能UP以后的很短时间又发生了其他的Action,那么就不认为是一次有效的点击), 只有在这个Runnable成功运行了,才会调用一个AbsListView.PerformClick进行click的逻辑调用(如果中间发生了什么ACTION,这个Runnbale就会被remove), 并且会将state设为TOUCH_MODE_REST标志这一轮Touch事务的结束
- TOUCH_MODE_SCROLL:在scroll的过程中松手,会有两种case:
- 如果scroll的速度没有突破mMinimumVelocity(速度通过velocityTracker计算), 那么就停止,TouchMode变为TOUCH_MODE_REST, scroll的state变为SCROLL_STATE_IDLE并通知出去.
- 如果突破了阈值,那么会启动一个mFlingRunnable.start(-initialVelocity), scroll state变为SCROLL_STATE_FLING并通知出去.
ACTION_CANCEL->onTouchCancel():
一般会发生在OVER_SCROLL/FLIG的情况下: 没什么大操作. 其他的touchMode下, 会直接将TouchMode设置为TOUCH_MODE_REST,视为本次Touch事务结束.
trackMotionScroll, 最关键的位移实现函数(int deltaY, int incrementalDeltaY):
- 如果没有childView 直接return true(return true 代表已经在list的begin/end了,没啥可干的)
- 先解释deltaY和incrementalDeltaY的意义:
- deltaY: 从本次Touch事务开始至今的Y轴上的offset,正数代表着使用者在向下滑动
- incrementalDeltaY: 本次Touch event在Y轴上导致的offset.和上一次Touch Event的Y做对比, 因此这里叫做incremental
- firstTop = getChildAt(0).getTop(), 当前第一个显示的ItemView的top坐标
- lastBottom = getChildAt(childCount - 1).getBottom(),当前最后一个显示的View的bottom坐标.
- 如果指定了CLIP_TO_PADDING_MASK, 那么还会增加两个值effectivePaddingTop和effectivePaddingBottom代表Top和Bottom两个位置的padding.
- 进一步的得到:
- spaceAbove = effectivePaddingTop - firstTop: 代表当前第一个可视ItemView的上沿与top位置最上方可见坐标Y之间的空挡.
- end = getHeight() - effectivePaddingBottom: 代表在AbsListView内Bottom最下可见的Y位置.
- spaceBelow = lastBottom - end: 显然是最后一个可见的ItemView的下沿与bottom位置最下方可见位置Y之间的空挡. 其实解释一通反而复杂了…., 本身名字起得已经很直白了.
- height = getHeight() - mPaddingBottom - mPaddingTop: AbsListView可见的区域的heigt(刨掉了padding这种不可见区域).
- 得到了height,那么就确定了一个deltaY的极值:即abs(deltaY) < height.(可以理解,一次Touch事务中,最多移动的距离就是height这么多)
- 同样的,incrementalDeltaY也会做一次极值约束.
下一步就是更新mFirstPositionDistanceGuess/mLastPositionDistanceGuess:
- mFirstPositionDistanceGuess: 对Data set中第一个ItemView(注意不是第一个可见的)的Top和List View Top(要考虑padding)之间的距离的估计值, 用来draw edge effect. 那么,如果当前第一个可见ItemView对应在Data set的pos就是 0, 第一个, 那么该值显然就是firstTop - listPadding.top, 否则就在原来的基础上加上这次的变化量: += incrementalDeltaY
- mLastPositionDistanceGuess: 同理是list view的bottom和data set对应的最后一个ItemView 之间的距离估计值. 赋值也基本同上.
- 注意一点, 这两个变量的观察视角是认为所有的Item View都在ListView中, 滑动意味着发生了位移,但是现实情况显然不会让所有ItemView都填充到ListView中
下一步是check两个condition: cannotScrollDown/cannotScrollUp:
- cannotScrollDown:意思是现在这次在向下scroll,但是其实scroll不动了
- cannotScrollUp: 同上,在向上scroll,但是scroll不动了.
- 这两个变量不单单表达当前是否可以向上/下scroll, 还包含了 在向上/下scroll,但是scroll不动了
- 因此紧接着一个判断,只要cannotScrollDown||cannotScrollUp,就直接return incrementalDeltaY != 0.
- 继续向下: boolean down = incrementalDeltaY < 0.注意这个down不是指使用者在向下拖动,而是指使用者向“上“拖,list view在向“下”移动
- 如果inTouchMode,把selector隐藏掉.
- 得到有几个headerView: headerViewsCount = getHeaderViewsCount().
- 得到footerView的start pos: footerViewsStart = mItemCount - getFooterViewsCount()
- 根据down进行操作:
- 如果是user向上滑,list向下走(down = true): 那么先做一个界限: top = -incrementalDeltaY + padding(如果考虑padding的话), 所有bottom比这个top小的View在这次滑动中都应该不可见了, 因此会将这些view回收到RecycleBin中(header&footer不会被回收, 调用的是RecycleBin的addScrapView(child, position)),直到遇到一个还应该显示的View,才会break这个对childViews的遍历.
- 反向情况同理.
- 相应的mMotionViewNewTop也会更新为: mMotionViewOriginalTop + deltaY
- mBlockLayoutRequests = true, 将childVIew的requestLayout全部屏蔽掉.
- 还需要将之前应该不显示的View从AbsListView中移除: detachViewsFromParent(start, count); mRecycler.removeSkippedScrap();
detachViewsFromParent(int start, int count)是ViewGroup定义的函数:
Detaches a range of views from their parents. Detaching a view should be followed either by a call to {@link #attachViewToParent(View, int, android.view.ViewGroup.LayoutParams)} or a call to {@link #removeDetachedView(View, boolean)}. Detachment should only be temporary; reattachment or removal should happen within the same drawing cycle as detachment. When a view is detached, its parent is null and cannot be retrieved by a call to {@link #getChildAt(int)}.
removeSkippedScrap()则是将RecycleBin中的mSkippedScrap中的View**remove(比Detach更进一步)** - 后面还会跟进一次invalidate(), 这样就避免在在后面添加ChildView时频繁触发ivalidate.
- offsetChildrenTopAndBottom(incrementalDeltaY), 终于可以看到位移实现的函数了, 该方法定义于ViewGroup, 作用就是将自己内部的ChildView全部垂直位移一定offset(ItemView的移动就是这么实现的)
- offsetChildrenTopAndBottom的内部原理就是调用了ChildView的mDisplayList的offsetTopAndBottom.
- 既然发生了位移, 那么就要更新mFirstPosition(注意,这只有在down,List展现了下方的内容时才会在这里更新mFirstPosition: += count,原因是因为不是down的时候, 第一个展示的ItemView还没有被填充进去,就无从判断mFirstPosition).
- 在经过上面的操作以后,ListView**可能就会出现空档(因为offsetChildrenTopAndBottom改变了ChildView的位置),这里会有一个判断,如果腾出的上/下方空档(spaceAbove/spaceBelow)超过了abs(incrementalDeltaY), 那么可能就需要一次View的填充: fillGap(down), 这个函数是abstract的, 由子类实现**:
Fills the gap left open by a touch-scroll. During a touch scroll, children that remain on screen are shifted and the other ones are discarded. The role of this method is to fill the gap thus created by performing a partial layout in the empty space. - 下一步是更新Selctor的位置.
- 最后在ChildView全部被部署完以后,会将mBlockLayoutRequests设为false.
- invokeOnItemScrollListener出发scrollListener的callback.
- return false. 代表真的scroll了.
Android Adapter机制 源码笔记(7): AbsListView(3)
最新推荐文章于 2023-02-24 14:23:56 发布
本文详细探讨了AbsListView的setAdapter方法,指出其并未替换Adapter,而是清空检查状态。同时,分析了setOnScrollListener的工作原理,包括如何触发OnScrollListener的回调以及滚动事件的处理。文章还讲解了SmoothScrollbar的概念,讨论了AbsListView在不同滚动条特性的处理方式。此外,还介绍了AbsListView中的滚动边界判断、布局测量和触摸事件处理,揭示了AbsListView在处理触摸事件时的逻辑和状态转换。

570

被折叠的 条评论
为什么被折叠?



