/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.widget; import android.annotation.DrawableRes; import android.annotation.NonNull; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.util.MathUtils; import android.view.FocusFinder; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.Interpolator; import android.widget.EdgeEffect; import android.widget.Scroller; import com.android.internal.R; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; /** * Layout manager that allows the user to flip left and right * through pages of data. You supply an implementation of a * {@link android.support.v4.view.PagerAdapter} to generate the pages that the view shows. * *

Note this class is currently under early design and * development. The API will likely change in later updates of * the compatibility library, requiring changes to the source code * of apps when they are compiled against the newer version.

* *

ViewPager is most often used in conjunction with {@link android.app.Fragment}, * which is a convenient way to supply and manage the lifecycle of each page. * There are standard adapters implemented for using fragments with the ViewPager, * which cover the most common use cases. These are * {@link android.support.v4.app.FragmentPagerAdapter} and * {@link android.support.v4.app.FragmentStatePagerAdapter}; each of these * classes have simple code showing how to build a full user interface * with them. * *

For more information about how to use ViewPager, read Creating Swipe Views with * Tabs.

* *

Below is a more complicated example of ViewPager, using it in conjunction * with {@link android.app.ActionBar} tabs. You can find other examples of using * ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code. * * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java * complete} */ public class ViewPager extends ViewGroup { private static final String TAG = "ViewPager"; private static final boolean DEBUG = false; private static final int MAX_SCROLL_X = 2 << 23; private static final boolean USE_CACHE = false; private static final int DEFAULT_OFFSCREEN_PAGES = 1; private static final int MAX_SETTLE_DURATION = 600; // ms private static final int MIN_DISTANCE_FOR_FLING = 25; // dips private static final int DEFAULT_GUTTER_SIZE = 16; // dips private static final int MIN_FLING_VELOCITY = 400; // dips private static final int[] LAYOUT_ATTRS = new int[] { com.android.internal.R.attr.layout_gravity }; /** * Used to track what the expected number of items in the adapter should be. * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. */ private int mExpectedAdapterCount; static class ItemInfo { Object object; boolean scrolling; float widthFactor; /** Logical position of the item within the pager adapter. */ int position; /** Offset between the starting edges of the item and its container. */ float offset; } private static final Comparator COMPARATOR = new Comparator(){ @Override public int compare(ItemInfo lhs, ItemInfo rhs) { return lhs.position - rhs.position; } }; private static final Interpolator sInterpolator = new Interpolator() { public float getInterpolation(float t) { t -= 1.0f; return t * t * t * t * t + 1.0f; } }; private final ArrayList mItems = new ArrayList(); private final ItemInfo mTempItem = new ItemInfo(); private final Rect mTempRect = new Rect(); private PagerAdapter mAdapter; private int mCurItem; // Index of currently displayed page. private int mRestoredCurItem = -1; private Parcelable mRestoredAdapterState = null; private ClassLoader mRestoredClassLoader = null; private final Scroller mScroller; private PagerObserver mObserver; private int mPageMargin; private Drawable mMarginDrawable; private int mTopPageBounds; private int mBottomPageBounds; /** * The increment used to move in the "left" direction. Dependent on layout * direction. */ private int mLeftIncr = -1; // Offsets of the first and last items, if known. // Set during population, used to determine if we are at the beginning // or end of the pager data set during touch scrolling. private float mFirstOffset = -Float.MAX_VALUE; private float mLastOffset = Float.MAX_VALUE; private int mChildWidthMeasureSpec; private int mChildHeightMeasureSpec; private boolean mInLayout; private boolean mScrollingCacheEnabled; private boolean mPopulatePending; private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; private boolean mIsBeingDragged; private boolean mIsUnableToDrag; private final int mDefaultGutterSize; private int mGutterSize; private final int mTouchSlop; /** * Position of the last motion event. */ private float mLastMotionX; private float mLastMotionY; private float mInitialMotionX; private float mInitialMotionY; /** * ID of the active pointer. This is used to retain consistency during * drags/flings if multiple pointers are used. */ private int mActivePointerId = INVALID_POINTER; /** * Sentinel value for no current active pointer. * Used by {@link #mActivePointerId}. */ private static final int INVALID_POINTER = -1; /** * Determines speed during touch scrolling */ private VelocityTracker mVelocityTracker; private final int mMinimumVelocity; private final int mMaximumVelocity; private final int mFlingDistance; private final int mCloseEnough; // If the pager is at least this close to its final position, complete the scroll // on touch down and let the user interact with the content inside instead of // "catching" the flinging pager. private static final int CLOSE_ENOUGH = 2; // dp private final EdgeEffect mLeftEdge; private final EdgeEffect mRightEdge; private boolean mFirstLayout = true; private boolean mCalledSuper; private int mDecorChildCount; private OnPageChangeListener mOnPageChangeListener; private OnPageChangeListener mInternalPageChangeListener; private OnAdapterChangeListener mAdapterChangeListener; private PageTransformer mPageTransformer; private static final int DRAW_ORDER_DEFAULT = 0; private static final int DRAW_ORDER_FORWARD = 1; private static final int DRAW_ORDER_REVERSE = 2; private int mDrawingOrder; private ArrayList mDrawingOrderedChildren; private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); /** * Indicates that the pager is in an idle, settled state. The current page * is fully in view and no animation is in progress. */ public static final int SCROLL_STATE_IDLE = 0; /** * Indicates that the pager is currently being dragged by the user. */ public static final int SCROLL_STATE_DRAGGING = 1; /** * Indicates that the pager is in the process of settling to a final position. */ public static final int SCROLL_STATE_SETTLING = 2; private final Runnable mEndScrollRunnable = new Runnable() { public void run() { setScrollState(SCROLL_STATE_IDLE); populate(); } }; private int mScrollState = SCROLL_STATE_IDLE; /** * Callback interface for responding to changing state of the selected page. */ public interface OnPageChangeListener { /** * This method will be invoked when the current page is scrolled, either as part * of a programmatically initiated smooth scroll or a user initiated touch scroll. * * @param position Position index of the first page currently being displayed. * Page position+1 will be visible if positionOffset is nonzero. * @param positionOffset Value from [0, 1) indicating the offset from the page at position. * @param positionOffsetPixels Value in pixels indicating the offset from position. */ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); /** * This method will be invoked when a new page becomes selected. Animation is not * necessarily complete. * * @param position Position index of the new selected page. */ public void onPageSelected(int position); /** * Called when the scroll state changes. Useful for discovering when the user * begins dragging, when the pager is automatically settling to the current page, * or when it is fully stopped/idle. * * @param state The new scroll state. * @see com.android.internal.widget.ViewPager#SCROLL_STATE_IDLE * @see com.android.internal.widget.ViewPager#SCROLL_STATE_DRAGGING * @see com.android.internal.widget.ViewPager#SCROLL_STATE_SETTLING */ public void onPageScrollStateChanged(int state); } /** * Simple implementation of the {@link OnPageChangeListener} interface with stub * implementations of each method. Extend this if you do not intend to override * every method of {@link OnPageChangeListener}. */ public static class SimpleOnPageChangeListener implements OnPageChangeListener { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { // This space for rent } @Override public void onPageSelected(int position) { // This space for rent } @Override public void onPageScrollStateChanged(int state) { // This space for rent } } /** * A PageTransformer is invoked whenever a visible/attached page is scrolled. * This offers an opportunity for the application to apply a custom transformation * to the page views using animation properties. * *

As property animation is only supported as of Android 3.0 and forward, * setting a PageTransformer on a ViewPager on earlier platform versions will * be ignored.

*/ public interface PageTransformer { /** * Apply a property transformation to the given page. * * @param page Apply the transformation to this page * @param position Position of page relative to the current front-and-center * position of the pager. 0 is front and center. 1 is one full * page position to the right, and -1 is one page position to the left. */ public void transformPage(View page, float position); } /** * Used internally to monitor when adapters are switched. */ interface OnAdapterChangeListener { public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter); } /** * Used internally to tag special types of child views that should be added as * pager decorations by default. */ interface Decor {} public ViewPager(Context context) { this(context, null); } public ViewPager(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ViewPager(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public ViewPager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); setWillNotDraw(false); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setFocusable(true); mScroller = new Scroller(context, sInterpolator); final ViewConfiguration configuration = ViewConfiguration.get(context); final float density = context.getResources().getDisplayMetrics().density; mTouchSlop = configuration.getScaledPagingTouchSlop(); mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); mLeftEdge = new EdgeEffect(context); mRightEdge = new EdgeEffect(context); mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); mCloseEnough = (int) (CLOSE_ENOUGH * density); mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } } @Override protected void onDetachedFromWindow() { removeCallbacks(mEndScrollRunnable); super.onDetachedFromWindow(); } private void setScrollState(int newState) { if (mScrollState == newState) { return; } mScrollState = newState; if (mPageTransformer != null) { // PageTransformers can do complex things that benefit from hardware layers. enableLayers(newState != SCROLL_STATE_IDLE); } if (mOnPageChangeListener != null) { mOnPageChangeListener.onPageScrollStateChanged(newState); } } /** * Set a PagerAdapter that will supply views for this pager as needed. * * @param adapter Adapter to use */ public void setAdapter(PagerAdapter adapter) { if (mAdapter != null) { mAdapter.unregisterDataSetObserver(mObserver); mAdapter.startUpdate(this); for (int i = 0; i < mItems.size(); i++) { final ItemInfo ii = mItems.get(i); mAdapter.destroyItem(this, ii.position, ii.object); } mAdapter.finishUpdate(this); mItems.clear(); removeNonDecorViews(); mCurItem = 0; scrollTo(0, 0); } final PagerAdapter oldAdapter = mAdapter; mAdapter = adapter; mExpectedAdapterCount = 0; if (mAdapter != null) { if (mObserver == null) { mObserver = new PagerObserver(); } mAdapter.registerDataSetObserver(mObserver); mPopulatePending = false; final boolean wasFirstLayout = mFirstLayout; mFirstLayout = true; mExpectedAdapterCount = mAdapter.getCount(); if (mRestoredCurItem >= 0) { mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); setCurrentItemInternal(mRestoredCurItem, false, true); mRestoredCurItem = -1; mRestoredAdapterState = null; mRestoredClassLoader = null; } else if (!wasFirstLayout) { populate(); } else { requestLayout(); } } if (mAdapterChangeListener != null && oldAdapter != adapter) { mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); } } private void removeNonDecorViews() { for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.isDecor) { removeViewAt(i); i--; } } } /** * Retrieve the current adapter supplying pages. * * @return The currently registered PagerAdapter */ public PagerAdapter getAdapter() { return mAdapter; } void setOnAdapterChangeListener(OnAdapterChangeListener listener) { mAdapterChangeListener = listener; } private int getPaddedWidth() { return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); } /** * Set the currently selected page. If the ViewPager has already been through its first * layout with its current adapter there will be a smooth animated transition between * the current item and the specified item. * * @param item Item index to select */ public void setCurrentItem(int item) { mPopulatePending = false; setCurrentItemInternal(item, !mFirstLayout, false); } /** * Set the currently selected page. * * @param item Item index to select * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately */ public void setCurrentItem(int item, boolean smoothScroll) { mPopulatePending = false; setCurrentItemInternal(item, smoothScroll, false); } public int getCurrentItem() { return mCurItem; } boolean setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { return setCurrentItemInternal(item, smoothScroll, always, 0); } boolean setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { if (mAdapter == null || mAdapter.getCount() <= 0) { setScrollingCacheEnabled(false); return false; } item = MathUtils.constrain(item, 0, mAdapter.getCount() - 1); if (!always && mCurItem == item && mItems.size() != 0) { setScrollingCacheEnabled(false); return false; } final int pageLimit = mOffscreenPageLimit; if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { // We are doing a jump by more than one page. To avoid // glitches, we want to keep all current pages in the view // until the scroll ends. for (int i = 0; i < mItems.size(); i++) { mItems.get(i).scrolling = true; } } final boolean dispatchSelected = mCurItem != item; if (mFirstLayout) { // We don't have any idea how big we are yet and shouldn't have any pages either. // Just set things up and let the pending layout handle things. mCurItem = item; if (dispatchSelected && mOnPageChangeListener != null) { mOnPageChangeListener.onPageSelected(item); } if (dispatchSelected && mInternalPageChangeListener != null) { mInternalPageChangeListener.onPageSelected(item); } requestLayout(); } else { populate(item); scrollToItem(item, smoothScroll, velocity, dispatchSelected); } return true; } private void scrollToItem(int position, boolean smoothScroll, int velocity, boolean dispatchSelected) { final int destX = getLeftEdgeForItem(position); if (smoothScroll) { smoothScrollTo(destX, 0, velocity); if (dispatchSelected && mOnPageChangeListener != null) { mOnPageChangeListener.onPageSelected(position); } if (dispatchSelected && mInternalPageChangeListener != null) { mInternalPageChangeListener.onPageSelected(position); } } else { if (dispatchSelected && mOnPageChangeListener != null) { mOnPageChangeListener.onPageSelected(position); } if (dispatchSelected && mInternalPageChangeListener != null) { mInternalPageChangeListener.onPageSelected(position); } completeScroll(false); scrollTo(destX, 0); pageScrolled(destX); } } private int getLeftEdgeForItem(int position) { final ItemInfo info = infoForPosition(position); if (info == null) { return 0; } final int width = getPaddedWidth(); final int scaledOffset = (int) (width * MathUtils.constrain( info.offset, mFirstOffset, mLastOffset)); if (isLayoutRtl()) { final int itemWidth = (int) (width * info.widthFactor + 0.5f); return MAX_SCROLL_X - itemWidth - scaledOffset; } else { return scaledOffset; } } /** * Set a listener that will be invoked whenever the page changes or is incrementally * scrolled. See {@link OnPageChangeListener}. * * @param listener Listener to set */ public void setOnPageChangeListener(OnPageChangeListener listener) { mOnPageChangeListener = listener; } /** * Set a {@link PageTransformer} that will be called for each attached page whenever * the scroll position is changed. This allows the application to apply custom property * transformations to each page, overriding the default sliding look and feel. * *

Note: Prior to Android 3.0 the property animation APIs did not exist. * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.

* * @param reverseDrawingOrder true if the supplied PageTransformer requires page views * to be drawn from last to first instead of first to last. * @param transformer PageTransformer that will modify each page's animation properties */ public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) { final boolean hasTransformer = transformer != null; final boolean needsPopulate = hasTransformer != (mPageTransformer != null); mPageTransformer = transformer; setChildrenDrawingOrderEnabled(hasTransformer); if (hasTransformer) { mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; } else { mDrawingOrder = DRAW_ORDER_DEFAULT; } if (needsPopulate) populate(); } @Override protected int getChildDrawingOrder(int childCount, int i) { final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; return result; } /** * Set a separate OnPageChangeListener for internal use by the support library. * * @param listener Listener to set * @return The old listener that was set, if any. */ OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) { OnPageChangeListener oldListener = mInternalPageChangeListener; mInternalPageChangeListener = listener; return oldListener; } /** * Returns the number of pages that will be retained to either side of the * current page in the view hierarchy in an idle state. Defaults to 1. * * @return How many pages will be kept offscreen on either side * @see #setOffscreenPageLimit(int) */ public int getOffscreenPageLimit() { return mOffscreenPageLimit; } /** * Set the number of pages that should be retained to either side of the * current page in the view hierarchy in an idle state. Pages beyond this * limit will be recreated from the adapter when needed. * *

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(android.graphics.drawable.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(@DrawableRes int resId) { setPageMarginDrawable(getContext().getDrawable(resId)); } @Override protected boolean verifyDrawable(@NonNull Drawable who) { return super.verifyDrawable(who) || who == mMarginDrawable; } @Override protected void drawableStateChanged() { super.drawableStateChanged(); final Drawable marginDrawable = mMarginDrawable; if (marginDrawable != null && marginDrawable.isStateful() && marginDrawable.setState(getDrawableState())) { invalidateDrawable(marginDrawable); } } // 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 android.view.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 android.view.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(false); populate(); setScrollState(SCROLL_STATE_IDLE); return; } setScrollingCacheEnabled(true); setScrollState(SCROLL_STATE_SETTLING); final int width = getPaddedWidth(); final int halfWidth = width / 2; final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio); int duration = 0; velocity = Math.abs(velocity); if (velocity > 0) { duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); } else { final float pageWidth = width * mAdapter.getPageWidth(mCurItem); final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); duration = (int) ((pageDelta + 1) * 100); } duration = Math.min(duration, MAX_SETTLE_DURATION); mScroller.startScroll(sx, sy, dx, dy, duration); postInvalidateOnAnimation(); } ItemInfo addNewItem(int position, int index) { ItemInfo ii = new ItemInfo(); ii.position = position; ii.object = mAdapter.instantiateItem(this, position); ii.widthFactor = mAdapter.getPageWidth(position); if (index < 0 || index >= mItems.size()) { mItems.add(ii); } else { mItems.add(index, ii); } return ii; } void dataSetChanged() { // This method only gets called if our observer is attached, so mAdapter is non-null. final int adapterCount = mAdapter.getCount(); mExpectedAdapterCount = adapterCount; boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && mItems.size() < adapterCount; int newCurrItem = mCurItem; boolean isUpdating = false; 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--; if (!isUpdating) { mAdapter.startUpdate(this); isUpdating = true; } 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, adapterCount - 1)); needPopulate = true; } continue; } if (ii.position != newPos) { if (ii.position == mCurItem) { // Our current item changed position. Follow it. newCurrItem = newPos; } ii.position = newPos; needPopulate = true; } } if (isUpdating) { mAdapter.finishUpdate(this); } Collections.sort(mItems, COMPARATOR); if (needPopulate) { // Reset our known page widths; populate will recompute them. final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.isDecor) { lp.widthFactor = 0.f; } } setCurrentItemInternal(newCurrItem, false, true); requestLayout(); } } public void populate() { populate(mCurItem); } void populate(int newCurrentItem) { ItemInfo oldCurInfo = null; int focusDirection = View.FOCUS_FORWARD; if (mCurItem != newCurrentItem) { focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT; oldCurInfo = infoForPosition(mCurItem); mCurItem = newCurrentItem; } if (mAdapter == null) { sortChildDrawingOrder(); 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..."); sortChildDrawingOrder(); 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 (N != mExpectedAdapterCount) { String resName; try { resName = getResources().getResourceName(getId()); } catch (Resources.NotFoundException e) { resName = Integer.toHexString(getId()); } throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + " contents without calling PagerAdapter#notifyDataSetChanged!" + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + " Pager id: " + resName + " Pager class: " + getClass() + " Problematic adapter: " + mAdapter.getClass()); } // Locate the currently focused item or add it if needed. int curIndex = -1; ItemInfo curItem = null; for (curIndex = 0; curIndex < mItems.size(); curIndex++) { final ItemInfo ii = mItems.get(curIndex); if (ii.position >= mCurItem) { if (ii.position == mCurItem) curItem = ii; break; } } if (curItem == null && N > 0) { curItem = addNewItem(mCurItem, curIndex); } // Fill 3x the available width or up to the number of offscreen // pages requested to either side, whichever is larger. // If we have no current item we have no work to do. if (curItem != null) { float extraWidthLeft = 0.f; int itemIndex = curIndex - 1; ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; final int clientWidth = getPaddedWidth(); final float leftWidthNeeded = clientWidth <= 0 ? 0 : 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; for (int pos = mCurItem - 1; pos >= 0; pos--) { if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { if (ii == null) { break; } if (pos == ii.position && !ii.scrolling) { mItems.remove(itemIndex); mAdapter.destroyItem(this, pos, ii.object); if (DEBUG) { Log.i(TAG, "populate() - destroyItem() with pos: " + pos + " view: " + ii.object); } itemIndex--; curIndex--; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } } else if (ii != null && pos == ii.position) { extraWidthLeft += ii.widthFactor; itemIndex--; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } else { ii = addNewItem(pos, itemIndex + 1); extraWidthLeft += ii.widthFactor; curIndex++; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } } float extraWidthRight = curItem.widthFactor; itemIndex = curIndex + 1; if (extraWidthRight < 2.f) { ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; final float rightWidthNeeded = clientWidth <= 0 ? 0 : (float) getPaddingRight() / (float) clientWidth + 2.f; for (int pos = mCurItem + 1; pos < N; pos++) { if (extraWidthRight >= rightWidthNeeded && pos > endPos) { if (ii == null) { break; } if (pos == ii.position && !ii.scrolling) { mItems.remove(itemIndex); mAdapter.destroyItem(this, pos, ii.object); if (DEBUG) { Log.i(TAG, "populate() - destroyItem() with pos: " + pos + " view: " + ii.object); } ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; } } else if (ii != null && pos == ii.position) { extraWidthRight += ii.widthFactor; itemIndex++; ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; } else { ii = addNewItem(pos, itemIndex); itemIndex++; extraWidthRight += ii.widthFactor; ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; } } } calculatePageOffsets(curItem, curIndex, oldCurInfo); } if (DEBUG) { Log.i(TAG, "Current page list:"); for (int i=0; i(); } else { mDrawingOrderedChildren.clear(); } final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); mDrawingOrderedChildren.add(child); } Collections.sort(mDrawingOrderedChildren, sPositionComparator); } } private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { final int N = mAdapter.getCount(); final int width = getPaddedWidth(); final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; // Fix up offsets for later layout. if (oldCurInfo != null) { final int oldCurPosition = oldCurInfo.position; // Base offsets off of oldCurInfo. if (oldCurPosition < curItem.position) { int itemIndex = 0; float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; for (int pos = oldCurPosition + 1; pos <= curItem.position && itemIndex < mItems.size(); pos++) { ItemInfo ii = mItems.get(itemIndex); while (pos > ii.position && itemIndex < mItems.size() - 1) { itemIndex++; ii = mItems.get(itemIndex); } while (pos < ii.position) { // We don't have an item populated for this, // ask the adapter for an offset. offset += mAdapter.getPageWidth(pos) + marginOffset; pos++; } ii.offset = offset; offset += ii.widthFactor + marginOffset; } } else if (oldCurPosition > curItem.position) { int itemIndex = mItems.size() - 1; float offset = oldCurInfo.offset; for (int pos = oldCurPosition - 1; pos >= curItem.position && itemIndex >= 0; pos--) { ItemInfo ii = mItems.get(itemIndex); while (pos < ii.position && itemIndex > 0) { itemIndex--; ii = mItems.get(itemIndex); } while (pos > ii.position) { // We don't have an item populated for this, // ask the adapter for an offset. offset -= mAdapter.getPageWidth(pos) + marginOffset; pos--; } offset -= ii.widthFactor + marginOffset; ii.offset = offset; } } } // Base all offsets off of curItem. final int itemCount = mItems.size(); float offset = curItem.offset; int pos = curItem.position - 1; mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; mLastOffset = curItem.position == N - 1 ? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; // Previous pages for (int i = curIndex - 1; i >= 0; i--, pos--) { final ItemInfo ii = mItems.get(i); while (pos > ii.position) { offset -= mAdapter.getPageWidth(pos--) + marginOffset; } offset -= ii.widthFactor + marginOffset; ii.offset = offset; if (ii.position == 0) mFirstOffset = offset; } offset = curItem.offset + curItem.widthFactor + marginOffset; pos = curItem.position + 1; // Next pages for (int i = curIndex + 1; i < itemCount; i++, pos++) { final ItemInfo ii = mItems.get(i); while (pos < ii.position) { offset += mAdapter.getPageWidth(pos++) + marginOffset; } if (ii.position == N - 1) { mLastOffset = offset + ii.widthFactor - 1; } ii.offset = offset; offset += ii.widthFactor + marginOffset; } } /** * This is the persistent state that is saved by ViewPager. Only needed * if you are creating a sublass of ViewPager that must save its own * state, in which case it should implement a subclass of this which * contains that state. */ public static class SavedState extends BaseSavedState { int position; Parcelable adapterState; ClassLoader loader; public SavedState(Parcel source) { super(source); } 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 Creator CREATOR = new Creator() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; SavedState(Parcel in, ClassLoader loader) { super(in); if (loader == null) { loader = getClass().getClassLoader(); } position = in.readInt(); adapterState = in.readParcelable(loader); this.loader = loader; } } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.position = mCurItem; if (mAdapter != null) { ss.adapterState = mAdapter.saveState(); } return ss; } @Override public void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState ss = (SavedState)state; super.onRestoreInstanceState(ss.getSuperState()); if (mAdapter != null) { mAdapter.restoreState(ss.adapterState, ss.loader); setCurrentItemInternal(ss.position, false, true); } else { mRestoredCurItem = ss.position; mRestoredAdapterState = ss.adapterState; mRestoredClassLoader = ss.loader; } } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (!checkLayoutParams(params)) { params = generateLayoutParams(params); } final LayoutParams lp = (LayoutParams) params; lp.isDecor |= child instanceof Decor; if (mInLayout) { if (lp != null && lp.isDecor) { throw new IllegalStateException("Cannot add pager decor view during layout"); } lp.needsMeasure = true; addViewInLayout(child, index, params); } else { super.addView(child, index, params); } if (USE_CACHE) { if (child.getVisibility() != GONE) { child.setDrawingCacheEnabled(mScrollingCacheEnabled); } else { child.setDrawingCacheEnabled(false); } } } public Object getCurrent() { final ItemInfo itemInfo = infoForPosition(getCurrentItem()); return itemInfo == null ? null : itemInfo.object; } @Override public void removeView(View view) { if (mInLayout) { removeViewInLayout(view); } else { super.removeView(view); } } ItemInfo infoForChild(View child) { for (int i=0; i 0 && !mItems.isEmpty()) { final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin; final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight() + oldMargin; final int xpos = getScrollX(); final float pageOffset = (float) xpos / oldWidthWithMargin; final int newOffsetPixels = (int) (pageOffset * widthWithMargin); scrollTo(newOffsetPixels, getScrollY()); if (!mScroller.isFinished()) { // We now return to your regularly scheduled scroll, already in progress. final int newDuration = mScroller.getDuration() - mScroller.timePassed(); ItemInfo targetInfo = infoForPosition(mCurItem); mScroller.startScroll(newOffsetPixels, 0, (int) (targetInfo.offset * width), 0, newDuration); } } else { final ItemInfo ii = infoForPosition(mCurItem); final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; final int scrollPos = (int) (scrollOffset * (width - getPaddingLeft() - getPaddingRight())); if (scrollPos != getScrollX()) { completeScroll(false); scrollTo(scrollPos, getScrollY()); } } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); int width = r - l; int height = b - t; int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int paddingRight = getPaddingRight(); int paddingBottom = getPaddingBottom(); final int scrollX = getScrollX(); int decorCount = 0; // First pass - decor views. We need to do this in two passes so that // we have the proper offsets for non-decor views later. for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childLeft = 0; int childTop = 0; if (lp.isDecor) { final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; switch (hgrav) { default: childLeft = paddingLeft; break; case Gravity.LEFT: childLeft = paddingLeft; paddingLeft += child.getMeasuredWidth(); break; case Gravity.CENTER_HORIZONTAL: childLeft = Math.max((width - child.getMeasuredWidth()) / 2, paddingLeft); break; case Gravity.RIGHT: childLeft = width - paddingRight - child.getMeasuredWidth(); paddingRight += child.getMeasuredWidth(); break; } switch (vgrav) { default: childTop = paddingTop; break; case Gravity.TOP: childTop = paddingTop; paddingTop += child.getMeasuredHeight(); break; case Gravity.CENTER_VERTICAL: childTop = Math.max((height - child.getMeasuredHeight()) / 2, paddingTop); break; case Gravity.BOTTOM: childTop = height - paddingBottom - child.getMeasuredHeight(); paddingBottom += child.getMeasuredHeight(); break; } childLeft += scrollX; child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight()); decorCount++; } } } final int childWidth = width - paddingLeft - paddingRight; // Page views. Do this once we have the right padding offsets from above. for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp.isDecor) { continue; } final ItemInfo ii = infoForChild(child); if (ii == null) { continue; } if (lp.needsMeasure) { // This was added during layout and needs measurement. // Do it now that we know what we're working with. lp.needsMeasure = false; final int widthSpec = MeasureSpec.makeMeasureSpec( (int) (childWidth * lp.widthFactor), MeasureSpec.EXACTLY); final int heightSpec = MeasureSpec.makeMeasureSpec( (int) (height - paddingTop - paddingBottom), MeasureSpec.EXACTLY); child.measure(widthSpec, heightSpec); } final int childMeasuredWidth = child.getMeasuredWidth(); final int startOffset = (int) (childWidth * ii.offset); final int childLeft; if (isLayoutRtl()) { childLeft = MAX_SCROLL_X - paddingRight - startOffset - childMeasuredWidth; } else { childLeft = paddingLeft + startOffset; } final int childTop = paddingTop; child.layout(childLeft, childTop, childLeft + childMeasuredWidth, childTop + child.getMeasuredHeight()); } mTopPageBounds = paddingTop; mBottomPageBounds = height - paddingBottom; mDecorChildCount = decorCount; if (mFirstLayout) { scrollToItem(mCurItem, false, 0, false); } mFirstLayout = false; } @Override public void computeScroll() { if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { final int oldX = getScrollX(); final int oldY = getScrollY(); final int x = mScroller.getCurrX(); final int y = mScroller.getCurrY(); if (oldX != x || oldY != y) { scrollTo(x, y); if (!pageScrolled(x)) { mScroller.abortAnimation(); scrollTo(0, y); } } // Keep on drawing until the animation has finished. postInvalidateOnAnimation(); return; } // Done with scroll, clean up state. completeScroll(true); } private boolean pageScrolled(int scrollX) { if (mItems.size() == 0) { mCalledSuper = false; onPageScrolled(0, 0, 0); if (!mCalledSuper) { throw new IllegalStateException( "onPageScrolled did not call superclass implementation"); } return false; } // Translate to scrollX to scrollStart for RTL. final int scrollStart; if (isLayoutRtl()) { scrollStart = MAX_SCROLL_X - scrollX; } else { scrollStart = scrollX; } final ItemInfo ii = infoForFirstVisiblePage(); final int width = getPaddedWidth(); final int widthWithMargin = width + mPageMargin; final float marginOffset = (float) mPageMargin / width; final int currentPage = ii.position; final float pageOffset = (((float) scrollStart / width) - ii.offset) / (ii.widthFactor + marginOffset); final int offsetPixels = (int) (pageOffset * widthWithMargin); mCalledSuper = false; onPageScrolled(currentPage, pageOffset, offsetPixels); if (!mCalledSuper) { throw new IllegalStateException( "onPageScrolled did not call superclass implementation"); } return true; } /** * This method will be invoked when the current page is scrolled, either as part * of a programmatically initiated smooth scroll or a user initiated touch scroll. * If you override this method you must call through to the superclass implementation * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled * returns. * * @param position Position index of the first page currently being displayed. * Page position+1 will be visible if positionOffset is nonzero. * @param offset Value from [0, 1) indicating the offset from the page at position. * @param offsetPixels Value in pixels indicating the offset from position. */ protected void onPageScrolled(int position, float offset, int offsetPixels) { // Offset any decor views if needed - keep them on-screen at all times. if (mDecorChildCount > 0) { final int scrollX = getScrollX(); int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); final int width = getWidth(); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.isDecor) continue; final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; int childLeft = 0; switch (hgrav) { default: childLeft = paddingLeft; break; case Gravity.LEFT: childLeft = paddingLeft; paddingLeft += child.getWidth(); break; case Gravity.CENTER_HORIZONTAL: childLeft = Math.max((width - child.getMeasuredWidth()) / 2, paddingLeft); break; case Gravity.RIGHT: childLeft = width - paddingRight - child.getMeasuredWidth(); paddingRight += child.getMeasuredWidth(); break; } childLeft += scrollX; final int childOffset = childLeft - child.getLeft(); if (childOffset != 0) { child.offsetLeftAndRight(childOffset); } } } if (mOnPageChangeListener != null) { mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); } if (mInternalPageChangeListener != null) { mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); } if (mPageTransformer != null) { final int scrollX = getScrollX(); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp.isDecor) continue; final float transformPos = (float) (child.getLeft() - scrollX) / getPaddedWidth(); mPageTransformer.transformPage(child, transformPos); } } mCalledSuper = true; } private void completeScroll(boolean postEvents) { boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; if (needPopulate) { // Done with scroll, no longer want to cache view drawing. setScrollingCacheEnabled(false); mScroller.abortAnimation(); int oldX = getScrollX(); int oldY = getScrollY(); int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); if (oldX != x || oldY != y) { scrollTo(x, y); } } mPopulatePending = false; for (int i=0; i 0) || (x > getWidth() - mGutterSize && dx < 0); } private void enableLayers(boolean enable) { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final int layerType = enable ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE; getChildAt(i).setLayerType(layerType, null); } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { /* * This method JUST determines whether we want to intercept the motion. * If we return true, onMotionEvent will be called and we do the actual * scrolling there. */ final int action = ev.getAction() & MotionEvent.ACTION_MASK; // Always take care of the touch gesture being complete. if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // Release the drag. if (DEBUG) Log.v(TAG, "Intercept done!"); mIsBeingDragged = false; mIsUnableToDrag = false; mActivePointerId = INVALID_POINTER; if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } return false; } // Nothing more to do here if we have decided whether or not we // are dragging. if (action != MotionEvent.ACTION_DOWN) { if (mIsBeingDragged) { if (DEBUG) Log.v(TAG, "Being dragged, intercept returning true!"); return true; } if (mIsUnableToDrag) { if (DEBUG) Log.v(TAG, "Unable to drag, intercept returning false!"); return false; } } switch (action) { case MotionEvent.ACTION_MOVE: { /* * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check * whether the user has moved far enough from his original down touch. */ /* * Locally do absolute value. mLastMotionY is set to the y value * of the down event. */ final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. break; } final int pointerIndex = ev.findPointerIndex(activePointerId); final float x = ev.getX(pointerIndex); final float dx = x - mLastMotionX; final float xDiff = Math.abs(dx); final float y = ev.getY(pointerIndex); final float yDiff = Math.abs(y - mInitialMotionY); if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && canScroll(this, false, (int) dx, (int) x, (int) y)) { // Nested view has scrollable area under this point. Let it be handled there. mLastMotionX = x; mLastMotionY = y; mIsUnableToDrag = true; return false; } if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; setScrollingCacheEnabled(true); } else if (yDiff > mTouchSlop) { // The finger has moved enough in the vertical // direction to be counted as a drag... abort // any attempt to drag horizontally, to work correctly // with children that have scrolling containers. if (DEBUG) Log.v(TAG, "Starting unable to drag!"); mIsUnableToDrag = true; } if (mIsBeingDragged) { // Scroll to follow the motion event if (performDrag(x)) { postInvalidateOnAnimation(); } } break; } case MotionEvent.ACTION_DOWN: { /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); mActivePointerId = ev.getPointerId(0); mIsUnableToDrag = false; mScroller.computeScrollOffset(); if (mScrollState == SCROLL_STATE_SETTLING && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { // Let the user 'catch' the pager as it animates. mScroller.abortAnimation(); mPopulatePending = false; populate(); mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); } else { completeScroll(false); mIsBeingDragged = false; } if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY + " mIsBeingDragged=" + mIsBeingDragged + "mIsUnableToDrag=" + mIsUnableToDrag); break; } case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); /* * The only time we want to intercept motion events is if we are in the * drag mode. */ return mIsBeingDragged; } @Override public boolean onTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { // Don't handle edge touches immediately -- they may actually belong to one of our // descendants. return false; } if (mAdapter == null || mAdapter.getCount() == 0) { // Nothing to present or scroll; nothing to touch. return false; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); final int action = ev.getAction(); boolean needsInvalidate = false; switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { mScroller.abortAnimation(); mPopulatePending = false; populate(); // Remember where the motion event started mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); mActivePointerId = ev.getPointerId(0); break; } case MotionEvent.ACTION_MOVE: if (!mIsBeingDragged) { final int pointerIndex = ev.findPointerIndex(mActivePointerId); final float x = ev.getX(pointerIndex); final float xDiff = Math.abs(x - mLastMotionX); final float y = ev.getY(pointerIndex); final float yDiff = Math.abs(y - mLastMotionY); if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); if (xDiff > mTouchSlop && xDiff > yDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; setScrollState(SCROLL_STATE_DRAGGING); setScrollingCacheEnabled(true); // Disallow Parent Intercept, just in case ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } } // Not else! Note that mIsBeingDragged can be set above. if (mIsBeingDragged) { // Scroll to follow the motion event final int activePointerIndex = ev.findPointerIndex(mActivePointerId); final float x = ev.getX(activePointerIndex); needsInvalidate |= performDrag(x); } break; case MotionEvent.ACTION_UP: if (mIsBeingDragged) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); final int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); mPopulatePending = true; final float scrollStart = getScrollStart(); final float scrolledPages = scrollStart / getPaddedWidth(); final ItemInfo ii = infoForFirstVisiblePage(); final int currentPage = ii.position; final float nextPageOffset; if (isLayoutRtl()) { nextPageOffset = (ii.offset - scrolledPages) / ii.widthFactor; } else { nextPageOffset = (scrolledPages - ii.offset) / ii.widthFactor; } final int activePointerIndex = ev.findPointerIndex(mActivePointerId); final float x = ev.getX(activePointerIndex); final int totalDelta = (int) (x - mInitialMotionX); final int nextPage = determineTargetPage( currentPage, nextPageOffset, initialVelocity, totalDelta); setCurrentItemInternal(nextPage, true, true, initialVelocity); mActivePointerId = INVALID_POINTER; endDrag(); mLeftEdge.onRelease(); mRightEdge.onRelease(); needsInvalidate = true; } break; case MotionEvent.ACTION_CANCEL: if (mIsBeingDragged) { scrollToItem(mCurItem, true, 0, false); mActivePointerId = INVALID_POINTER; endDrag(); mLeftEdge.onRelease(); mRightEdge.onRelease(); needsInvalidate = true; } break; case MotionEvent.ACTION_POINTER_DOWN: { final int index = ev.getActionIndex(); final float x = ev.getX(index); mLastMotionX = x; mActivePointerId = ev.getPointerId(index); break; } case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId)); break; } if (needsInvalidate) { postInvalidateOnAnimation(); } return true; } private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(disallowIntercept); } } private boolean performDrag(float x) { boolean needsInvalidate = false; final int width = getPaddedWidth(); final float deltaX = mLastMotionX - x; mLastMotionX = x; final EdgeEffect startEdge; final EdgeEffect endEdge; if (isLayoutRtl()) { startEdge = mRightEdge; endEdge = mLeftEdge; } else { startEdge = mLeftEdge; endEdge = mRightEdge; } // Translate scroll to relative coordinates. final float nextScrollX = getScrollX() + deltaX; final float scrollStart; if (isLayoutRtl()) { scrollStart = MAX_SCROLL_X - nextScrollX; } else { scrollStart = nextScrollX; } final float startBound; final ItemInfo startItem = mItems.get(0); final boolean startAbsolute = startItem.position == 0; if (startAbsolute) { startBound = startItem.offset * width; } else { startBound = width * mFirstOffset; } final float endBound; final ItemInfo endItem = mItems.get(mItems.size() - 1); final boolean endAbsolute = endItem.position == mAdapter.getCount() - 1; if (endAbsolute) { endBound = endItem.offset * width; } else { endBound = width * mLastOffset; } final float clampedScrollStart; if (scrollStart < startBound) { if (startAbsolute) { final float over = startBound - scrollStart; startEdge.onPull(Math.abs(over) / width); needsInvalidate = true; } clampedScrollStart = startBound; } else if (scrollStart > endBound) { if (endAbsolute) { final float over = scrollStart - endBound; endEdge.onPull(Math.abs(over) / width); needsInvalidate = true; } clampedScrollStart = endBound; } else { clampedScrollStart = scrollStart; } // Translate back to absolute coordinates. final float targetScrollX; if (isLayoutRtl()) { targetScrollX = MAX_SCROLL_X - clampedScrollStart; } else { targetScrollX = clampedScrollStart; } // Don't lose the rounded component. mLastMotionX += targetScrollX - (int) targetScrollX; scrollTo((int) targetScrollX, getScrollY()); pageScrolled((int) targetScrollX); return needsInvalidate; } /** * @return Info about the page at the current scroll position. * This can be synthetic for a missing middle page; the 'object' field can be null. */ private ItemInfo infoForFirstVisiblePage() { final int startOffset = getScrollStart(); final int width = getPaddedWidth(); final float scrollOffset = width > 0 ? (float) startOffset / width : 0; final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; int lastPos = -1; float lastOffset = 0.f; float lastWidth = 0.f; boolean first = true; ItemInfo lastItem = null; final int N = mItems.size(); for (int i = 0; i < N; i++) { ItemInfo ii = mItems.get(i); // Seek to position. if (!first && ii.position != lastPos + 1) { // Create a synthetic item for a missing page. ii = mTempItem; ii.offset = lastOffset + lastWidth + marginOffset; ii.position = lastPos + 1; ii.widthFactor = mAdapter.getPageWidth(ii.position); i--; } final float offset = ii.offset; final float startBound = offset; if (first || scrollOffset >= startBound) { final float endBound = offset + ii.widthFactor + marginOffset; if (scrollOffset < endBound || i == mItems.size() - 1) { return ii; } } else { return lastItem; } first = false; lastPos = ii.position; lastOffset = offset; lastWidth = ii.widthFactor; lastItem = ii; } return lastItem; } private int getScrollStart() { if (isLayoutRtl()) { return MAX_SCROLL_X - getScrollX(); } else { return getScrollX(); } } /** * @param currentPage the position of the page with the first visible starting edge * @param pageOffset the fraction of the right-hand page that's visible * @param velocity the velocity of the touch event stream * @param deltaX the distance of the touch event stream * @return the position of the target page */ private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { int targetPage; if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { targetPage = currentPage - (velocity < 0 ? mLeftIncr : 0); } else { final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; targetPage = (int) (currentPage - mLeftIncr * (pageOffset + truncator)); } if (mItems.size() > 0) { final ItemInfo firstItem = mItems.get(0); final ItemInfo lastItem = mItems.get(mItems.size() - 1); // Only let the user target pages we have items for targetPage = MathUtils.constrain(targetPage, firstItem.position, lastItem.position); } return targetPage; } @Override public void draw(Canvas canvas) { super.draw(canvas); boolean needsInvalidate = false; final int overScrollMode = getOverScrollMode(); if (overScrollMode == View.OVER_SCROLL_ALWAYS || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && mAdapter != null && mAdapter.getCount() > 1)) { if (!mLeftEdge.isFinished()) { final int restoreCount = canvas.save(); final int height = getHeight() - getPaddingTop() - getPaddingBottom(); final int width = getWidth(); canvas.rotate(270); canvas.translate(-height + getPaddingTop(), mFirstOffset * width); mLeftEdge.setSize(height, width); needsInvalidate |= mLeftEdge.draw(canvas); canvas.restoreToCount(restoreCount); } if (!mRightEdge.isFinished()) { final int restoreCount = canvas.save(); final int width = getWidth(); final int height = getHeight() - getPaddingTop() - getPaddingBottom(); canvas.rotate(90); canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); mRightEdge.setSize(height, width); needsInvalidate |= mRightEdge.draw(canvas); canvas.restoreToCount(restoreCount); } } else { mLeftEdge.finish(); mRightEdge.finish(); } if (needsInvalidate) { // Keep animating postInvalidateOnAnimation(); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Draw the margin drawable between pages if needed. if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { final int scrollX = getScrollX(); final int width = getWidth(); final float marginOffset = (float) mPageMargin / width; int itemIndex = 0; ItemInfo ii = mItems.get(0); float offset = ii.offset; final int itemCount = mItems.size(); final int firstPos = ii.position; final int lastPos = mItems.get(itemCount - 1).position; for (int pos = firstPos; pos < lastPos; pos++) { while (pos > ii.position && itemIndex < itemCount) { ii = mItems.get(++itemIndex); } final float itemOffset; final float widthFactor; if (pos == ii.position) { itemOffset = ii.offset; widthFactor = ii.widthFactor; } else { itemOffset = offset; widthFactor = mAdapter.getPageWidth(pos); } final float left; final float scaledOffset = itemOffset * width; if (isLayoutRtl()) { left = MAX_SCROLL_X - scaledOffset; } else { left = scaledOffset + widthFactor * width; } offset = itemOffset + widthFactor + marginOffset; if (left + mPageMargin > scrollX) { mMarginDrawable.setBounds((int) left, mTopPageBounds, (int) (left + mPageMargin + 0.5f), mBottomPageBounds); mMarginDrawable.draw(canvas); } if (left > scrollX + width) { break; // No more visible, no sense in continuing } } } } private void onSecondaryPointerUp(MotionEvent ev) { final int pointerIndex = ev.getActionIndex(); final int pointerId = ev.getPointerId(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 = ev.getX(newPointerIndex); mActivePointerId = ev.getPointerId(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); } } } } } public boolean canScrollHorizontally(int direction) { if (mAdapter == null) { return false; } final int width = getPaddedWidth(); final int scrollX = getScrollX(); if (direction < 0) { return (scrollX > (int) (width * mFirstOffset)); } else if (direction > 0) { return (scrollX < (int) (width * mLastOffset)); } else { return false; } } /** * 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 support for transformed views. 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 && v.canScrollHorizontally(-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 (event.hasNoModifiers()) { handled = arrowScroll(FOCUS_FORWARD); } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { handled = arrowScroll(FOCUS_BACKWARD); } break; } } return handled; } public boolean arrowScroll(int direction) { View currentFocused = findFocus(); if (currentFocused == this) { currentFocused = null; } else if (currentFocused != null) { boolean isChild = false; for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; parent = parent.getParent()) { if (parent == this) { isChild = true; break; } } if (!isChild) { // This would cause the focus search down below to fail in fun ways. final StringBuilder sb = new StringBuilder(); sb.append(currentFocused.getClass().getSimpleName()); for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; parent = parent.getParent()) { sb.append(" => ").append(parent.getClass().getSimpleName()); } Log.e(TAG, "arrowScroll tried to find focus based on non-child " + "current focused view " + sb.toString()); 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. final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; if (currentFocused != null && nextLeft >= currLeft) { 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. final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; if (currentFocused != null && nextLeft <= currLeft) { 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; } private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { if (outRect == null) { outRect = new Rect(); } if (child == null) { outRect.set(0, 0, 0, 0); return outRect; } outRect.left = child.getLeft(); outRect.right = child.getRight(); outRect.top = child.getTop(); outRect.bottom = child.getBottom(); ViewParent parent = child.getParent(); while (parent instanceof ViewGroup && parent != this) { final ViewGroup group = (ViewGroup) parent; outRect.left += group.getLeft(); outRect.right += group.getRight(); outRect.top += group.getTop(); outRect.bottom += group.getBottom(); parent = group.getParent(); } return outRect; } boolean pageLeft() { return setCurrentItemInternal(mCurItem + mLeftIncr, true, false); } boolean pageRight() { return setCurrentItemInternal(mCurItem - mLeftIncr, true, false); } @Override public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); if (layoutDirection == LAYOUT_DIRECTION_LTR) { mLeftIncr = -1; } else { mLeftIncr = 1; } } /** * We only want the current page that is being shown to be focusable. */ @Override public void addFocusables(ArrayList views, int direction, int focusableMode) { final int focusableCount = views.size(); final int descendantFocusability = getDescendantFocusability(); if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); if (child.getVisibility() == VISIBLE) { ItemInfo ii = infoForChild(child); if (ii != null && ii.position == mCurItem) { child.addFocusables(views, direction, focusableMode); } } } } // we add ourselves (if focusable) in all cases except for when we are // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is // to avoid the focus search finding layouts when a more precise search // among the focusable children would be more interesting. if ( descendantFocusability != FOCUS_AFTER_DESCENDANTS || // No focusable descendants (focusableCount == views.size())) { // Note that we can't call the superclass here, because it will // add all views in. So we need to do the same thing View does. if (!isFocusable()) { return; } if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && isInTouchMode() && !isFocusableInTouchMode()) { return; } if (views != null) { views.add(this); } } } /** * We only want the current page that is being shown to be touchable. */ @Override public void addTouchables(ArrayList views) { // Note that we don't call super.addTouchables(), which means that // we don't call View.addTouchables(). This is okay because a ViewPager // is itself not touchable. for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); if (child.getVisibility() == VISIBLE) { ItemInfo ii = infoForChild(child); if (ii != null && ii.position == mCurItem) { child.addTouchables(views); } } } } /** * We only want the current page that is being shown to be focusable. */ @Override protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { int index; int increment; int end; int count = getChildCount(); if ((direction & FOCUS_FORWARD) != 0) { index = 0; increment = 1; end = count; } else { index = count - 1; increment = -1; end = -1; } for (int i = index; i != end; i += increment) { View child = getChildAt(i); if (child.getVisibility() == VISIBLE) { ItemInfo ii = infoForChild(child); if (ii != null && ii.position == mCurItem) { if (child.requestFocus(direction, previouslyFocusedRect)) { return true; } } } } return false; } @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new LayoutParams(); } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return generateDefaultLayoutParams(); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams && super.checkLayoutParams(p); } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); event.setClassName(ViewPager.class.getName()); event.setScrollable(canScroll()); if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED && mAdapter != null) { event.setItemCount(mAdapter.getCount()); event.setFromIndex(mCurItem); event.setToIndex(mCurItem); } } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(ViewPager.class.getName()); info.setScrollable(canScroll()); if (canScrollHorizontally(1)) { info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD); info.addAction(AccessibilityAction.ACTION_SCROLL_RIGHT); } if (canScrollHorizontally(-1)) { info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD); info.addAction(AccessibilityAction.ACTION_SCROLL_LEFT); } } @Override public boolean performAccessibilityAction(int action, Bundle args) { if (super.performAccessibilityAction(action, args)) { return true; } switch (action) { case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: case R.id.accessibilityActionScrollRight: if (canScrollHorizontally(1)) { setCurrentItem(mCurItem + 1); return true; } return false; case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: case R.id.accessibilityActionScrollLeft: if (canScrollHorizontally(-1)) { setCurrentItem(mCurItem - 1); return true; } return false; } return false; } private boolean canScroll() { return mAdapter != null && mAdapter.getCount() > 1; } private class PagerObserver extends DataSetObserver { @Override public void onChanged() { dataSetChanged(); } @Override public void onInvalidated() { dataSetChanged(); } } /** * Layout parameters that should be supplied for views added to a * ViewPager. */ public static class LayoutParams extends ViewGroup.LayoutParams { /** * true if this view is a decoration on the pager itself and not * a view supplied by the adapter. */ public boolean isDecor; /** * Gravity setting for use on decor views only: * Where to position the view page within the overall ViewPager * container; constants are defined in {@link android.view.Gravity}. */ public int gravity; /** * Width as a 0-1 multiplier of the measured pager width */ float widthFactor = 0.f; /** * true if this view was added during layout and needs to be measured * before being positioned. */ boolean needsMeasure; /** * Adapter position this view is for if !isDecor */ int position; /** * Current child index within the ViewPager that this view occupies */ int childIndex; public LayoutParams() { super(FILL_PARENT, FILL_PARENT); } public LayoutParams(Context context, AttributeSet attrs) { super(context, attrs); final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); gravity = a.getInteger(0, Gravity.TOP); a.recycle(); } } static class ViewPositionComparator implements Comparator { @Override public int compare(View lhs, View rhs) { final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); if (llp.isDecor != rlp.isDecor) { return llp.isDecor ? 1 : -1; } return llp.position - rlp.position; } } }