HorizontalListView源码解读

HorizontalListView源码解读

[TOC]

前言

​ 随着RecycleView进入人们的视野,其强大的功能让人们逐渐忘记了曾经的王者”AdapterView”,但这绝不影响它对我们的吸引力,要知道当年流行的Gallery,Spinner,ListView,GridView等等都是它的孩子。

​ 那么,假如今天我们要在它的孩子中增加一个横向滚动控件,我们应该怎么实现呢?HorizontalListView给出了我们想要的答案。

把ListView横过来

需要什么类辅助

  • Scroller

    一个平滑滚动的辅助工具,可以随时获取当前横向滚动距离,这里使用为了跟踪fling事件。

    • 调用关系

      onLayout -> computerScrollOffset //获得新的滚动位置x,y。

      onFling -> fling //根据手势和速度,计算滚动距离。

  • GestureListener

    一个用来跟踪触摸,点击,长按事件的监听器。

    [onDown,onFling,onScrol,onSingleTapConfirm,onLongPress]

  • ListAdapter

    一个非常常见的可用于填充数据和视图的适配器。

  • EdgeEffectCompat

    一个处理两边边界动画效果的工具,在合适的时机传入距离,速度等参数进行渲染。

    [onAbsorb,onPull,onRelease]

    • 调用关系

      onScroll -> updateOverscrollAnimation -> onPull

      dispatchDraw -> drawEdgeGlow ->[setSize,draw]

      onLayout -> onAbsorb

      onTouchEvent -> releaseEdgeGlow -> onRelease

布局处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
* 1.判断是否需要重置数据
* 2.恢复之前的滚动状态
* 3.滚动状态的合法性校验
* 4.获得deltx之后处理滚动后布局问题
* 4.1 remove 滚动后的最左边子视图右边距小于0,最右边的子视图左边距大于父宽度
* 4.2 add 向右边,向左边添加滚动后可见的新的子视图
* 4.3 根据dx重新layout这些子视图
*
* @param changed
* @param left
* @param top
* @param right
* @param bottom
*/
@SuppressLint("WrongCall")
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);

if (mAdapter == null) {
return;
}

// Force the OS to redraw this view
invalidate();

// If the data changed then reset everything and render from scratch at the same offset as last time
if (mDataChanged) {
int oldCurrentX = mCurrentX;
initView();
removeAllViewsInLayout();
mNextX = oldCurrentX;
mDataChanged = false;
}

// If restoring from a rotation
if (mRestoreX != null) {
mNextX = mRestoreX;
mRestoreX = null;
}

// If in a fling
if (mFlingTracker.computeScrollOffset()) {
// Compute the next position
mNextX = mFlingTracker.getCurrX();
}

// Prevent scrolling past 0 so you can't scroll past the end of the list to the left
if (mNextX < 0) {
mNextX = 0;

// Show an edge effect absorbing the current velocity
if (mEdgeGlowLeft.isFinished()) {
mEdgeGlowLeft.onAbsorb((int) determineFlingAbsorbVelocity());
}

mFlingTracker.forceFinished(true);
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
} else if (mNextX > mMaxX) {
// Clip the maximum scroll position at mMaxX so you can't scroll past the end of the list to the right
mNextX = mMaxX;

// Show an edge effect absorbing the current velocity
if (mEdgeGlowRight.isFinished()) {
mEdgeGlowRight.onAbsorb((int) determineFlingAbsorbVelocity());
}

mFlingTracker.forceFinished(true);
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
}

// Calculate our delta from the last time the view was drawn
int dx = mCurrentX - mNextX;
removeNonVisibleChildren(dx);
fillList(dx);
positionChildren(dx);

// Since the view has now been drawn, update our current position
mCurrentX = mNextX;

// If we have scrolled enough to lay out all views, then determine the maximum scroll position now
if (determineMaxX()) {
// Redo the layout pass since we now know the maximum scroll position
onLayout(changed, left, top, right, bottom);
return;
}

// If the fling has finished
if (mFlingTracker.isFinished()) {
// If the fling just ended
if (mCurrentScrollState == OnScrollStateChangedListener.ScrollState.SCROLL_STATE_FLING) {
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
}
} else {
// Still in a fling so schedule the next frame
ViewCompat.postOnAnimation(this, mDelayedLayout);
}
}

手势处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
 private class GestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return HorizontalListView.this.onDown(e);
}

/**
* 跟踪fling事件入口
* @param e1
* @param e2
* @param velocityX
* @param velocityY
* @return
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return HorizontalListView.this.onFling(e1, e2, velocityX, velocityY);
}

/**
* 更新滚动状态计算获得新的nextX
* @param e1
* @param e2
* @param distanceX
* @param distanceY
* @return
*/
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// Lock the user into interacting just with this view
requestParentListViewToNotInterceptTouchEvents(true);

setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_TOUCH_SCROLL);
unpressTouchedChild();
mNextX += (int) distanceX;
updateOverscrollAnimation(Math.round(distanceX));
requestLayout();

return true;
}

/**
* 根据 MotionEvent找到真正的childIndex,获得child后传递click事件
* @param e
* @return
*/
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
unpressTouchedChild();
OnItemClickListener onItemClickListener = getOnItemClickListener();

final int index = getChildIndex((int) e.getX(), (int) e.getY());

// If the tap is inside one of the child views, and we are not blocking touches
if (index >= 0 && !mBlockTouchAction) {
View child = getChildAt(index);
int adapterIndex = mLeftViewAdapterIndex + index;

if (onItemClickListener != null) {
onItemClickListener.onItemClick(HorizontalListView.this, child, adapterIndex, mAdapter.getItemId(adapterIndex));
return true;
}
}

if (mOnClickListener != null && !mBlockTouchAction) {
mOnClickListener.onClick(HorizontalListView.this);
}

return false;
}

/**
* 根据 MotionEvent找到真正的childIndex,获得child后传递LongClick事件
* @param e
* @return
*/
@Override
public void onLongPress(MotionEvent e) {
unpressTouchedChild();

final int index = getChildIndex((int) e.getX(), (int) e.getY());
if (index >= 0 && !mBlockTouchAction) {
View child = getChildAt(index);
OnItemLongClickListener onItemLongClickListener = getOnItemLongClickListener();
if (onItemLongClickListener != null) {
int adapterIndex = mLeftViewAdapterIndex + index;
boolean handled = onItemLongClickListener.onItemLongClick(HorizontalListView.this, child, adapterIndex, mAdapter
.getItemId(adapterIndex));

if (handled) {
// BZZZTT!!1!
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
}
}
}
};

其他

复用机制

1
2
//每一个Adapter的type对应一个queue,把同一个type的view插入到同一个queue中,然后通过queue的offer/poll接口进行复用
private List<Queue<View>> mRemovedViewsCache = new ArrayList<Queue<View>>();
  • 调用关系

    setAdapter -> initializeRecycledViewCache(typeCount) -> [clear,add]
    [fillListLeft,fillRight] -> getRecycledView -> [poll]
    removeNonVisibleChildren -> recycleView -> [offer]