ListView源码解读

ListView的测量

onMeasure

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
/**
* onMeasure four step
*
* 1. getWidthMode and getWidthSize etc.
* 2. widthSize if(unspcified)
* use (position of 0' child width + paddingRight + paddingLeft + scrollBarWidth)
* else
* modify measureState by childSate
*
* 3.heightSize if(unspecified)
* use(0'child height +paddingBottom+paddingTop+verticalScrollPadding)
* else if(At_Most)
* use the givend heightSize as max and compared with the inclusive size of children,
* also should add paddings
*
* 4.setMeasuredDimension()
*
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding by view's Padding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

int childWidth = 0;
int childHeight = 0;
int childState = 0;

mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap);

// Lay out child directly against the parent measure spec so that
// we can obtain exected minimum width and height.
measureScrapChild(child, 0, widthMeasureSpec, heightSize);

childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());

if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0);
}
}

if (widthMode == MeasureSpec.UNSPECIFIED) {
//use (position of 0' child width + paddingRight + paddingLeft + scrollBarWidth)
widthSize = mListPadding.left + mListPadding.right + childWidth +
getVerticalScrollbarWidth();
} else {
//modify measureState by childSate
widthSize |= (childState & MEASURED_STATE_MASK);
}

if (heightMode == MeasureSpec.UNSPECIFIED) {
//use(0'child height +paddingBottom+paddingTop+verticalScrollPadding)
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}

if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0

// use the givend heightSize as max and compared with the inclusive size of children,
// also should add paddings
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}

//often here heightMode AT_MOST or EXCACTLY
setMeasuredDimension(widthSize, heightSize);

mWidthMeasureSpec = widthMeasureSpec;
}

measureHeightofChildren

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
/**
* Measures the height of the given range of children (inclusive) and
* returns the height with this ListView's padding and divider heights
* included. If maxHeight is provided, the measuring will stop when the
* current height reaches maxHeight.
*
* @param widthMeasureSpec The width measure spec to be given to a child's
* {@link View#measure(int, int)}.
* @param startPosition The position of the first child to be shown.
* @param endPosition The (inclusive) position of the last child to be
* shown. Specify {@link #NO_POSITION} if the last child should be
* the last available child from the adapter.
* @param maxHeight The maximum height that will be returned (if all the
* children don't fit in this value, this value will be
* returned).
* @param disallowPartialChildPosition In general, whether the returned
* height should only contain entire children. This is more
* powerful--it is the first inclusive position at which partial
* children will not be allowed. Example: it looks nice to have
* at least 3 completely visible children, and in portrait this
* will most likely fit; but in landscape there could be times
* when even 2 children can not be completely shown, so a value
* of 2 (remember, inclusive) would be good (assuming
* startPosition is 0).
* @return The height of this ListView with the given children.
*/
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
int maxHeight, int disallowPartialChildPosition) {
final ListAdapter adapter = mAdapter;
if (adapter == null) {
return mListPadding.top + mListPadding.bottom;
}

// Include the padding of the list
int returnedHeight = mListPadding.top + mListPadding.bottom;
final int dividerHeight = ((mDividerHeight > 0) && mDivider != null) ? mDividerHeight : 0;
// The previous height value that was less than maxHeight and contained
// no partial children
int prevHeightWithoutPartialChild = 0;
int i;
View child;

// mItemCount - 1 since endPosition parameter is inclusive
endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
final AbsListView.RecycleBin recycleBin = mRecycler;
final boolean recyle = recycleOnMeasure();
final boolean[] isScrap = mIsScrap;

for (i = startPosition; i <= endPosition; ++i) {
//obtain view and add to the scrapList
child = obtainView(i, isScrap);
//measure the child
measureScrapChild(child, i, widthMeasureSpec, maxHeight);

if (i > 0) {
// Count the divider for all but one child
returnedHeight += dividerHeight;
}

// Recycle the view before we possibly return from the method
if (recyle && recycleBin.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
recycleBin.addScrapView(child, -1);
}

returnedHeight += child.getMeasuredHeight();

if (returnedHeight >= maxHeight) {
// We went over, figure out which height to return. If returnedHeight > maxHeight,
// then the i'th position did not fit completely.
return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
&& (i > disallowPartialChildPosition) // We've past the min pos
&& (prevHeightWithoutPartialChild > 0) // We have a prev height
&& (returnedHeight != maxHeight) // i'th child did not fit completely
? prevHeightWithoutPartialChild
: maxHeight;
}

if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
prevHeightWithoutPartialChild = returnedHeight;
}
}

// At this point, we went through the range of children, and they each
// completely fit, so return the returnedHeight
return returnedHeight;
}

ListView的生成

一般流程

1
2
3
4
5
6
7
8
abslistView:onLayout->
ListView:layoutChildren->
ListView:fillFromTop->
ListView:fillDown->
ListView:makeAndAddView->
mRecycler.getActiveView(position) or obtainView(position, mIsScrap)[has adapter 的 getview]
and then
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0])[has addViewInLayout ]

