Created
August 9, 2017 06:41
-
-
Save anonymous/24831ea667540ab6a43cc777b7e38a65 to your computer and use it in GitHub Desktop.
Custom View Pager
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class CustomViewPager extends ViewGroup { | |
private static final String TAG = "ViewPager"; | |
private static final boolean DEBUG = false; | |
private static final boolean USE_CACHE = false; | |
private static final int DEFAULT_OFFSCREEN_PAGES = 1; | |
private static final int MAX_SETTLE_DURATION = 600; // ms | |
static class ItemInfo { | |
Object object; | |
int position; | |
boolean scrolling; | |
} | |
private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>() { | |
@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) { | |
// _o(t) = t * t * ((tension + 1) * t + tension) | |
// o(t) = _o(t - 1) + 1 | |
t -= 1.0f; | |
return t * t * t + 1.0f; | |
} | |
}; | |
private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>(); | |
private PagerAdapter mAdapter; | |
private int mCurItem; // Index of currently displayed page. | |
private int mRestoredCurItem = -1; | |
private Parcelable mRestoredAdapterState = null; | |
private ClassLoader mRestoredClassLoader = null; | |
private Scroller mScroller; | |
//private PagerAdapter.DataSetObserver mObserver; | |
private int mPageMargin; | |
private Drawable mMarginDrawable; | |
private int mChildWidthMeasureSpec; | |
private int mChildHeightMeasureSpec; | |
private boolean mInLayout; | |
private boolean mScrollingCacheEnabled; | |
private boolean mPopulatePending; | |
private boolean mScrolling; | |
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; | |
private boolean mIsBeingDragged; | |
private boolean mIsUnableToDrag; | |
private int mTouchSlop; | |
private float mInitialMotionX; | |
/** | |
* Position of the last motion event. | |
*/ | |
private float mLastMotionX; | |
private float mLastMotionY; | |
/** | |
* 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 int mMinimumVelocity; | |
private int mMaximumVelocity; | |
private float mBaseLineFlingVelocity; | |
private float mFlingVelocityInfluence; | |
private boolean mFakeDragging; | |
private long mFakeDragBeginTime; | |
private EdgeEffectCompat mLeftEdge; | |
private EdgeEffectCompat mRightEdge; | |
private boolean mFirstLayout = true; | |
private OnPageChangeListener mOnPageChangeListener; | |
/** | |
* 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 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. | |
*/ | |
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. | |
*/ | |
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 CustomViewPager#SCROLL_STATE_IDLE | |
* @see CustomViewPager#SCROLL_STATE_DRAGGING | |
* @see CustomViewPager#SCROLL_STATE_SETTLING | |
*/ | |
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 | |
} | |
} | |
public CustomViewPager(Context context) { | |
super(context); | |
initViewPager(); | |
} | |
public CustomViewPager(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
initViewPager(); | |
} | |
void initViewPager() { | |
setWillNotDraw(false); | |
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); | |
setFocusable(true); | |
final Context context = getContext(); | |
mScroller = new Scroller(context, sInterpolator); | |
final ViewConfiguration configuration = ViewConfiguration.get(context); | |
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); | |
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); | |
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); | |
mLeftEdge = new EdgeEffectCompat(context); | |
mRightEdge = new EdgeEffectCompat(context); | |
float density = context.getResources().getDisplayMetrics().density; | |
mBaseLineFlingVelocity = 2500.0f * density; | |
mFlingVelocityInfluence = 0.4f; | |
} | |
private void setScrollState(int newState) { | |
if (mScrollState == newState) { | |
return; | |
} | |
mScrollState = newState; | |
if (mOnPageChangeListener != null) { | |
mOnPageChangeListener.onPageScrollStateChanged(newState); | |
} | |
} | |
public void setAdapter(PagerAdapter adapter) { | |
if (mAdapter != null) { | |
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(); | |
removeAllViews(); | |
mCurItem = 0; | |
scrollTo(0, 0); | |
} | |
mAdapter = adapter; | |
if (mAdapter != null) { | |
mPopulatePending = false; | |
if (mRestoredCurItem >= 0) { | |
mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); | |
setCurrentItemInternal(mRestoredCurItem, false, true); | |
mRestoredCurItem = -1; | |
mRestoredAdapterState = null; | |
mRestoredClassLoader = null; | |
} else { | |
populate(); | |
} | |
} | |
} | |
public PagerAdapter getAdapter() { | |
return mAdapter; | |
} | |
/** | |
* Set the currently selected page. If the ViewPager has already been through its first | |
* layout 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; | |
} | |
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { | |
setCurrentItemInternal(item, smoothScroll, always, 0); | |
} | |
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { | |
if (mAdapter == null || mAdapter.getCount() <= 0) { | |
setScrollingCacheEnabled(false); | |
return; | |
} | |
if (!always && mCurItem == item && mItems.size() != 0) { | |
setScrollingCacheEnabled(false); | |
return; | |
} | |
if (item < 0) { | |
item = 0; | |
} else if (item >= mAdapter.getCount()) { | |
item = mAdapter.getCount() - 1; | |
} | |
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; | |
mCurItem = item; | |
populate(); | |
final int destX = (getWidth() + mPageMargin) * item; | |
if (smoothScroll) { | |
smoothScrollTo(destX, 0, velocity); | |
if (dispatchSelected && mOnPageChangeListener != null) { | |
mOnPageChangeListener.onPageSelected(item); | |
} | |
} else { | |
if (dispatchSelected && mOnPageChangeListener != null) { | |
mOnPageChangeListener.onPageSelected(item); | |
} | |
completeScroll(); | |
scrollTo(destX, 0); | |
} | |
} | |
public void setOnPageChangeListener(OnPageChangeListener listener) { | |
mOnPageChangeListener = listener; | |
} | |
/** | |
* 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. | |
* <p> | |
* <p>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.</p> | |
* <p> | |
* <p>You should keep this limit low, especially if your pages have complex layouts. | |
* This setting defaults to 1.</p> | |
* | |
* @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 Creator<SavedState> CREATOR | |
= ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks<SavedState>() { | |
@Override | |
public SavedState createFromParcel(Parcel in, ClassLoader loader) { | |
return new SavedState(in, loader); | |
} | |
@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, LayoutParams params) { | |
if (mInLayout) { | |
addViewInLayout(child, index, params); | |
child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec); | |
} else { | |
super.addView(child, index, params); | |
} | |
if (USE_CACHE) { | |
if (child.getVisibility() != GONE) { | |
child.setDrawingCacheEnabled(mScrollingCacheEnabled); | |
} else { | |
child.setDrawingCacheEnabled(false); | |
} | |
} | |
} | |
ItemInfo infoForChild(View child) { | |
for (int i = 0; i < mItems.size(); i++) { | |
ItemInfo ii = mItems.get(i); | |
if (mAdapter.isViewFromObject(child, ii.object)) { | |
return ii; | |
} | |
} | |
return null; | |
} | |
ItemInfo infoForAnyChild(View child) { | |
ViewParent parent; | |
while ((parent = child.getParent()) != this) { | |
if (parent == null || !(parent instanceof View)) { | |
return null; | |
} | |
child = (View) parent; | |
} | |
return infoForChild(child); | |
} | |
@Override | |
protected void onAttachedToWindow() { | |
super.onAttachedToWindow(); | |
mFirstLayout = true; | |
} | |
@Override | |
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
// For simple implementation, or internal size is always 0. | |
// We depend on the container to specify the layout size of | |
// our view. We can't really know what it is since we will be | |
// adding and removing different arbitrary views and do not | |
// want the layout to change as this happens. | |
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), | |
getDefaultSize(0, heightMeasureSpec)); | |
// Children are just made to fill our space. | |
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - | |
getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY); | |
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - | |
getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY); | |
// Make sure we have created all fragments that we need to have shown. | |
mInLayout = true; | |
populate(); | |
mInLayout = false; | |
// Make sure all children have been properly measured. | |
final int size = getChildCount(); | |
for (int i = 0; i < size; ++i) { | |
final View child = getChildAt(i); | |
if (child.getVisibility() != GONE) { | |
if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child | |
+ ": " + mChildWidthMeasureSpec); | |
child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec); | |
} | |
} | |
} | |
@Override | |
protected void onSizeChanged(int w, int h, int oldw, int oldh) { | |
super.onSizeChanged(w, h, oldw, oldh); | |
// Make sure scroll position is set correctly. | |
if (w != oldw) { | |
recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin); | |
} | |
} | |
private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) { | |
final int widthWithMargin = width + margin; | |
if (oldWidth > 0) { | |
final int oldScrollPos = getScrollX(); | |
final int oldwwm = oldWidth + oldMargin; | |
final int oldScrollItem = oldScrollPos / oldwwm; | |
final float scrollOffset = (float) (oldScrollPos % oldwwm) / oldwwm; | |
final int scrollPos = (int) ((oldScrollItem + scrollOffset) * widthWithMargin); | |
scrollTo(scrollPos, getScrollY()); | |
if (!mScroller.isFinished()) { | |
// We now return to your regularly scheduled scroll, already in progress. | |
final int newDuration = mScroller.getDuration() - mScroller.timePassed(); | |
mScroller.startScroll(scrollPos, 0, mCurItem * widthWithMargin, 0, newDuration); | |
} | |
} else { | |
int scrollPos = mCurItem * widthWithMargin; | |
if (scrollPos != getScrollX()) { | |
completeScroll(); | |
scrollTo(scrollPos, getScrollY()); | |
} | |
} | |
} | |
@Override | |
protected void onLayout(boolean changed, int l, int t, int r, int b) { | |
mInLayout = true; | |
populate(); | |
mInLayout = false; | |
final int count = getChildCount(); | |
final int width = r - l; | |
for (int i = 0; i < count; i++) { | |
View child = getChildAt(i); | |
ItemInfo ii; | |
if (child.getVisibility() != GONE && (ii = infoForChild(child)) != null) { | |
int loff = (width + mPageMargin) * ii.position; | |
int childLeft = getPaddingLeft() + loff; | |
int childTop = getPaddingTop(); | |
if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object | |
+ ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() | |
+ "x" + child.getMeasuredHeight()); | |
child.layout(childLeft, childTop, | |
childLeft + child.getMeasuredWidth(), | |
childTop + child.getMeasuredHeight()); | |
} | |
} | |
mFirstLayout = false; | |
} | |
@Override | |
public void computeScroll() { | |
if (DEBUG) Log.i(TAG, "computeScroll: finished=" + mScroller.isFinished()); | |
if (!mScroller.isFinished()) { | |
if (mScroller.computeScrollOffset()) { | |
if (DEBUG) Log.i(TAG, "computeScroll: still scrolling"); | |
int oldX = getScrollX(); | |
int oldY = getScrollY(); | |
int x = mScroller.getCurrX(); | |
int y = mScroller.getCurrY(); | |
if (oldX != x || oldY != y) { | |
scrollTo(x, y); | |
} | |
if (mOnPageChangeListener != null) { | |
final int widthWithMargin = getWidth() + mPageMargin; | |
final int position = x / widthWithMargin; | |
final int offsetPixels = x % widthWithMargin; | |
final float offset = (float) offsetPixels / widthWithMargin; | |
mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); | |
} | |
// Keep on drawing until the animation has finished. | |
invalidate(); | |
return; | |
} | |
} | |
// Done with scroll, clean up state. | |
completeScroll(); | |
} | |
private void completeScroll() { | |
boolean needPopulate = mScrolling; | |
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); | |
} | |
setScrollState(SCROLL_STATE_IDLE); | |
} | |
mPopulatePending = false; | |
mScrolling = false; | |
for (int i = 0; i < mItems.size(); i++) { | |
ItemInfo ii = mItems.get(i); | |
if (ii.scrolling) { | |
needPopulate = true; | |
ii.scrolling = false; | |
} | |
} | |
if (needPopulate) { | |
populate(); | |
} | |
} | |
@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() & MotionEventCompat.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; | |
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, "Intercept returning true!"); | |
return true; | |
} | |
if (mIsUnableToDrag) { | |
if (DEBUG) Log.v(TAG, "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 = MotionEventCompat.findPointerIndex(ev, activePointerId); | |
final float x = MotionEventCompat.getX(ev, pointerIndex); | |
final float dx = x - mLastMotionX; | |
final float xDiff = Math.abs(dx); | |
final float y = MotionEventCompat.getY(ev, pointerIndex); | |
final float yDiff = Math.abs(y - mLastMotionY); | |
final int scrollX = getScrollX(); | |
final boolean atEdge = (dx > 0 && scrollX == 0) || (dx < 0 && mAdapter != null && | |
scrollX >= (mAdapter.getCount() - 1) * getWidth() - 1); | |
if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); | |
if (canScroll(this, false, (int) dx, (int) x, (int) y)) { | |
// Nested view has scrollable area under this point. Let it be handled there. | |
mInitialMotionX = mLastMotionX = x; | |
mLastMotionY = y; | |
return false; | |
} | |
if (xDiff > mTouchSlop && xDiff > yDiff) { | |
if (DEBUG) Log.v(TAG, "Starting drag!"); | |
mIsBeingDragged = true; | |
setScrollState(SCROLL_STATE_DRAGGING); | |
mLastMotionX = x; | |
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; | |
} | |
} | |
break; | |
} | |
case MotionEvent.ACTION_DOWN: { | |
/* | |
* Remember location of down touch. | |
* ACTION_DOWN always refers to pointer index 0. | |
*/ | |
mLastMotionX = mInitialMotionX = ev.getX(); | |
mLastMotionY = ev.getY(); | |
mActivePointerId = MotionEventCompat.getPointerId(ev, 0); | |
if (mScrollState == SCROLL_STATE_SETTLING) { | |
// Let the user 'catch' the pager as it animates. | |
mIsBeingDragged = true; | |
mIsUnableToDrag = false; | |
setScrollState(SCROLL_STATE_DRAGGING); | |
} else { | |
completeScroll(); | |
mIsBeingDragged = false; | |
mIsUnableToDrag = false; | |
} | |
if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY | |
+ " mIsBeingDragged=" + mIsBeingDragged | |
+ "mIsUnableToDrag=" + mIsUnableToDrag); | |
break; | |
} | |
case MotionEventCompat.ACTION_POINTER_UP: | |
onSecondaryPointerUp(ev); | |
break; | |
} | |
/* | |
* 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 (mFakeDragging) { | |
// A fake drag is in progress already, ignore this real one | |
// but still eat the touch events. | |
// (It is likely that the user is multi-touching the screen.) | |
return true; | |
} | |
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 & MotionEventCompat.ACTION_MASK) { | |
case MotionEvent.ACTION_DOWN: { | |
/* | |
* If being flinged and user touches, stop the fling. isFinished | |
* will be false if being flinged. | |
*/ | |
completeScroll(); | |
// Remember where the motion event started | |
mLastMotionX = mInitialMotionX = ev.getX(); | |
mActivePointerId = MotionEventCompat.getPointerId(ev, 0); | |
break; | |
} | |
case MotionEvent.ACTION_MOVE: | |
if (!mIsBeingDragged) { | |
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); | |
final float x = MotionEventCompat.getX(ev, pointerIndex); | |
final float xDiff = Math.abs(x - mLastMotionX); | |
final float y = MotionEventCompat.getY(ev, 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; | |
mLastMotionX = x; | |
setScrollState(SCROLL_STATE_DRAGGING); | |
setScrollingCacheEnabled(true); | |
} | |
} | |
if (mIsBeingDragged) { | |
// Scroll to follow the motion event | |
final int activePointerIndex = MotionEventCompat.findPointerIndex( | |
ev, mActivePointerId); | |
final float x = MotionEventCompat.getX(ev, activePointerIndex); | |
final float deltaX = mLastMotionX - x; | |
mLastMotionX = x; | |
float oldScrollX = getScrollX(); | |
float scrollX = oldScrollX + deltaX; | |
final int width = getWidth(); | |
final int widthWithMargin = width + mPageMargin; | |
final int lastItemIndex = mAdapter.getCount() - 1; | |
final float leftBound = Math.max(0, (mCurItem - 1) * widthWithMargin); | |
final float rightBound = | |
Math.min(mCurItem + 1, lastItemIndex) * widthWithMargin; | |
if (scrollX < leftBound) { | |
if (leftBound == 0) { | |
float over = -scrollX; | |
needsInvalidate = mLeftEdge.onPull(over / width); | |
} | |
scrollX = leftBound; | |
} else if (scrollX > rightBound) { | |
if (rightBound == lastItemIndex * widthWithMargin) { | |
float over = scrollX - rightBound; | |
needsInvalidate = mRightEdge.onPull(over / width); | |
} | |
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); | |
} | |
} | |
break; | |
case MotionEvent.ACTION_UP: | |
if (mIsBeingDragged) { | |
final VelocityTracker velocityTracker = mVelocityTracker; | |
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); | |
int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( | |
velocityTracker, mActivePointerId); | |
mPopulatePending = true; | |
final int widthWithMargin = getWidth() + mPageMargin; | |
final int scrollX = getScrollX(); | |
final int currentPage = scrollX / widthWithMargin; | |
int nextPage = initialVelocity > 0 ? currentPage : currentPage + 1; | |
setCurrentItemInternal(nextPage, true, true, initialVelocity); | |
mActivePointerId = INVALID_POINTER; | |
endDrag(); | |
needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); | |
} | |
break; | |
case MotionEvent.ACTION_CANCEL: | |
if (mIsBeingDragged) { | |
setCurrentItemInternal(mCurItem, true, true); | |
mActivePointerId = INVALID_POINTER; | |
endDrag(); | |
needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); | |
} | |
break; | |
case MotionEventCompat.ACTION_POINTER_DOWN: { | |
final int index = MotionEventCompat.getActionIndex(ev); | |
final float x = MotionEventCompat.getX(ev, index); | |
mLastMotionX = x; | |
mActivePointerId = MotionEventCompat.getPointerId(ev, index); | |
break; | |
} | |
case MotionEventCompat.ACTION_POINTER_UP: | |
onSecondaryPointerUp(ev); | |
mLastMotionX = MotionEventCompat.getX(ev, | |
MotionEventCompat.findPointerIndex(ev, mActivePointerId)); | |
break; | |
} | |
if (needsInvalidate) { | |
invalidate(); | |
} | |
return true; | |
} | |
@Override | |
public void draw(Canvas canvas) { | |
super.draw(canvas); | |
boolean needsInvalidate = false; | |
final int overScrollMode = ViewCompat.getOverScrollMode(this); | |
if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || | |
(overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && | |
mAdapter != null && mAdapter.getCount() > 1)) { | |
if (!mLeftEdge.isFinished()) { | |
final int restoreCount = canvas.save(); | |
final int height = getHeight() - getPaddingTop() - getPaddingBottom(); | |
canvas.rotate(270); | |
canvas.translate(-height + getPaddingTop(), 0); | |
mLeftEdge.setSize(height, getWidth()); | |
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(); | |
final int itemCount = mAdapter != null ? mAdapter.getCount() : 1; | |
canvas.rotate(90); | |
canvas.translate(-getPaddingTop(), | |
-itemCount * (width + mPageMargin) + mPageMargin); | |
mRightEdge.setSize(height, width); | |
needsInvalidate |= mRightEdge.draw(canvas); | |
canvas.restoreToCount(restoreCount); | |
} | |
} else { | |
mLeftEdge.finish(); | |
mRightEdge.finish(); | |
} | |
if (needsInvalidate) { | |
// Keep animating | |
invalidate(); | |
} | |
} | |
@Override | |
protected void onDraw(Canvas canvas) { | |
super.onDraw(canvas); | |
// Draw the margin drawable if needed. | |
if (mPageMargin > 0 && mMarginDrawable != null) { | |
final int scrollX = getScrollX(); | |
final int width = getWidth(); | |
final int offset = scrollX % (width + mPageMargin); | |
if (offset != 0) { | |
// Pages fit completely when settled; we only need to draw when in between | |
final int left = scrollX - offset + width; | |
mMarginDrawable.setBounds(left, 0, left + mPageMargin, getHeight()); | |
mMarginDrawable.draw(canvas); | |
} | |
} | |
} | |
/** | |
* Start a fake drag of the pager. | |
* <p> | |
* <p>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. | |
* <p> | |
* <p>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() { | |
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; | |
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<View> 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<View> 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 | |
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { | |
// ViewPagers should only report accessibility info for the current page, | |
// otherwise things get very confusing. | |
// TODO: Should this note something about the paging container? | |
final int childCount = getChildCount(); | |
for (int i = 0; i < childCount; i++) { | |
final View child = getChildAt(i); | |
if (child.getVisibility() == VISIBLE) { | |
final ItemInfo ii = infoForChild(child); | |
if (ii != null && ii.position == mCurItem && | |
child.dispatchPopulateAccessibilityEvent(event)) { | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment