* 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