onLayout

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
/**
* Subclasses should NOT override this method but
* {@link #layoutChildren()} instead.
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);

mInLayout = true;

final int childCount = getChildCount();
if (changed) {
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}

//here:diff from ListView to GrideView
layoutChildren();
mInLayout = false;

mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;

// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
}

layoutChildren

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/**
* do with focus or aceessibility or other stuff style etc.
*
* 1.IMPORTANT ROUTE:
* ListView:fillFromTop->
* ListView:fillDown->
* ListView:makeAndAddView->
* mRecycler.getActiveView(position) or obtainView(position, mIsScrap)[has adapter 的 getview] and then setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0])
*
* // input startTop and startPos then to caculate
* while (List.top > List.end)
* {
* position +List.Top -> nextChild'Bottom ->nextChild'Bottom +divider height as next List.Top
* }
*
* // also in setupChild() to add the views into view trees
*
*
*/
@Override
protected void layoutChildren() {
final boolean blockLayoutRequests = mBlockLayoutRequests;
if (blockLayoutRequests) {
return;
}

mBlockLayoutRequests = true;

try {
super.layoutChildren();

invalidate();

if (mAdapter == null) {
resetList();
invokeOnItemScrollListener();
return;
}

final int childrenTop = mListPadding.top;
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
final int childCount = getChildCount();

int index = 0;
int delta = 0;

View sel;
View oldSel = null;
View oldFirst = null;
View newSel = null;

// Remember stuff we will need down below
switch (mLayoutMode) {
//...........
default:
// Remember the previously selected view
index = mSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
oldSel = getChildAt(index);
}

// Remember the previous first child
oldFirst = getChildAt(0);

if (mNextSelectedPosition >= 0) {
delta = mNextSelectedPosition - mSelectedPosition;
}

// Caution: newSel might be null
newSel = getChildAt(index + delta);
}


boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}

// Handle the empty set by removing all views that are visible
// and calling it a day
if (mItemCount == 0) {
resetList();
invokeOnItemScrollListener();
return;
} else if (mItemCount != mAdapter.getCount()) {
throw new IllegalStateException("The content of the adapter has changed but "
+ "ListView did not receive a notification. Make sure the content of "
+ "your adapter is not modified from a background thread, but only from "
+ "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
+ "when its content changes. [in ListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}

setSelectedPositionInt(mNextSelectedPosition);

//..................

// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}

// Clear out old views
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();

switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
if (newSel != null) {
sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
} else {
sel = fillFromMiddle(childrenTop, childrenBottom);
}
break;
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
break;
case LAYOUT_FORCE_BOTTOM:
sel = fillUp(mItemCount - 1, childrenBottom);
adjustViewsUpOrDown();
break;
case LAYOUT_FORCE_TOP:
mFirstPosition = 0;
//Here:to fill the list..
sel = fillFromTop(childrenTop);
//..................

// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();

//.................

// Tell focus view we are done mucking with it, if it is still in
// our view hierarchy.
//............

if (mItemCount > 0) {
checkSelectionChanged();
}

invokeOnItemScrollListener();
} finally {
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}

fillFromTop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Fills the list from top to bottom, starting with mFirstPosition
*
* @param nextTop The location where the top of the first item should be
* drawn
*
* @return The view that is currently selected
*/
private View fillFromTop(int nextTop) {
mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
if (mFirstPosition < 0) {
mFirstPosition = 0;
}
// the first nextTop is listpadding.top
return fillDown(mFirstPosition, nextTop);
}

fillDown

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
/**
* Fills the list from pos down to the end of the list view.
*
* @param pos The first position to put in the list
*
* @param nextTop The location where the top of the item associated with pos
* should be drawn
*
* @return The view that is currently selected, if it happens to be in the
* range that we draw.
*/
private View fillDown(int pos, int nextTop) {
View selectedView = null;

int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}

//the condition to break the while
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
// caculate the nextTop
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}

setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}

makeAndAddView

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
/**
* Obtain the view and add it to our list of children. The view can be made
* fresh, converted from an unused view, or used as is if it was in the
* recycle bin.
*
* @param position Logical position in the list
* @param y Top or bottom edge of the view to add
* @param flow If flow is true, align top edge to y. If false, align bottom
* edge to y.
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @return View that was added
*/
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;

if (!mDataChanged) {
// Try to use an existing view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);

return child;
}
}

// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);

// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

return child;
}

setupChild

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
/**
* Add a view as a child and make sure it is measured (if necessary) and
* positioned properly.
*
* @param child The view to add
* @param position The position of this child
* @param y The y position relative to which this view will be positioned
* @param flowDown If true, align top edge to y. If false, align bottom
* edge to y.
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @param recycled Has this view been pulled from the recycle bin? If so it
* does not need to be remeasured.
*/
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) {
//.............

if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
//HERE:
addViewInLayout(child, flowDown ? -1 : 0, p, true);
}

if (updateChildSelected) {
child.setSelected(isSelected);
}

if (updateChildPressed) {
child.setPressed(isPressed);
}

if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
if (child instanceof Checkable) {
((Checkable) child).setChecked(mCheckStates.get(position));
} else if (getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB) {
child.setActivated(mCheckStates.get(position));
}
}

