In article juejin. Cn/post / 696909…
Slide to load more data
After two layoutings, we can already see the content in the ListView, but the most amazing part of the ListView is still missing, because the ListView only loads and displays the data from the first screen. For example, we have 1000 entries in the Adapter, but only 10 entries are displayed in the first screen, and only 10 sub-views are displayed in the ListView. How does the remaining 990 entries work and are displayed in the ListView? This will take a look at the source of the ListView slide section, because we are using the finger slide to display more data.
Since the sliding mechanism is generic, i.e. the ListView and GridView use the same mechanism, this code must be written in the AbsListView. So listening for touch events is done in the onTouchEvent() method, let’s look at this method in the AbsListView:
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!isEnabled()) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return isClickable() || isLongClickable();
}
final int action = ev.getAction();
View v;
int deltaY;
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
mActivePointerId = ev.getPointerId(0);
final int x = (int) ev.getX();
final int y = (int) ev.getY();
int motionPosition = pointToPosition(x, y);
if (!mDataChanged) {
if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
&& (getAdapter().isEnabled(motionPosition))) {
// User clicked on an actual view (and was not stopping a
// fling). It might be a
// click or a scroll. Assume it is a click until proven
// otherwise
mTouchMode = TOUCH_MODE_DOWN;
// FIXME Debounce
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
// If we couldn't find a view to click on, but the down
// event was touching
// the edge, we will bail out and try again. This allows
// the edge correcting
// code in ViewRoot to try to find a nearby view to
// select
return false;
}
if (mTouchMode == TOUCH_MODE_FLING) {
// Stopped a fling. It is a scroll.
createScrollingCache();
mTouchMode = TOUCH_MODE_SCROLL;
mMotionCorrection = 0;
motionPosition = findMotionRow(y);
reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
}
}
if (motionPosition >= 0) {
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = v.getTop();
}
mMotionX = x;
mMotionY = y;
mMotionPosition = motionPosition;
mLastY = Integer.MIN_VALUE;
break;
}
case MotionEvent.ACTION_MOVE: {
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final int y = (int) ev.getY(pointerIndex);
deltaY = y - mMotionY;
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
// Check if we have moved far enough that it looks more like a
// scroll than a tap
startScrollIfNeeded(deltaY);
break;
case TOUCH_MODE_SCROLL:
if (PROFILE_SCROLLING) {
if (!mScrollProfilingStarted) {
Debug.startMethodTracing("AbsListViewScroll");
mScrollProfilingStarted = true;
}
}
if (y != mLastY) {
deltaY -= mMotionCorrection;
int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
// No need to do all this work if we're not going to move
// anyway
boolean atEdge = false;
if (incrementalDeltaY != 0) {
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
// Check to see if we have bumped into the scroll limit
if (atEdge && getChildCount() > 0) {
// Treat this like we're starting a new scroll from the
// current
// position. This will let the user start scrolling back
// into
// content immediately rather than needing to scroll
// back to the
// point where they hit the limit first.
int motionPosition = findMotionRow(y);
if (motionPosition >= 0) {
final View motionView = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = motionView.getTop();
}
mMotionY = y;
mMotionPosition = motionPosition;
invalidate();
}
mLastY = y;
}
break;
}
break;
}
case MotionEvent.ACTION_UP: {
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 && !child.hasFocusable()) {
if (mTouchMode != TOUCH_MODE_DOWN) {
child.setPressed(false);
}
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
final AbsListView.PerformClick performClick = mPerformClick;
performClick.mChild = child;
performClick.mClickMotionPosition = motionPosition;
performClick.rememberWindowAttachCount();
mResurrectToPosition = motionPosition;
if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap
: mPendingCheckForLongPress);
}
mLayoutMode = LAYOUT_NORMAL;
if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
mTouchMode = TOUCH_MODE_TAP;
setSelectedPositionInt(mMotionPosition);
layoutChildren();
child.setPressed(true);
positionSelector(child);
setPressed(true);
if (mSelector != null) {
Drawable d = mSelector.getCurrent();
if (d != null && d instanceof TransitionDrawable) {
((TransitionDrawable) d).resetTransition();
}
}
postDelayed(new Runnable() {
public void run() {
child.setPressed(false);
setPressed(false);
if (!mDataChanged) {
post(performClick);
}
mTouchMode = TOUCH_MODE_REST;
}
}, ViewConfiguration.getPressedStateDuration());
} else {
mTouchMode = TOUCH_MODE_REST;
}
return true;
} else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
post(performClick);
}
}
mTouchMode = TOUCH_MODE_REST;
break;
case TOUCH_MODE_SCROLL:
final int childCount = getChildCount();
if (childCount > 0) {
if (mFirstPosition == 0
&& getChildAt(0).getTop() >= mListPadding.top
&& mFirstPosition + childCount < mItemCount
&& getChildAt(childCount - 1).getBottom() <= getHeight()
- mListPadding.bottom) {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
} else {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
final int initialVelocity = (int) velocityTracker
.getYVelocity(mActivePointerId);
if (Math.abs(initialVelocity) > mMinimumVelocity) {
if (mFlingRunnable == null) {
mFlingRunnable = new FlingRunnable();
}
reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
mFlingRunnable.start(-initialVelocity);
} else {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
}
} else {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
break;
}
setPressed(false);
// Need to redraw since we probably aren't drawing the selector
// anymore
invalidate();
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mActivePointerId = INVALID_POINTER;
if (PROFILE_SCROLLING) {
if (mScrollProfilingStarted) {
Debug.stopMethodTracing();
mScrollProfilingStarted = false;
}
}
break;
}
case MotionEvent.ACTION_CANCEL: {
mTouchMode = TOUCH_MODE_REST;
setPressed(false);
View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
if (motionView != null) {
motionView.setPressed(false);
}
clearScrollingCache();
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mActivePointerId = INVALID_POINTER;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
onSecondaryPointerUp(ev);
final int x = mMotionX;
final int y = mMotionY;
final int motionPosition = pointToPosition(x, y);
if (motionPosition >= 0) {
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = v.getTop();
mMotionPosition = motionPosition;
}
mLastY = y;
break;
}
}
return true;
}
Copy the code
There’s a lot of code in this method because it handles a lot of logic, listening for all kinds of touch screen events. But all we care about so far is the single event of the finger sliding on the screen, which corresponds to the ACTION_MOVE action, so we’ll just look at this part of the code.
ACTION_MOVE case contains a nested switch statement, which is selected according to the current TouchMode. So I can just tell you that when you swipe your finger on the screen, TouchMode is equal to TOUCH_MODE_SCROLL, and why that involves a bunch of other methods, which I won’t go into for space reasons, Like inquisitive friends can go to the source code to find a reason.
If so, the code should end up in this case on line 78, where there’s not much to notice except for the trackMotionScroll() method called on line 92, This method will be called as long as our finger moves on the screen slightly, and as long as we swipe on the screen normally, this method will be called many times. So let’s go into this method and take a look. The code looks like this:
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { final int childCount = getChildCount(); if (childCount == 0) { return true; } final int firstTop = getChildAt(0).getTop(); final int lastBottom = getChildAt(childCount - 1).getBottom(); final Rect listPadding = mListPadding; final int spaceAbove = listPadding.top - firstTop; final int end = getHeight() - listPadding.bottom; final int spaceBelow = lastBottom - end; final int height = getHeight() - getPaddingBottom() - getPaddingTop(); 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 int firstPosition = mFirstPosition; if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) { // Don't need to move views down if the top of the first position // is already visible return true; } if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) { // Don't need to move views up if the bottom of the last position // is already visible return true; } final boolean down = incrementalDeltaY < 0; final boolean inTouchMode = isInTouchMode(); if (inTouchMode) { hideSelector(); } final int headerViewsCount = getHeaderViewsCount(); final int footerViewsStart = mItemCount - getFooterViewsCount(); int start = 0; int count = 0; if (down) { final int top = listPadding.top - incrementalDeltaY; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getBottom() >= top) { break; } else { count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { mRecycler.addScrapView(child); } } } } else { final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY; for (int i = childCount - 1; i >= 0; i--) { final View child = getChildAt(i); if (child.getTop() <= bottom) { break; } else { start = i; count++; int position = firstPosition + i; if (position >= headerViewsCount && position < footerViewsStart) { mRecycler.addScrapView(child); } } } } mMotionViewNewTop = mMotionViewOriginalTop + deltaY; mBlockLayoutRequests = true; if (count > 0) { detachViewsFromParent(start, count); } offsetChildrenTopAndBottom(incrementalDeltaY); if (down) { mFirstPosition += count; } invalidate(); final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { fillGap(down); } if (! inTouchMode && mSelectedPosition ! = INVALID_POSITION) { final int childIndex = mSelectedPosition - mFirstPosition; if (childIndex >= 0 && childIndex < getChildCount()) { positionSelector(getChildAt(childIndex)); } } mBlockLayoutRequests = false; invokeOnItemScrollListener(); awakenScrollBars(); return false; }Copy the code
This method takes two parameters, deltaY representing the distance from the position of the finger when it was pressed to the current position of the finger, incrementalDeltaY representing the change in the position of the finger in the Y direction since the last time the Event was triggered, We can tell if the user is sliding up or down by checking the positive or negative value of incrementalDeltaY. As line 34 shows, if incrementalDeltaY is less than 0, it is sliding down, otherwise it is sliding up.
Now we’re going to do a boundary check, and you can see that starting at line 43, as the ListView slides down, we’re going to enter a for loop, and we’re going to get the child views from the top down. In line 47, If the bottom value of the child View is less than the top value, the child View has been removed from the screen, so the RecycleBin addScrapView() method is used to add the View to the scrap cache. And increment the count counter, which counts how many child views are removed from the screen, by one. So if you slide the ListView up, the process is basically the same, except that you get the child View from the bottom up, and then you determine whether the top value of the child View is greater than the bottom value, and if it is greater than the View has moved off the screen, Again, add it to the obsolete cache and increment the counter by one.
In line 76, a detach operation is performed based on the value of the current counter. This detach operation detach all child views that are removed from the screen. In the ListView concept, there is no need to save any View that is not visible. Because there are hundreds of thousands of pieces of data waiting to be displayed outside the screen, a good recycling strategy ensures a high performance and high efficiency ListView. Then in line 78 calls the offsetChildrenTopAndBottom () method, and the incrementalDeltaY passed as a parameter, This method offsets all the subviews of the ListView to the value of the parameter passed in, so that the contents of the ListView will scroll along with your finger.
Then line 84 checks that if the bottom of the last View in the ListView has moved into the screen, or the top of the first View in the ListView has moved into the screen, the fillGap() method is called, So we can guess that the fillGap() method is used to load off-screen data. Enter this method and take a look, as follows:
/**
* 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.
*
* @param down
* true if the scroll is going down, false if it is going up
*/
abstract void fillGap(boolean down);
Copy the code
OK, fillGap() in the AbsListView is an abstract method, so we immediately know that its implementation must be done in the ListView. Back in the ListView, the code for the fillGap() method looks like this:
void fillGap(boolean down) { final int count = getChildCount(); if (down) { final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight : getListPaddingTop(); fillDown(mFirstPosition + count, startOffset); correctTooHigh(getChildCount()); } else { final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight : getHeight() - getListPaddingBottom(); fillUp(mFirstPosition - 1, startOffset); correctTooLow(getChildCount()); }}Copy the code
The down parameter is used to indicate whether the ListView slides down or up. As you can see, the fillDown() method is called if the ListView slides down and the fillUp() method is called if the ListView slides up. So both of these methods we’re pretty familiar with, they’re all going through a loop to fill the ListView internally, so we’re not going to look at either of these methods, but filling the ListView is done by calling makeAndAddView(), Again, the makeAndAddView() method, but this time the logic is different, so let’s go back to it:
/** * 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 exsiting 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; }Copy the code
The RecycleBin getActiveView() method will be used to retrieve the child Layout, but it will not be able to retrieve the child Layout, because in the second Layout process we have obtained the data from mActiveViews. According to the RecycleBin mechanism, mActiveViews can not be reused, so the value returned here must be null.
Since the getActiveView() method returns null, we still go to the obtainView() method on line 28, which looks like this:
/** * Get a view and have it show the data associated with the specified * position. This is called when we have already discovered that the view is * not available for reuse in the recycle bin. The only choices left are * converting an old view or making a new one. * * @param position * The position to display * @param isScrap * Array of at least 1 boolean, the first entry will become true * if the returned view was taken from the scrap heap, false if * otherwise. * * @return A view displaying the data associated with the specified position */ View obtainView(int position, boolean[] isScrap) { isScrap[0] = false; View scrapView; scrapView = mRecycler.getScrapView(position); View child; if (scrapView ! = null) { child = mAdapter.getView(position, scrapView, this); if (child ! = scrapView) { mRecycler.addScrapView(scrapView); if (mCacheColorHint ! = 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } } else { isScrap[0] = true; dispatchFinishTemporaryDetach(child); } } else { child = mAdapter.getView(position, null, this); if (mCacheColorHint ! = 0) { child.setDrawingCacheBackgroundColor(mCacheColorHint); } } return child; }Copy the code
The RecyleBin getScrapView() method is invoked on line 19 to retrieve a View from the scrapheap. Of course, because as we saw in the trackMotionScroll() method, once any child View is removed from the screen, it will be added to the scrap cache, and from the logic in the obtainView() method, once new data needs to be displayed on the screen, I’ll try to fetch the View from the stale cache. So they form a producer and consumer pattern, so the magic of ListView is reflected here, no matter how much data you have to display, the subviews in ListView actually go back and forth with just a few, Off-screen child views are quickly reused by on-screen data, so no matter how much data we load, we don’t get OOM or even increase memory.
One other thing to note here is that we get a scrapView, which we pass in line 22 as the second argument to the Adapter’s getView() method. So what does the second parameter mean? Let’s look again at an example of a simple getView() method:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position);
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}
Copy the code
The second argument is the familiar convertView. No wonder we write the getView() method to check whether convertView is null and call the inflate() method to load the layout if it is null. If it’s not null, you can just use convertView, because convertView is the View that we used between us, but it just got removed from the screen, went into the scrap cache, and now it’s back. Then all we need to do is update the data in the convertView to the data that should be displayed at the current location, so it looks like a new layout that’s been loaded. Do you get the whole idea?
After that, we’re back to the familiar process of retrieving the child View from the cache and reattaching it to the ListView by calling setupChild(), because the cached View was detached from the ListView. This part of the code will not be analyzed repeatedly.
For your convenience, here is another illustration:
So far, we will put the ListView workflow code basic analysis of the end, the article is longer, I hope you can understand clearly, the next article will explain our usual use of ListView encountered problems, Android ListView asynchronously loading images out of order problem, cause analysis and solution
Blockquote {border – left: 10 px solid rgba (128128128,0.075); Background – color: rgba (128128128,0.05); border-radius: 0 5px 5px 0; padding: 15px 20px; }
Pay attention to my technical public account “Guo Lin”, there are high-quality technical articles push.