* This is offered as an optimization. If you know in advance the number of * pages you will need to support or have lazy-loading mechanisms in place * on your pages, tweaking this setting can have benefits in perceived * smoothness of paging animations and interaction. If you have a small * number of pages (3-4) that you can keep active all at once, less time * will be spent in layout for newly created view subtrees as the user pages * back and forth. *
* ** You should keep this limit low, especially if your pages have complex * layouts. This setting defaults to 1. *
* * @param limit * How many pages will be kept offscreen in an idle state. */ public void setOffscreenPageLimit(int limit) { if (limit < DEFAULT_OFFSCREEN_PAGES) { Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + DEFAULT_OFFSCREEN_PAGES); limit = DEFAULT_OFFSCREEN_PAGES; } if (limit != mOffscreenPageLimit) { mOffscreenPageLimit = limit; populate(); } } /** * Set the margin between pages. * * @param marginPixels * Distance between adjacent pages in pixels * @see #getPageMargin() * @see #setPageMarginDrawable(Drawable) * @see #setPageMarginDrawable(int) */ public void setPageMargin(int marginPixels) { final int oldMargin = mPageMargin; mPageMargin = marginPixels; final int width = getWidth(); recomputeScrollPosition(width, width, marginPixels, oldMargin); requestLayout(); } /** * Return the margin between pages. * * @return The size of the margin in pixels */ public int getPageMargin() { return mPageMargin; } /** * Set a drawable that will be used to fill the margin between pages. * * @param d * Drawable to display between pages */ public void setPageMarginDrawable(Drawable d) { mMarginDrawable = d; if (d != null) refreshDrawableState(); setWillNotDraw(d == null); invalidate(); } /** * Set a drawable that will be used to fill the margin between pages. * * @param resId * Resource ID of a drawable to display between pages */ public void setPageMarginDrawable(int resId) { setPageMarginDrawable(getContext().getResources().getDrawable(resId)); } @Override protected boolean verifyDrawable(Drawable who) { return super.verifyDrawable(who) || who == mMarginDrawable; } @Override protected void drawableStateChanged() { super.drawableStateChanged(); final Drawable d = mMarginDrawable; if (d != null && d.isStateful()) { d.setState(getDrawableState()); } } // We want the duration of the page snap animation to be influenced by the // distance that // the screen has to travel, however, we don't want this duration to be // effected in a // purely linear fashion. Instead, we use this method to moderate the effect // that the distance // of travel has on the overall snap duration. float distanceInfluenceForSnapDuration(float f) { f -= 0.5f; // center the values about 0. f *= 0.3f * Math.PI / 2.0f; return (float) Math.sin(f); } /** * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. * * @param x * the number of pixels to scroll by on the X axis * @param y * the number of pixels to scroll by on the Y axis */ void smoothScrollTo(int x, int y) { smoothScrollTo(x, y, 0); } /** * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. * * @param x * the number of pixels to scroll by on the X axis * @param y * the number of pixels to scroll by on the Y axis * @param velocity * the velocity associated with a fling, if applicable. (0 * otherwise) */ void smoothScrollTo(int x, int y, int velocity) { if (getChildCount() == 0) { // Nothing to do. setScrollingCacheEnabled(false); return; } int sx = getScrollX(); int sy = getScrollY(); int dx = x - sx; int dy = y - sy; if (dx == 0 && dy == 0) { completeScroll(); setScrollState(SCROLL_STATE_IDLE); return; } setScrollingCacheEnabled(true); mScrolling = true; setScrollState(SCROLL_STATE_SETTLING); final float pageDelta = (float) Math.abs(dx) / (getWidth() + mPageMargin); int duration = (int) (pageDelta * 100); velocity = Math.abs(velocity); if (velocity > 0) { duration += (duration / (velocity / mBaseLineFlingVelocity)) * mFlingVelocityInfluence; } else { duration += 100; } duration = Math.min(duration, MAX_SETTLE_DURATION); mScroller.startScroll(sx, sy, dx, dy, duration); invalidate(); } void addNewItem(int position, int index) { ItemInfo ii = new ItemInfo(); ii.position = position; ii.object = mAdapter.instantiateItem(this, position); if (index < 0) { mItems.add(ii); } else { mItems.add(index, ii); } } void dataSetChanged() { // This method only gets called if our observer is attached, so mAdapter // is non-null. boolean needPopulate = mItems.size() < 3 && mItems.size() < mAdapter.getCount(); int newCurrItem = -1; for (int i = 0; i < mItems.size(); i++) { final ItemInfo ii = mItems.get(i); final int newPos = mAdapter.getItemPosition(ii.object); if (newPos == PagerAdapter.POSITION_UNCHANGED) { continue; } if (newPos == PagerAdapter.POSITION_NONE) { mItems.remove(i); i--; mAdapter.destroyItem(this, ii.position, ii.object); needPopulate = true; if (mCurItem == ii.position) { // Keep the current item in the valid range newCurrItem = Math.max(0, Math.min(mCurItem, mAdapter.getCount() - 1)); } continue; } if (ii.position != newPos) { if (ii.position == mCurItem) { // Our current item changed position. Follow it. newCurrItem = newPos; } ii.position = newPos; needPopulate = true; } } Collections.sort(mItems, COMPARATOR); if (newCurrItem >= 0) { // TODO This currently causes a jump. setCurrentItemInternal(newCurrItem, false, true); needPopulate = true; } if (needPopulate) { populate(); requestLayout(); } } void populate() { if (mAdapter == null) { return; } // Bail now if we are waiting to populate. This is to hold off // on creating views from the time the user releases their finger to // fling to a new position until we have finished the scroll to // that position, avoiding glitches from happening at that point. if (mPopulatePending) { if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); return; } // Also, don't populate until we are attached to a window. This is to // avoid trying to populate before we have restored our view hierarchy // state and conflicting with what is restored. if (getWindowToken() == null) { return; } mAdapter.startUpdate(this); final int pageLimit = mOffscreenPageLimit; final int startPos = Math.max(0, mCurItem - pageLimit); final int N = mAdapter.getCount(); final int endPos = Math.min(N - 1, mCurItem + pageLimit); if (DEBUG) Log.v(TAG, "populating: startPos=" + startPos + " endPos=" + endPos); // Add and remove pages in the existing list. int lastPos = -1; for (int i = 0; i < mItems.size(); i++) { ItemInfo ii = mItems.get(i); if ((ii.position < startPos || ii.position > endPos) && !ii.scrolling) { if (DEBUG) Log.i(TAG, "removing: " + ii.position + " @ " + i); mItems.remove(i); i--; mAdapter.destroyItem(this, ii.position, ii.object); } else if (lastPos < endPos && ii.position > startPos) { // The next item is outside of our range, but we have a gap // between it and the last item where we want to have a page // shown. Fill in the gap. lastPos++; if (lastPos < startPos) { lastPos = startPos; } while (lastPos <= endPos && lastPos < ii.position) { if (DEBUG) Log.i(TAG, "inserting: " + lastPos + " @ " + i); addNewItem(lastPos, i); lastPos++; i++; } } lastPos = ii.position; } // Add any new pages we need at the end. lastPos = mItems.size() > 0 ? mItems.get(mItems.size() - 1).position : -1; if (lastPos < endPos) { lastPos++; lastPos = lastPos > startPos ? lastPos : startPos; while (lastPos <= endPos) { if (DEBUG) Log.i(TAG, "appending: " + lastPos); addNewItem(lastPos, -1); lastPos++; } } if (DEBUG) { Log.i(TAG, "Current page list:"); for (int i = 0; i < mItems.size(); i++) { Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); } } ItemInfo curItem = null; for (int i = 0; i < mItems.size(); i++) { if (mItems.get(i).position == mCurItem) { curItem = mItems.get(i); break; } } mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); mAdapter.finishUpdate(this); if (hasFocus()) { View currentFocused = findFocus(); ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; if (ii == null || ii.position != mCurItem) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); ii = infoForChild(child); if (ii != null && ii.position == mCurItem) { if (child.requestFocus(FOCUS_FORWARD)) { break; } } } } } } public static class SavedState extends BaseSavedState { int position; Parcelable adapterState; ClassLoader loader; public SavedState(Parcelable superState) { super(superState); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(position); out.writeParcelable(adapterState, flags); } @Override public String toString() { return "FragmentPager.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " position=" + position + "}"; } public static final Parcelable.Creator* A fake drag can be useful if you want to synchronize the motion of the * ViewPager with the touch scrolling of another view, while still letting * the ViewPager control the snapping motion and fling behavior. (e.g. * parallax-scrolling tabs.) Call {@link #fakeDragBy(float)} to simulate the * actual drag motion. Call {@link #endFakeDrag()} to complete the fake drag * and fling as necessary. * *
* During a fake drag the ViewPager will ignore all touch events. If a real
* drag is already in progress, this method will return false.
*
* @return true if the fake drag began successfully, false if it could not
* be started.
*
* @see #fakeDragBy(float)
* @see #endFakeDrag()
*/
public boolean beginFakeDrag() {
if (mIsBeingDragged) {
return false;
}
mFakeDragging = true;
setScrollState(SCROLL_STATE_DRAGGING);
mInitialMotionX = mLastMotionX = 0;
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();
}
final long time = SystemClock.uptimeMillis();
final MotionEvent ev = MotionEvent.obtain(time, time,
MotionEvent.ACTION_DOWN, 0, 0, 0);
mVelocityTracker.addMovement(ev);
ev.recycle();
mFakeDragBeginTime = time;
return true;
}
/**
* End a fake drag of the pager.
*
* @see #beginFakeDrag()
* @see #fakeDragBy(float)
*/
public void endFakeDrag() {
Log.d(TAG, "endFakeDrag");
if (!mFakeDragging) {
throw new IllegalStateException(
"No fake drag in progress. Call beginFakeDrag first.");
}
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
velocityTracker, mActivePointerId);
mPopulatePending = true;
Log.d(TAG,
"initialVelocity: " + initialVelocity
+ ", Math.abs(mInitialMotionX - mLastMotionX): "
+ Math.abs(mInitialMotionX - mLastMotionX));
if ((Math.abs(initialVelocity) > mMinimumVelocity)
|| Math.abs(mInitialMotionX - mLastMotionX) >= (getWidth() / 3)) {
if (mLastMotionX > mInitialMotionX) {
setCurrentItemInternal(mCurItem - 1, true, true);
} else {
setCurrentItemInternal(mCurItem + 1, true, true);
}
} else {
setCurrentItemInternal(mCurItem, true, true);
}
endDrag();
mFakeDragging = false;
}
/**
* Fake drag by an offset in pixels. You must have called
* {@link #beginFakeDrag()} first.
*
* @param xOffset
* Offset in pixels to drag by.
* @see #beginFakeDrag()
* @see #endFakeDrag()
*/
public void fakeDragBy(float xOffset) {
if (!mFakeDragging) {
throw new IllegalStateException(
"No fake drag in progress. Call beginFakeDrag first.");
}
mLastMotionX += xOffset;
float scrollX = getScrollX() - xOffset;
final int width = getWidth();
final int widthWithMargin = width + mPageMargin;
final float leftBound = Math.max(0, (mCurItem - 1) * widthWithMargin);
final float rightBound = Math
.min(mCurItem + 1, mAdapter.getCount() - 1) * widthWithMargin;
if (scrollX < leftBound) {
scrollX = leftBound;
} else if (scrollX > rightBound) {
scrollX = rightBound;
}
// Don't lose the rounded component
mLastMotionX += scrollX - (int) scrollX;
scrollTo((int) scrollX, getScrollY());
if (mOnPageChangeListener != null) {
final int position = (int) scrollX / widthWithMargin;
final int positionOffsetPixels = (int) scrollX % widthWithMargin;
final float positionOffset = (float) positionOffsetPixels
/ widthWithMargin;
mOnPageChangeListener.onPageScrolled(position, positionOffset,
positionOffsetPixels);
}
// Synthesize an event for the VelocityTracker.
final long time = SystemClock.uptimeMillis();
final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time,
MotionEvent.ACTION_MOVE, mLastMotionX, 0, 0);
mVelocityTracker.addMovement(ev);
ev.recycle();
}
/**
* Returns true if a fake drag is in progress.
*
* @return true if currently in a fake drag, false otherwise.
*
* @see #beginFakeDrag()
* @see #fakeDragBy(float)
* @see #endFakeDrag()
*/
public boolean isFakeDragging() {
return mFakeDragging;
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
mActivePointerId = MotionEventCompat.getPointerId(ev,
newPointerIndex);
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
}
private void endDrag() {
mIsBeingDragged = false;
mIsUnableToDrag = false;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private void setScrollingCacheEnabled(boolean enabled) {
if (mScrollingCacheEnabled != enabled) {
mScrollingCacheEnabled = enabled;
if (USE_CACHE) {
final int size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
child.setDrawingCacheEnabled(enabled);
}
}
}
}
}
/**
* Tests scrollability within child views of v given a delta of dx.
*
* @param v
* View to test for horizontal scrollability
* @param checkV
* Whether the view v passed should itself be checked for
* scrollability (true), or just its children (false).
* @param dx
* Delta scrolled in pixels
* @param x
* X coordinate of the active touch point
* @param y
* Y coordinate of the active touch point
* @return true if child views of v can be scrolled by delta of dx.
*/
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int scrollX = v.getScrollX();
final int scrollY = v.getScrollY();
final int count = group.getChildCount();
// Count backwards - let topmost views consume scroll distance
// first.
for (int i = count - 1; i >= 0; i--) {
// TODO: Add versioned support here for transformed views.
// This will not work for transformed views in Honeycomb+
final View child = group.getChildAt(i);
if (x + scrollX >= child.getLeft()
&& x + scrollX < child.getRight()
&& y + scrollY >= child.getTop()
&& y + scrollY < child.getBottom()
&& canScroll(child, true, dx,
x + scrollX - child.getLeft(), y + scrollY
- child.getTop())) {
return true;
}
}
}
return checkV && ViewCompat.canScrollHorizontally(v, -dx);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Let the focused view and/or our descendants get the key first
return super.dispatchKeyEvent(event) || executeKeyEvent(event);
}
/**
* You can call this function yourself to have the scroll view perform
* scrolling from a key event, just as if the event had been dispatched to
* it by the view hierarchy.
*
* @param event
* The key event to execute.
* @return Return true if the event was handled, else false.
*/
public boolean executeKeyEvent(KeyEvent event) {
boolean handled = false;
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
handled = arrowScroll(FOCUS_LEFT);
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
handled = arrowScroll(FOCUS_RIGHT);
break;
case KeyEvent.KEYCODE_TAB:
if (KeyEventCompat.hasNoModifiers(event)) {
handled = arrowScroll(FOCUS_FORWARD);
} else if (KeyEventCompat.hasModifiers(event,
KeyEvent.META_SHIFT_ON)) {
handled = arrowScroll(FOCUS_BACKWARD);
}
break;
}
}
return handled;
}
public boolean arrowScroll(int direction) {
View currentFocused = findFocus();
if (currentFocused == this)
currentFocused = null;
boolean handled = false;
View nextFocused = FocusFinder.getInstance().findNextFocus(this,
currentFocused, direction);
if (nextFocused != null && nextFocused != currentFocused) {
if (direction == View.FOCUS_LEFT) {
// If there is nothing to the left, or this is causing us to
// jump to the right, then what we really want to do is page
// left.
if (currentFocused != null
&& nextFocused.getLeft() >= currentFocused.getLeft()) {
handled = pageLeft();
} else {
handled = nextFocused.requestFocus();
}
} else if (direction == View.FOCUS_RIGHT) {
// If there is nothing to the right, or this is causing us to
// jump to the left, then what we really want to do is page
// right.
if (currentFocused != null
&& nextFocused.getLeft() <= currentFocused.getLeft()) {
handled = pageRight();
} else {
handled = nextFocused.requestFocus();
}
}
} else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {
// Trying to move left and nothing there; try to page.
handled = pageLeft();
} else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {
// Trying to move right and nothing there; try to page.
handled = pageRight();
}
if (handled) {
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));
}
return handled;
}
boolean pageLeft() {
if (mCurItem > 0) {
setCurrentItem(mCurItem - 1, true);
return true;
}
return false;
}
boolean pageRight() {
if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) {
setCurrentItem(mCurItem + 1, true);
return true;
}
return false;
}
/**
* We only want the current page that is being shown to be focusable.
*/
@Override
public void addFocusables(ArrayList