// reMeasure..........
if (needToMeasure) {
final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
final int lpHeight = p.height;
final int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
} else {
cleanupLayoutState(child);
}

final int w = child.getMeasuredWidth();
final int h = child.getMeasuredHeight();
final int childTop = flowDown ? y : y - h;

if (needToMeasure) {
final int childRight = childrenLeft + w;
final int childBottom = childTop + h;
child.layout(childrenLeft, childTop, childRight, childBottom);
} else {
child.offsetLeftAndRight(childrenLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}
//........
}

addViewInLayout

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
   /**
* Adds a view during layout. This is useful if in your onLayout() method,
* you need to add more views (as does the list view for example).
*
* If index is negative, it means put it at the end of the list.
*
* @param child the view to add to the group
* @param index the index at which the child must be added or -1 to add last
* @param params the layout parameters to associate with the child
* @param preventRequestLayout if true, calling this method will not trigger a
* layout request on child
* @return true if the child was added, false otherwise
*/
protected boolean addViewInLayout(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
child.mParent = null;
addViewInner(child, index, params, preventRequestLayout);
child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
return true;
}


/**
*---------------Things to do ----------------
* addInArray
* assignPanrent
* requestFocus
* dispatchAttachInfo
**/
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {

//....
if (!checkLayoutParams(params)) {
params = generateLayoutParams(params);
}

if (preventRequestLayout) {
child.mLayoutParams = params;
} else {
child.setLayoutParams(params);
}

if (index < 0) {
index = mChildrenCount;
}

addInArray(child, index);

// tell our children
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this;
}

if (child.hasFocus()) {
requestChildFocus(child, child.findFocus());
}

AttachInfo ai = mAttachInfo;
if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
boolean lastKeepOn = ai.mKeepScreenOn;
ai.mKeepScreenOn = false;
child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
if (ai.mKeepScreenOn) {
needGlobalAttributesUpdate(true);
}
ai.mKeepScreenOn = lastKeepOn;
//.............
}

回收机制

RecycleBin

背景
1
2
3
4
       The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
start of a layout. By construction, they are displaying current information. At the end of layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
could potentially be used by the adapter to avoid allocating views unnecessarily.
关键参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* The position of the first view stored in mActiveViews.
*/
private int mFirstActivePosition;

/* Views that were on screen at the start of layout. This array is populated at the start of layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.Views in mActiveViews represent a contiguous range of Views, with position of the first view store in mFirstActivePosition.
*/
private View[] mActiveViews = new View[0];

/**
* Unsorted views that can be used by the adapter as a convert view.
* eg: ArrayList<View>[TYPE1]=......
* ArrayList<View>[TYPE2]=......
* ArrayList<View>[TYPE3]=......
*/
private ArrayList<View>[] mScrapViews;
关键方法
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



//usage:addToActiveViews
private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
final int size = scrapViews.size();
if (size > 0) {
// See if we still have a view for this position or ID.
for (int i = 0; i < size; i++) {
final View view = scrapViews.get(i);
final AbsListView.LayoutParams params =
(AbsListView.LayoutParams) view.getLayoutParams();

if (mAdapterHasStableIds) {
final long id = mAdapter.getItemId(position);
if (id == params.itemId) {
return scrapViews.remove(i);
}
} else if (params.scrappedFromPosition == position) {
final View scrap = scrapViews.remove(i);
clearAccessibilityFromScrap(scrap);
return scrap;
}
}
final View scrap = scrapViews.remove(size - 1);
clearAccessibilityFromScrap(scrap);
return scrap;
} else {
return null;
}
}





//usage:removeFromActiveViews
/**
* Puts a view into the list of scrap views.
* <p>
* If the list data hasn't changed or the adapter has stable IDs, views
* with transient state will be preserved for later retrieval.
*
* @param scrap The view to add
* @param position The view's position within its parent
*/
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
// Can't recycle, but we don't know anything about the view.
// Ignore it completely.
return;
}

lp.scrappedFromPosition = position;

// Remove but don't scrap header or footer views, or views that
// should otherwise not be recycled.
final int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {
// Can't recycle. If it's not a header or footer, which have
// special handling and should be ignored, then skip the scrap
// heap and we'll fully detach the view later.
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
getSkippedScrap().add(scrap);
}
return;
}

//................

// Don't scrap views that have transient state.
final boolean scrapHasTransientState = scrap.hasTransientState();
if (scrapHasTransientState) {
//...............
} else {
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}

if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}
recycle模型

ListView_RecycleBin

onTouchEvent事件的处理

一般流程

1
2
3
4
5
AbsListView:
onTouchEvent->
onTouchMove->
scrollIfNeeded->
trackMotionScroll

trackMotionScroll

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
/**
* Track a motion scroll
* 1.when the motion is moving,you should first addScrapView by inceamentDeltay
* 2.detachViewsFromParent ,this method to modify the viewTree by motion
* 3.fillGap
* if(incrementalDeltaY < 0){
* //as down
* fillDown(int pos, int nextTop)
* }else{
* //as up
* fillUp(int pos, int nextBottom)
* }
* // also here by use the method makeAndaddView,obtinView,setupView
* to reuse the view
*
* Here is the difference between deltaY and incrementalDeltaY.
* @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion
* began. Positive numbers mean the user's finger is moving down the screen.
* @param incrementalDeltaY Change in deltaY from the previous event.
* @return true if we're already at the beginning/end of the list and have nothing to do.
*/
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
//......

