Android Adapter机制 源码笔记(7): AbsListView(3)

  1. AbsListView的setAdapter函数其实没有替换和保存新的Adapter, 也没有触发UI重绘等,只是简单的将mCheckStates/mCheckedIdStates清空, 是重点子类override函数, AbslListView中关于Item**check和select的处理逻辑还是很多的,但是这一块逻辑反而平时开发基本不会涉及(很少有需求对这些功能进行高度定制的)**

  2. setOnScrollListener(OnScrollListener l): 除了将给的listener设置以外,还会触发一次invokeOnItemScrollListener(), 后者会触发mFastScroller/mOnScrollListener的onScroll函数: 并且还会调用一次onScrollChanged(0, 0, 0, 0)(dummy values, View’s implementation does not use these)

  3. 解释一下SmoothScrollbar这个概念:

    • 如果开启了这个特性:那么scrollBar的thumb(滑块)的位置和尺寸都是基于当前可视的ItemView的像素值(pixel), 这个特性假设所有的itemView都有相同的height, 如果不能满足这个情况,会导致scrollBar在滑动时发生不应该的形变(一会儿长一会儿短), 这种情况应该关闭这个feature.
    • 如果这个特性被关掉,那么thumb的位置和尺寸都仅仅取决于当前爱Adapter中item的总数和当前可见的itemView在data set中的pos, 这样就不会因为itemView的不同height造成活动中的尺寸畸变.
  4. int computeVerticalScrollExtent(): 继承自View, 计算scroll时显示的滑块(thumb)的垂直尺寸:
    * 在没有childView时, 为 0.
    * 如果有childView,但是没有开启mSmoothScrollbarEnabled, 那么就是 1.
    * 如果有childView并且开启了mSmoothScrollbarEnabled, 会根据child的count/height以及View的bottom等信息综得到一个合适的值.

  5. int computeVerticalScrollOffset(): 同样继承自View, 计算的是ScrollBar的thumb的在滑道(track)中的垂直偏移,其实就是thumb的垂直坐标.

    • 如果没有childVIew或者mFirstPosition是无效pos,那么返回0.
    • 否则:
      • 如果mSmoothScrollbarEnabled: 像之前说的一样,基于Item的pixel级尺寸进行计算.
      • 如果没有mSmoothScrollbarEnabled: 像之前说的一样,基于Item的总数和当前可视的item在data set中的pos决定.
  6. int computeVerticalScrollRange(): 继承自View, 这个是计算滑道的长度. 同样的会有对SmoothScrollbarEnabled的分别处理. (这三个计算滑道与滑块的函数返回的值应该基于同一标尺)

  7. getTopFadingEdgeStrength/getBottomFadingEdgeStrength: 继承自View, Returns the strength, or intensity, of the top faded edge.

  8. onMeasure(int widthMeasureSpec, int heightMeasureSpec): 感觉其实没干啥实质性的工作…

  9. 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(空函数,交由子类实现).

  10. boolean setFrame(int left, int top, int right, int bottom): 继承自View,这个函数是在layout的时候被调用, 最终制定View的尺寸和位置,是一个非常重要的函数. 这里只是顺带在super.setFrame(…)返回表示change时会重新设一下mPopup的位置.

  11. updateScrollIndicators(): 根据当前情况设置mScrollUp/mScrollDown(如果有的话)的可见性,代表这可以向上/下scroll. 其判断逻辑还是挺简单的(不过很有参考价值):

    • 如果第一个/最后一个Item当前没有显示(使用mFirstPosition和childCount判断),那么显然是可以向上/下scroll的.
    • 还有就是第一个/最后一个Item的上沿/下沿在AbsListView的mTop/mBottom的上/下(就是纯坐标判断了,还会考虑到padding, 如果Item的top小于padding,那么说明上沿没有显示, 同样,如果Item的Bottom已经超过了parentView的高度-padding,那么说明下沿没有显示),那么可以认为还是可以向上/下scroll的.
  12. 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(…).
  13. 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的被绘制区域.

  14. touchModeDrawsInPressedState(): 返回是否需要在pressed的状态下绘制selector. 被shouldShowSelector()所使用,

  15. 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中,也会调用此函数.
  16. 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.

  17. 想大多数继承了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.
  18. reclaimViews(List views): 将当前absListView所hold的view(除了header&footer)fill到views中, 包括了当前所展示的View以及RecycleBin中的View, 在这个过程中,会回调onMovedToScrapHeap(childView), RecycleBin的reclaimScrapViews也会被调用, 最后一步是removeAllViewsInLayout()(不会触发重新layout).

  19. 自定义的AdapterDataSetObserver进一步扩展了AdapterView.AdapterDataSetObserver:

    • onChanged()会额外调用mFastScroller的onSectionsChanged()
    • onInvalidated()会额外调用mFastScroller的onSectionsChanged()
  20. 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
  21. 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.
  22. scrollIfNeeded(int y):

    • 首先根据 y, mMotionY, mMotionCorrection得到一个delatY表示这一次y坐标上的变化.
    • 如果当前touchMode是TOUCH_MODE_SCROLL: ,最关键的移动函数trackMotionScroll(deltaY, incrementalDeltaY)在这里被调用了, 还有对OverScroll的检测和执行(overScrollBy()). onTouchEvent中对ACTION_MOVE的处理最后也会跑到这里.
    • TOUCH_MODE_OVERSCROLL:先不管.
  23. 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.

        更新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事务结束.

  24. trackMotionScroll, 最关键的位移实现函数(int deltaY, int incrementalDeltaY):

    • 如果没有childView 直接return true(return true 代表已经在list的begin/end了,没啥可干的)
    • 先解释deltaYincrementalDeltaY的意义:
      • 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中
  25. 下一步是check两个condition: cannotScrollDown/cannotScrollUp:

    • cannotScrollDown:意思是现在这次在向下scroll,但是其实scroll不动了
    • cannotScrollUp: 同上,在向上scroll,但是scroll不动了.
    • 这两个变量不单单表达当前是否可以向上/下scroll, 还包含了 在向上/下scroll,但是scroll不动了
    • 因此紧接着一个判断,只要cannotScrollDown||cannotScrollUp,就直接return incrementalDeltaY != 0.
  26. 继续向下: boolean down = incrementalDeltaY < 0.注意这个down不是指使用者在向下拖动,而是指使用者向“上“拖,list view在向“下”移动
  27. 如果inTouchMode,把selector隐藏掉.
  28. 得到有几个headerView: headerViewsCount = getHeaderViewsCount().
  29. 得到footerView的start pos: footerViewsStart = mItemCount - getFooterViewsCount()
  30. 根据down进行操作:
    • 如果是user向上滑,list向下走(down = true): 那么先做一个界限: top = -incrementalDeltaY + padding(如果考虑padding的话), 所有bottom比这个top小的View在这次滑动中都应该不可见了, 因此会将这些view回收到RecycleBin中(header&footer不会被回收, 调用的是RecycleBin的addScrapView(child, position)),直到遇到一个还应该显示的View,才会break这个对childViews的遍历.
    • 反向情况同理.
  31. 相应的mMotionViewNewTop也会更新为: mMotionViewOriginalTop + deltaY
  32. mBlockLayoutRequests = true, 将childVIew的requestLayout全部屏蔽掉.
  33. 还需要将之前应该不显示的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更进一步)**

  34. 后面还会跟进一次invalidate(), 这样就避免在在后面添加ChildView时频繁触发ivalidate.
  35. offsetChildrenTopAndBottom(incrementalDeltaY), 终于可以看到位移实现的函数了, 该方法定义于ViewGroup, 作用就是将自己内部的ChildView全部垂直位移一定offset(ItemView的移动就是这么实现的)
    • offsetChildrenTopAndBottom的内部原理就是调用了ChildView的mDisplayList的offsetTopAndBottom.
  36. 既然发生了位移, 那么就要更新mFirstPosition(注意,这只有在down,List展现了下方的内容时才会在这里更新mFirstPosition: += count,原因是因为不是down的时候, 第一个展示的ItemView还没有被填充进去,就无从判断mFirstPosition).
  37. 在经过上面的操作以后,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.
  38. 下一步是更新Selctor的位置.
  39. 最后在ChildView全部被部署完以后,会将mBlockLayoutRequests设为false.
  40. invokeOnItemScrollListener出发scrollListener的callback.
  41. return false. 代表真的scroll了.

你可能感兴趣的:(android)