final int height = getHeight() - mPaddingBottom - mPaddingTop;
if (deltaY < 0) {
deltaY = Math.max(-(height - 1), deltaY);
} else {
deltaY = Math.min(height - 1, deltaY);
}

if (incrementalDeltaY < 0) {
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
} else {
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
}

//......

final boolean down = incrementalDeltaY < 0;

//......

if (down) {
//......
//how to addScrapView by motion down
mRecycler.addScrapView(child, position);

} else {
//......
//how to addScrapView by motion up
mRecycler.addScrapView(child, position);
}

//......

if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
//......

final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}

//......

return false;
}

fillGap

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
/**
* a interface method for the function with fillDown and fillUp
**/
@Override
void fillGap(boolean down) {
final int count = getChildCount();
if (down) {
int paddingTop = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingTop = getListPaddingTop();
}
final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
paddingTop;
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
} else {
int paddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingBottom = getListPaddingBottom();
}
final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
getHeight() - paddingBottom;
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
}
}

velocityTracker

背景

1
2
3
4
5
手势速度判断的工具类。
用obtain来获取velocityTracker对象;
用addMoveMent来添加需要观察的motionEvent;
用computerCurrentVelocity来计算速度;
用getXVelocity和getYVelocity来做相应的定制化行为处理事件

obtain

1
2
3
4
5
6
7
    private static final SynchronizedPool<VelocityTracker> sPool =
new SynchronizedPool<VelocityTracker>(2);//经常使用的耗资源对象
//......
static public VelocityTracker obtain() {
VelocityTracker instance = sPool.acquire();
return (instance != null) ? instance : new VelocityTracker(null);
}

computerCureentVelocity

1
2
3
4
5
6
7
8
9
/**
Compute the current velocity based on the points that have been collected. Only call this when you actually want to retrieve velocity information, as it is relatively expensive. You can then retrieve the velocity with getXVelocity() and getYVelocity().
Parameters:
units The units you would like the velocity in. A value of 1 provides pixels per millisecond, 1000 provides pixels per second, etc.
maxVelocity The maximum velocity that can be computed by this method. This value must be declared in the same unit as the units parameter. This value must be positive.
**/
public void computeCurrentVelocity(int units, float maxVelocity) {
nativeComputeCurrentVelocity(mPtr, units, maxVelocity);
}

GestureDetector

背景

1
2
3
MotionEevent分析工具类
用new GestureDetector(Context context, OnGestureListener listener)的listener传入需要分析的操作类型
用onTouchEvent(MotionEvent event)来分析具体的motion.eg onDoubleTap,onFling etc.

NestedScrolling

背景

1
2
痛点:如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下
解决:定义了一套子view的滑动和父view的交互机制

关于ListView的处理

1
2
3
4
5
6
7
8
9
10
View (rolePlayer as children)
stopNestedScroll
startNestedScroll
dispatchNestedPreScroll
dispatchNestedScroll

AbsListView(rolePlayer as parent)
onNestedScroll
onNestedScrollAccepted
onStartNestedScroll

google的再次抽象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NestedScrollingParent 父容器实现接口
NestedScrollingChild 子容器实现接口
NestedScrollingChildHelper 子容器帮助类,主要作用找到父容器并传递相应参数
NestedScrollingParentHelper 父容器帮助类,主要作用是接受与分发motion事件

一般流程:
子view的onTouchEvent()

Motion.Down:
————–Child startNestedScroll——————
—-Parent onStartNestedScroll—————-
—-Parent onNestedScrollAccepted—————

Motion.Move:
———–Child 把总的滚动距离传给父布局 dispatchNestedPreScroll——–
—-Parent onNestedPreScroll—————-
———–Child 把剩余的滚动距离传给父布局 dispatchNestedScroll——-
—-Parent onNestedScroll—————-

Motion.Up
———–Child 停止滚动 stopNestedScroll—————
—-Parent onStopNestedScroll—————-

Interpolator

背景

1
matlab里有一个插值的概念,而在手机app中有很多加速或者减速的运动,需要再相同的时间间隔内获取不同的间距

getInterpolation

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

//eg:DecelerateInterpolator
/**
* An interpolator where the rate of change starts out quickly and
* and then decelerates.
*
*/
/**
* Constructor
*
* @param factor Degree to which the animation should be eased. Setting factor to 1.0f produces
* an upside-down y=x^2 parabola. Increasing factor above 1.0f makes exaggerates the
* ease-out effect (i.e., it starts even faster and ends evens slower)
*/
public DecelerateInterpolator(float factor) {
mFactor = factor;
}

//......

//input 从0到1的匀速时间的百分比
//output 对应时间百分比下的value的百分比,有可能大于1或者小于0的情况.
//获得output值后根据自身的实际情况来更新视图
public float getInterpolation(float input) {
float result;
if (mFactor == 1.0f) {
result = (float)(1.0f - (1.0f - input) * (1.0f - input));
} else {
result = (float)(1.0f - Math.pow((1.0f - input), 2 * mFactor));
}
return result;
}

//......

ListView点击事件

为什么点击失效了

  • WHAT

    在Listview的Item里面有一个Button,那么这个item的点击事件往往不会被触发到

  • HOW

    在Item的根布局上增加,android:descendantFocusability=”blocksDescendants”

  • WHY

    1. Descendant

      焦点管理
      1
      2
      3
      requestFocus -> 
      requestFocusNoSearch ->
      handleFocusGainInternal -> (dispatchOnGlobalFocusChange,onFocusChanged,refreshDrawableState)
      requestFocusNoSearch
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
      // need to be focusable
      if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE ||
      (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
      return false;
      }

      // need to be focusable in touch mode if in touch mode
      if (isInTouchMode() &&
      (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
      return false;
      }

      //the xml tag works here
      // need to not have any parents blocking us
      if (hasAncestorThatBlocksDescendantFocus()) {
      return false;
      }

      handleFocusGainInternal(direction, previouslyFocusedRect);
      return true;
      }
      handleFocusGainInternal
      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
      /**
      *Three things to do
      * 1.onFocusChange
      * 2.dispatchFocusChange
      * 3.refreshDrawableState
      * Give this view focus. This will cause
      * {@link #onFocusChanged(boolean, int, android.graphics.Rect)} to be called.
      *
      * Note: this does not check whether this {@link View} should get focus, it just
      * gives it focus no matter what. It should only be called internally by framework
      * code that knows what it is doing, namely {@link #requestFocus(int, Rect)}.
      *
      * @param direction values are {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
      * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT}. This is the direction which
      * focus moved when requestFocus() is called. It may not always
      * apply, in which case use the default View.FOCUS_DOWN.
      * @param previouslyFocusedRect The rectangle of the view that had focus
      * prior in this View's coordinate system.
      */
      void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
      if (DBG) {
      System.out.println(this + " requestFocus()");
      }

      if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
      mPrivateFlags |= PFLAG_FOCUSED;

      View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

      if (mParent != null) {
      mParent.requestChildFocus(this, this);
      }

      if (mAttachInfo != null) {
      mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
      }

      onFocusChanged(true, direction, previouslyFocusedRect);
      refreshDrawableState();
      }
      }

    2. ItemOnClick事件

      处理流程
      1
      2
      3
      abslistView:onTouchEvent()
      ->onTouchUp()
      ->PerformClick.run()
      onTouchUp
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      private void onTouchUp(MotionEvent ev) {
      switch (mTouchMode) {
      case TOUCH_MODE_DOWN:
      case TOUCH_MODE_TAP:
      case TOUCH_MODE_DONE_WAITING:
      final int motionPosition = mMotionPosition;
      final View child = getChildAt(motionPosition - mFirstPosition);
      if (child != null) {
      if (mTouchMode != TOUCH_MODE_DOWN) {
      child.setPressed(false);
      }

      final float x = ev.getX();
      final boolean inList = x > mListPadding.left && x < getWidth() - mListPadding.right;
      // here to decide to invoke which child
      if (inList && !child.hasFocusable()) {
      //.......

      break;
      case TOUCH_MODE_SCROLL:
      //......
      }

扩展功能

阻尼效果

背景

1
ios 在系统层面上实现了触碰边缘的阻尼回弹效果,通知用户已经到达了边界。但是由于专利的问题,android在无法在系统层面上使用统一的效果,但是通过了一个渐变的颜色来告知用户已经到达了边界状态

How to Invoke

1
2
3
4
5
6
AbsListView:onTouchEvent->
AbsListView:onTouchMove->
AbsListView:startScrollIfNeeded->
AbsListView:scrollIfNeeded->//effect
View:overScrollBy->//action
View:onOverScrolled

overScrollBy

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
/**
* 1.this is a method help for caculate the distance when overScroll has happened
* 2.ListView,Webview,ScrollView,HorizontalScrollView has then same scource code
* 3.when to peresent when overScroll,this is decided by the overridde method onOverScrolled
* 4.ablistview input the valus maxOverScrollY = ViewConfiguration.mOverscrollDistance = 0;
*
* Scroll the view with standard behavior for scrolling beyond the normal
* content boundaries. Views that call this method should override
* {@link #onOverScrolled(int, int, boolean, boolean)} to respond to the
* results of an over-scroll operation.
*
* Views can use this method to handle any touch or fling-based scrolling.
*
* @param deltaX Change in X in pixels
* @param deltaY Change in Y in pixels
* @param scrollX Current X scroll value in pixels before applying deltaX
* @param scrollY Current Y scroll value in pixels before applying deltaY
* @param scrollRangeX Maximum content scroll range along the X axis
* @param scrollRangeY Maximum content scroll range along the Y axis
* @param maxOverScrollX Number of pixels to overscroll by in either direction
* along the X axis.
* @param maxOverScrollY Number of pixels to overscroll by in either direction
* along the Y axis.
* @param isTouchEvent true if this scroll operation is the result of a touch event.
* @return true if scrolling was clamped to an over-scroll boundary along either
* axis, false otherwise.
*/
@SuppressWarnings({"UnusedParameters"})
protected boolean overScrollBy(int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent) {
final int overScrollMode = mOverScrollMode;
final boolean canScrollHorizontal =
computeHorizontalScrollRange() > computeHorizontalScrollExtent();
final boolean canScrollVertical =
computeVerticalScrollRange() > computeVerticalScrollExtent();
final boolean overScrollHorizontal = overScrollMode == OVER_SCROLL_ALWAYS ||
(overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS ||
(overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);

int newScrollX = scrollX + deltaX;
if (!overScrollHorizontal) {
maxOverScrollX = 0;
}

int newScrollY = scrollY + deltaY;
if (!overScrollVertical) {
maxOverScrollY = 0;
}

// Clamp values if at the limits and record
final int left = -maxOverScrollX;
final int right = maxOverScrollX + scrollRangeX;
final int top = -maxOverScrollY;
final int bottom = maxOverScrollY + scrollRangeY;

boolean clampedX = false;
if (newScrollX > right) {
newScrollX = right;
clampedX = true;
} else if (newScrollX < left) {
newScrollX = left;
clampedX = true;
}

boolean clampedY = false;
if (newScrollY > bottom) {
newScrollY = bottom;
clampedY = true;
} else if (newScrollY < top) {
newScrollY = top;
clampedY = true;
}

onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);

return clampedX || clampedY;
}

Android的原生阻尼

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
    /**
* use
* EdgeEffect mEdgeGlowTop;Tracks the state of the top edge glow.
* EdgeEffect mEdgeGlowBottom;Tracks the state of the bottom edge glow.
*
* @param x
* @param y
* @param vtev
*/
private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
//......

final int overscrollMode = getOverScrollMode();
if (overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS &&
!contentFits())) {
if (!atOverscrollEdge) {
mDirection = 0; // Reset when entering overscroll.
mTouchMode = TOUCH_MODE_OVERSCROLL;
}
if (incrementalDeltaY > 0) {
mEdgeGlowTop.onPull((float) -overscroll / getHeight(),
(float) x / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
invalidateTopGlow();
} else if (incrementalDeltaY < 0) {
mEdgeGlowBottom.onPull((float) overscroll / getHeight(),
1.f - (float) x / getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
invalidateBottomGlow();
}
}
}
}
mMotionY = y + lastYCorrection + scrollOffsetCorrection;
}
mLastY = y + lastYCorrection + scrollOffsetCorrection;
}
} else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
if (y != mLastY) {
final int oldScroll = mScrollY;
final int newScroll = oldScroll - incrementalDeltaY;
int newDirection = y > mLastY ? 1 : -1;

if (mDirection == 0) {
mDirection = newDirection;
}

int overScrollDistance = -incrementalDeltaY;
if ((newScroll < 0 && oldScroll >= 0) || (newScroll > 0 && oldScroll <= 0)) {
overScrollDistance = -oldScroll;
incrementalDeltaY += overScrollDistance;
} else {
incrementalDeltaY = 0;
}

if (overScrollDistance != 0) {
overScrollBy(0, overScrollDistance, 0, mScrollY, 0, 0,
0, mOverscrollDistance, true);
final int overscrollMode = getOverScrollMode();
if (overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS &&
!contentFits())) {
if (rawDeltaY > 0) {
mEdgeGlowTop.onPull((float) overScrollDistance / getHeight(),
(float) x / getWidth());
if (!mEdgeGlowBottom.isFinished()) {
mEdgeGlowBottom.onRelease();
}
invalidateTopGlow();
} else if (rawDeltaY < 0) {
mEdgeGlowBottom.onPull((float) overScrollDistance / getHeight(),
1.f - (float) x / getWidth());
if (!mEdgeGlowTop.isFinished()) {
mEdgeGlowTop.onRelease();
}
invalidateBottomGlow();
}
}
}
//.......
}
EdgeEeffect
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
/**
* update state
*/
private void update() {
final long time = AnimationUtils.currentAnimationTimeMillis();
final float t = Math.min((time - mStartTime) / mDuration, 1.f);

final float interp = mInterpolator.getInterpolation(t);

mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
mDisplacement = (mDisplacement + mTargetDisplacement) / 2;

if (t >= 1.f - EPSILON) {
switch (mState) {
//start to init parameter
case STATE_ABSORB:

mState = STATE_RECEDE;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mDuration = RECEDE_TIME;

mGlowAlphaStart = mGlowAlpha;
mGlowScaleYStart = mGlowScaleY;

// After absorb, the glow should fade to nothing.
mGlowAlphaFinish = 0.f;
mGlowScaleYFinish = 0.f;
break;
//the pull motivation
case STATE_PULL:
mState = STATE_PULL_DECAY;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mDuration = PULL_DECAY_TIME;

mGlowAlphaStart = mGlowAlpha;
mGlowScaleYStart = mGlowScaleY;

// After pull, the glow should fade to nothing.
mGlowAlphaFinish = 0.f;
mGlowScaleYFinish = 0.f;
break;
// decay
case STATE_PULL_DECAY:
mState = STATE_RECEDE;
break;
//finished
case STATE_RECEDE:
mState = STATE_IDLE;
break;
}
}
}

SwipeRefreshLayout下拉刷新/上拉加载更多

how to create

contruction
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
  /**
* SwipeRefreshLayout extends ViewGroup
* Constructor that is called when inflating SwipeRefreshLayout from XML.
*
* @param context
* @param attrs
*/
public SwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);

mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

mMediumAnimationDuration = getResources().getInteger(
android.R.integer.config_mediumAnimTime);

//ViewGroup subClass call this,setWillNotDraw to forbidden the callback method onDraw()
setWillNotDraw(false);
mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);

final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
setEnabled(a.getBoolean(0, true));
a.recycle();

final DisplayMetrics metrics = getResources().getDisplayMetrics();
mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density);
mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density);

//create the pull View,here use the progressView
createProgressView();
ViewCompat.setChildrenDrawingOrderEnabled(this, true);
// the absolute offset has to take into account that the circle starts at an offset
//max pull distance
mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density;
mTotalDragDistance = mSpinnerFinalOffset;
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);

mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
onMeasure
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
/**
* 1.find the right target ,this is inside the swipeLayout by static xml;
* 2.measure the target manually
* 3.measure the added view which is inserted by the constructon method.here is the mCircleView
* 4.initial the value mOriginalOffsetTop. stand on the mTargetView//negative value
* 5.get the CircleIndex in parentView and store it in mCircleViewIndex
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
mTarget.measure(MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY));
if (!mUsingCustomStart && !mOriginalOffsetCalculated) {
mOriginalOffsetCalculated = true;
mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight();//stand on the mTargetView
}
mCircleViewIndex = -1;
// Get the index of the circleview.
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mCircleView) {
mCircleViewIndex = index;
break;
}
}
}
onLayout
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
/**
* 1. layout the target view
* left = getPaddingLeft()
* right = getPaddingLeft()+ [width - getPaddingLeft() - getPaddingRight()]
* top = getPaddingTop()
* bottom = getPaddingTop()+[height - getPaddingTop() - getPaddingBottom()]
*
* 2.layout the mCircleView
* left = make the circleView centerX
* right = make the circleView centerX
* top = above targetView mCurrentTargetOffsetTop
* bottom = alginTop of targetView
*
* @param changed
* @param left
* @param top
* @param right
* @param bottom
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0) {
return;
}
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
final View child = mTarget;
final int childLeft = getPaddingLeft();
final int childTop = getPaddingTop();
final int childWidth = width - getPaddingLeft() - getPaddingRight();
final int childHeight = height - getPaddingTop() - getPaddingBottom();
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
int circleWidth = mCircleView.getMeasuredWidth();
int circleHeight = mCircleView.getMeasuredHeight();
mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
(width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
}

touchEvent&&stateChange

onInterceptTouchEvent
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
/**
* consider when to intercept
* 1.reverse condition:canScrollUp,inRefreshState// not block
* 2.and then sequence condition:moveDistance larger than touchSlop// block
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();

final int action = MotionEventCompat.getActionMasked(ev);

if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}

if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}

switch (action) {
case MotionEvent.ACTION_DOWN:
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
final float initialDownY = getMotionEventY(ev, mActivePointerId);
if (initialDownY == -1) {
return false;
}
mInitialDownY = initialDownY;
break;

case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
return false;
}

final float y = getMotionEventY(ev, mActivePointerId);
if (y == -1) {
return false;
}
final float yDiff = y - mInitialDownY;
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialDownY + mTouchSlop;// record startY
mIsBeingDragged = true;
mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
}
break;

case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;

case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
break;
}

return mIsBeingDragged;
}
onTouchEvent
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
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);

if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}

if (!isEnabled() || mReturningToStart || canChildScrollUp()) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}

switch (action) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
break;

case MotionEvent.ACTION_MOVE: {
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
return false;
}

final float y = MotionEventCompat.getY(ev, pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
//开始pull 并且超过了一半
if (mIsBeingDragged) {
if (overscrollTop > 0) {
//do animation
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int index = MotionEventCompat.getActionIndex(ev);
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
break;
}

case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;

case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
if (mActivePointerId == INVALID_POINTER) {
if (action == MotionEvent.ACTION_UP) {
Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
}
return false;
}
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
mIsBeingDragged = false;
finishSpinner(overscrollTop);//cancel animation
mActivePointerId = INVALID_POINTER;
return false;
}
}

return true;
}

用progressView一样的方式去添加

construction
1
2
3
4
5
6
7
8
9
public SwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);

......
addView(mHeadViewContainer);
......
addView(mFooterViewContainer);
......
}
onMeasure
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
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
.......
// measure Header
mHeadViewContainer.measure(MeasureSpec.makeMeasureSpec(
mHeaderViewWidth, MeasureSpec.EXACTLY), MeasureSpec
.makeMeasureSpec(mHeaderViewHeight, MeasureSpec.EXACTLY));
// measure Footer
mFooterViewContainer.measure(MeasureSpec.makeMeasureSpec(
mFooterViewWidth, MeasureSpec.EXACTLY), MeasureSpec
.makeMeasureSpec(mFooterViewHeight, MeasureSpec.EXACTLY));
......
//record header index
mHeaderViewIndex = -1;
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mHeadViewContainer) {
mHeaderViewIndex = index;
break;
}
}
//record footer index
mFooterViewIndex = -1;
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mFooterViewContainer) {
mFooterViewIndex = index;
break;
}
}
}
onLayout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
......
int headViewWidth = mHeadViewContainer.getMeasuredWidth();
int headViewHeight = mHeadViewContainer.getMeasuredHeight();
mHeadViewContainer.layout((width / 2 - headViewWidth / 2),
mCurrentTargetOffsetTop, (width / 2 + headViewWidth / 2),
mCurrentTargetOffsetTop + headViewHeight);
int footViewWidth = mFooterViewContainer.getMeasuredWidth();
int footViewHeight = mFooterViewContainer.getMeasuredHeight();
mFooterViewContainer.layout((width / 2 - footViewWidth / 2), height
- pushDistance, (width / 2 + footViewWidth / 2), height
+ footViewHeight - pushDistance);
}
notify pull and push Action

如何判断到顶部或者底部

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134

/**
is at top
**/
public boolean isChildScrollToTop() {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return !(absListView.getChildCount() > 0 && (absListView
.getFirstVisiblePosition() > 0 || absListView
.getChildAt(0).getTop() < absListView.getPaddingTop()));
} else {
return !(mTarget.getScrollY() > 0);
}
} else {
return !ViewCompat.canScrollVertically(mTarget, -1);
}
}

/**
is at bottom
**/
public boolean isChildScrollToBottom() {
if (isChildScrollToTop()) {
return false;
}
if (mTarget instanceof RecyclerView) {//RecyclerView
RecyclerView recyclerView = (RecyclerView) mTarget;
LayoutManager layoutManager = recyclerView.getLayoutManager();
int count = recyclerView.getAdapter().getItemCount();
if (layoutManager instanceof LinearLayoutManager && count > 0) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
if (linearLayoutManager.findLastCompletelyVisibleItemPosition() == count - 1) {
return true;
}
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
int[] lastItems = new int[2];
staggeredGridLayoutManager
.findLastCompletelyVisibleItemPositions(lastItems);
int lastItem = Math.max(lastItems[0], lastItems[1]);
if (lastItem == count - 1) {
return true;
}
}
return false;
} else if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
int count = absListView.getAdapter().getCount();
int fristPos = absListView.getFirstVisiblePosition();
if (fristPos == 0
&& absListView.getChildAt(0).getTop() >= absListView
.getPaddingTop()) {
return false;
}
int lastPos = absListView.getLastVisiblePosition();
if (lastPos > 0 && count > 0 && lastPos == count - 1) {
return true;
}
return false;
} else if (mTarget instanceof ScrollView) {
ScrollView scrollView = (ScrollView) mTarget;
View view = (View) scrollView
.getChildAt(scrollView.getChildCount() - 1);
if (view != null) {
int diff = (view.getBottom() - (scrollView.getHeight() + scrollView
.getScrollY()));
if (diff == 0) {
return true;
}
}
}
return false;
}

/**
block event
**/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
......
if (!isEnabled() || mReturningToStart || mRefreshing || mLoadMore
|| (!isChildScrollToTop() && !isChildScrollToBottom())) {
// not at top ,not at bottom,not refresh ,not loadingMore
return false;
}

switch (action) {
case MotionEvent.ACTION_DOWN:
......

case MotionEvent.ACTION_MOVE:
......
float yDiff = 0;
if (isChildScrollToBottom()) {
yDiff = mInitialMotionY - y;// pull distance
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mIsBeingDragged = true;
}
} else {
yDiff = y - mInitialMotionY;// push distance
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mIsBeingDragged = true;
}
}
break;
......
}

return mIsBeingDragged;
}

/**
deal with touchEvent
**/
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);

if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
if (!isEnabled() || mReturningToStart
|| (!isChildScrollToTop() && !isChildScrollToBottom())) {
// if childView could move,do nothing
return false;
}

if (isChildScrollToBottom()) {// push
return handlerPushTouchEvent(ev, action);
} else {// pull
return handlerPullTouchEvent(ev, action);
}
}

add secondFloor

  1. abstract the headerView

    you can add other three states in the headerView .eg:readyEnter,startEnter,endEnter.

    And use the headerView to manager all the states.

  2. pull Action can be separated into two states

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
     private boolean handlePullTouchEvent(MotionEvent ev, int action) {
    ......
    case MotionEvent.ACTION_MOVE: {

    ......
    if(mIsBeingDragged) {
    distance = (int) (distance * mDragRate);
    ......
    if (distance < mTotalDragDistance) {
    // do refresh action
    } else {
    //do second floor action
    }
    break;
    }
    ......
    }
    }

ultimate struct

ListView_Struct