() {
@Override
public ViewPagerSavedState createFromParcel(Parcel in, ClassLoader loader) {
return new ViewPagerSavedState(in, loader);
}
@Override
public ViewPagerSavedState[] newArray(int size) {
return new ViewPagerSavedState[size];
}
});
ViewPagerSavedState(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();
ViewPagerSavedState ss = new ViewPagerSavedState(superState);
ss.position = mCurItem;
if (mAdapter != null) {
ss.adapterState = mAdapter.saveState();
}
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof ViewPagerSavedState)) {
super.onRestoreInstanceState(state);
return;
}
ViewPagerSavedState ss = (ViewPagerSavedState) 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);
}
}
}
@Override
public void removeView(View view) {
if (mInLayout) {
removeViewInLayout(view);
} else {
super.removeView(view);
}
}
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);
}
ItemInfo infoForPosition(int position) {
for (int i = 0; i < mItems.size(); i++) {
ItemInfo ii = mItems.get(i);
if (ii.position == position) {
return ii;
}
}
return null;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFirstLayout = true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// For simple implementation, our 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));
final int measuredSize =
(mOrientation == Orientation.VERTICAL) ? getMeasuredHeight() : getMeasuredWidth();
final int maxGutterSize = measuredSize / 10;
mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
// Children are just made to fill our space.
int childWidthSize;
int childHeightSize;
if (mOrientation == Orientation.VERTICAL) {
childWidthSize = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
childHeightSize = measuredSize - getPaddingTop() - getPaddingBottom();
} else {
childWidthSize = measuredSize - getPaddingLeft() - getPaddingRight();
childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
}
/*
* Make sure all children have been properly measured. Decor views first.
* Right now we cheat and make this less complicated by assuming decor
* views won't intersect. We will pin to edges based on gravity.
*/
int size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp != null && lp.isDecor) {
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
int widthMode = MeasureSpec.AT_MOST;
int heightMode = MeasureSpec.AT_MOST;
boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
if (consumeVertical) {
widthMode = MeasureSpec.EXACTLY;
} else if (consumeHorizontal) {
heightMode = MeasureSpec.EXACTLY;
}
int widthSize = childWidthSize;
int heightSize = childHeightSize;
if (lp.width != LayoutParams.WRAP_CONTENT) {
widthMode = MeasureSpec.EXACTLY;
if (lp.width != LayoutParams.FILL_PARENT) {
widthSize = lp.width;
}
}
if (lp.height != LayoutParams.WRAP_CONTENT) {
heightMode = MeasureSpec.EXACTLY;
if (lp.height != LayoutParams.FILL_PARENT) {
heightSize = lp.height;
}
}
final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
child.measure(widthSpec, heightSpec);
if (consumeVertical) {
childHeightSize -= child.getMeasuredHeight();
} else if (consumeHorizontal) {
childWidthSize -= child.getMeasuredWidth();
}
}
}
}
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
// Make sure we have created all fragments that we need to have shown.
mInLayout = true;
populate();
mInLayout = false;
// Page views next.
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);
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp == null || !lp.isDecor) {
if (mOrientation == Orientation.VERTICAL) {
final int heightSpec = MeasureSpec.makeMeasureSpec(
(int) (childHeightSize * lp.heightFactor), MeasureSpec.EXACTLY);
child.measure(mChildWidthMeasureSpec, heightSpec);
} else {
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
child.measure(widthSpec, 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 (mOrientation == Orientation.VERTICAL) {
if (h != oldh) {
recomputeScrollPosition(h, oldh, mPageMargin, mPageMargin);
}
} else {
if (w != oldw) {
recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin);
}
}
}
private void recomputeScrollPosition(int size, int oldSize, int margin, int oldMargin) {
if (mOrientation == Orientation.VERTICAL) {
if (oldSize > 0 && !mItems.isEmpty()) {
final int heightWithMargin = size - getPaddingTop() - getPaddingBottom() + margin;
final int oldHeightWithMargin = oldSize - getPaddingTop() - getPaddingBottom()
+ oldMargin;
final int ypos = getScrollY();
final float pageOffset = (float) ypos / oldHeightWithMargin;
final int newOffsetPixels = (int) (pageOffset * heightWithMargin);
scrollTo(getScrollX(), newOffsetPixels);
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(0, newOffsetPixels,
0, (int) (targetInfo.offset * size), newDuration);
}
} else {
final ItemInfo ii = infoForPosition(mCurItem);
final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;
final int scrollPos = (int) (scrollOffset *
(size - getPaddingTop() - getPaddingBottom()));
if (scrollPos != getScrollY()) {
completeScroll(false);
scrollTo(getScrollX(), scrollPos);
}
}
} else {
if (oldSize > 0 && !mItems.isEmpty()) {
final int widthWithMargin = size - getPaddingLeft() - getPaddingRight() + margin;
final int oldWidthWithMargin = oldSize - 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 * size), 0, newDuration);
}
} else {
final ItemInfo ii = infoForPosition(mCurItem);
final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0;
final int scrollPos = (int) (scrollOffset *
(size - 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 scroll = (mOrientation == Orientation.VERTICAL) ? getScrollY() : 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;
}
if (mOrientation == Orientation.VERTICAL) {
childTop += scroll;
} else {
childLeft += scroll;
}
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
decorCount++;
}
}
}
final int childSize =
(mOrientation == Orientation.VERTICAL) ? height - paddingTop - paddingBottom
: 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) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
ItemInfo ii;
if (!lp.isDecor && (ii = infoForChild(child)) != null) {
int topLeftoff = (int) (childSize * ii.offset);
int childLeft;
int childTop;
if (mOrientation == Orientation.VERTICAL) {
childLeft = paddingLeft;
childTop = paddingTop + topLeftoff;
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) (width - paddingLeft - paddingRight),
MeasureSpec.EXACTLY);
final int heightSpec = MeasureSpec.makeMeasureSpec(
(int) (childSize * lp.heightFactor),
MeasureSpec.EXACTLY);
child.measure(widthSpec, heightSpec);
}
} else {
childLeft = paddingLeft + topLeftoff;
childTop = paddingTop;
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) (childSize * lp.widthFactor),
MeasureSpec.EXACTLY);
final int heightSpec = MeasureSpec.makeMeasureSpec(
(int) (height - paddingTop - paddingBottom),
MeasureSpec.EXACTLY);
child.measure(widthSpec, heightSpec);
}
}
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());
}
}
}
mTopLeftPageBounds = (mOrientation == Orientation.VERTICAL) ? paddingLeft : paddingTop;
mBottomRightPageBounds =
(mOrientation == Orientation.VERTICAL) ? width - paddingRight : height - paddingBottom;
mDecorChildCount = decorCount;
if (mFirstLayout) {
scrollToItem(mCurItem, false, 0, false);
}
mFirstLayout = false;
}
@Override
public void computeScroll() {
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
scrollTo(x, y);
if (mOrientation == Orientation.VERTICAL) {
if (!pageScrolled(y)) {
mScroller.abortAnimation();
scrollTo(x, 0);
}
} else {
if (!pageScrolled(x)) {
mScroller.abortAnimation();
scrollTo(0, y);
}
}
}
// Keep on drawing until the animation has finished.
ViewCompat.postInvalidateOnAnimation(this);
return;
}
// Done with scroll, clean up state.
completeScroll(true);
}
private boolean pageScrolled(int pos) {
if (mItems.size() == 0) {
mCalledSuper = false;
onPageScrolled(0, 0, 0);
if (!mCalledSuper) {
throw new IllegalStateException(
"onPageScrolled did not call superclass implementation");
}
return false;
}
final ItemInfo ii = infoForCurrentScrollPosition();
final int size = getClientSize();
final int sizeWithMargin = size + mPageMargin;
final float marginOffset = (float) mPageMargin / size;
final int currentPage = ii.position;
final float pageOffset = (((float) pos / size) - ii.offset) / (ii.sizeFactor + marginOffset);
final int offsetPixels = (int) (pageOffset * sizeWithMargin);
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) {
if (mOrientation == Orientation.VERTICAL) {
final int scrollY = getScrollY();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
final int height = getHeight();
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 vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
int childTop = 0;
switch (vgrav) {
default:
childTop = paddingTop;
break;
case Gravity.TOP:
childTop = paddingTop;
paddingTop += child.getHeight();
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;
}
childTop += scrollY;
final int childOffset = childTop - child.getTop();
if (childOffset != 0) {
child.offsetTopAndBottom(childOffset);
}
}
} else {
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 scroll = (mOrientation == Orientation.VERTICAL) ? getScrollY() : 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) (((mOrientation == Orientation.VERTICAL) ? child.getTop() : child.getLeft())
- scroll) / getClientSize();
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 < mItems.size(); i++) {
ItemInfo ii = mItems.get(i);
if (ii.scrolling) {
needPopulate = true;
ii.scrolling = false;
}
}
if (needPopulate) {
if (postEvents) {
ViewCompat.postOnAnimation(this, mEndScrollRunnable);
} else {
mEndScrollRunnable.run();
}
}
}
private boolean isGutterDrag(float axis, float dAxis) {
return (axis < mGutterSize && dAxis > 0) || (axis
> (mOrientation == Orientation.VERTICAL ? getHeight() : getWidth()) - mGutterSize
&& dAxis < 0);
}
private void enableLayers(boolean enable) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final int layerType = enable ?
ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE;
ViewCompat.setLayerType(getChildAt(i), 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() & 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;
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, "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);
if (mOrientation == Orientation.VERTICAL) {
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float dy = y - mLastMotionY;
final float yDiff = Math.abs(dy);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float xDiff = Math.abs(x - mInitialMotionX);
if (DEBUG) {
Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
}
if (dy != 0 && !isGutterDrag(mLastMotionY, dy) &&
canScroll(this, false, (int) dy, (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 (yDiff > mTouchSlop && yDiff * 0.5f > xDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
mLastMotionY = dy > 0 ? mInitialMotionY + mTouchSlop :
mInitialMotionY - mTouchSlop;
mLastMotionX = x;
setScrollingCacheEnabled(true);
} else if (xDiff > 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(y)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
} else {
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 - 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)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
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 = MotionEventCompat.getPointerId(ev, 0);
mIsUnableToDrag = false;
mScroller.computeScrollOffset();
if (mOrientation == Orientation.VERTICAL) {
if (mScrollState == SCROLL_STATE_SETTLING &&
Math.abs(mScroller.getFinalY() - mScroller.getCurrY()) > 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;
}
} else {
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 MotionEventCompat.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 (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: {
mScroller.abortAnimation();
mPopulatePending = false;
populate();
// Remember where the motion event started
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = mInitialMotionY = ev.getY();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
break;
}
case MotionEvent.ACTION_MOVE:
if (!mIsBeingDragged) {
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mLastMotionY);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float xDiff = Math.abs(x - mLastMotionX);
if (DEBUG) {
Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
}
if (mOrientation == Orientation.VERTICAL) {
if (yDiff > mTouchSlop && yDiff > xDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
mLastMotionY = y - mInitialMotionY > 0 ? mInitialMotionY + mTouchSlop :
mInitialMotionY - mTouchSlop;
mLastMotionX = x;
setScrollState(SCROLL_STATE_DRAGGING);
setScrollingCacheEnabled(true);
// Disallow Parent Intercept, just in case
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
} else {
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 = MotionEventCompat.findPointerIndex(
ev, mActivePointerId);
if (mOrientation == Orientation.VERTICAL) {
final float y = MotionEventCompat.getY(ev, activePointerIndex);
needsInvalidate |= performDrag(y);
} else {
final float x = MotionEventCompat.getX(ev, activePointerIndex);
needsInvalidate |= performDrag(x);
}
}
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
int currentPage;
int initialVelocity;
int totalDelta;
float pageOffset;
if (mOrientation == Orientation.VERTICAL) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
velocityTracker, mActivePointerId);
mPopulatePending = true;
final int height = getClientSize();
final int scrollY = getScrollY();
final ItemInfo ii = infoForCurrentScrollPosition();
currentPage = ii.position;
pageOffset = (((float) scrollY / height) - ii.offset) / ii.sizeFactor;
final int activePointerIndex =
MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float y = MotionEventCompat.getY(ev, activePointerIndex);
totalDelta = (int) (y - mInitialMotionY);
} else {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
velocityTracker, mActivePointerId);
mPopulatePending = true;
final int width = getClientSize();
final int scrollX = getScrollX();
final ItemInfo ii = infoForCurrentScrollPosition();
currentPage = ii.position;
pageOffset = (((float) scrollX / width) - ii.offset) / ii.sizeFactor;
final int activePointerIndex =
MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, activePointerIndex);
totalDelta = (int) (x - mInitialMotionX);
}
int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
totalDelta);
setCurrentItemInternal(nextPage, true, true, initialVelocity);
mActivePointerId = INVALID_POINTER;
endDrag();
needsInvalidate = mTopLeftEdge.onRelease() | mRightBottomEdge.onRelease();
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged) {
scrollToItem(mCurItem, true, 0, false);
mActivePointerId = INVALID_POINTER;
endDrag();
needsInvalidate = mTopLeftEdge.onRelease() | mRightBottomEdge.onRelease();
}
break;
case MotionEventCompat.ACTION_POINTER_DOWN: {
int index;
if (mOrientation == Orientation.VERTICAL) {
index = MotionEventCompat.getActionIndex(ev);
final float y = MotionEventCompat.getY(ev, index);
mLastMotionY = y;
} else {
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);
if (mOrientation == Orientation.VERTICAL) {
mLastMotionY = MotionEventCompat.getY(ev,
MotionEventCompat.findPointerIndex(ev, mActivePointerId));
} else {
mLastMotionX = MotionEventCompat.getX(ev,
MotionEventCompat.findPointerIndex(ev, mActivePointerId));
}
break;
}
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
return true;
}
private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
private boolean performDrag(float dimen) {
boolean needsInvalidate = false;
if (mOrientation == Orientation.VERTICAL) {
float y = dimen;
final float deltaY = mLastMotionY - y;
mLastMotionY = y;
float oldScrollY = getScrollY();
float scrollY = oldScrollY + deltaY;
final int height = getClientSize();
float topBound = height * mFirstOffset;
float bottomBound = height * mLastOffset;
boolean topAbsolute = true;
boolean bottomAbsolute = true;
final ItemInfo firstItem = mItems.get(0);
final ItemInfo lastItem = mItems.get(mItems.size() - 1);
if (firstItem.position != 0) {
topAbsolute = false;
topBound = firstItem.offset * height;
}
if (lastItem.position != mAdapter.getCount() - 1) {
bottomAbsolute = false;
bottomBound = lastItem.offset * height;
}
if (scrollY < topBound) {
if (topAbsolute) {
float over = topBound - scrollY;
needsInvalidate = mTopLeftEdge.onPull(Math.abs(over) / height);
}
scrollY = topBound;
} else if (scrollY > bottomBound) {
if (bottomAbsolute) {
float over = scrollY - bottomBound;
needsInvalidate = mRightBottomEdge.onPull(Math.abs(over) / height);
}
scrollY = bottomBound;
}
// Don't lose the rounded component
mLastMotionX += scrollY - (int) scrollY;
scrollTo(getScrollX(), (int) scrollY);
pageScrolled((int) scrollY);
} else {
float x = dimen;
final float deltaX = mLastMotionX - x;
mLastMotionX = x;
float oldScrollX = getScrollX();
float scrollX = oldScrollX + deltaX;
final int width = getClientSize();
float leftBound = width * mFirstOffset;
float rightBound = width * mLastOffset;
boolean leftAbsolute = true;
boolean rightAbsolute = true;
final ItemInfo firstItem = mItems.get(0);
final ItemInfo lastItem = mItems.get(mItems.size() - 1);
if (firstItem.position != 0) {
leftAbsolute = false;
leftBound = firstItem.offset * width;
}
if (lastItem.position != mAdapter.getCount() - 1) {
rightAbsolute = false;
rightBound = lastItem.offset * width;
}
if (scrollX < leftBound) {
if (leftAbsolute) {
float over = leftBound - scrollX;
needsInvalidate = mTopLeftEdge.onPull(Math.abs(over) / width);
}
scrollX = leftBound;
} else if (scrollX > rightBound) {
if (rightAbsolute) {
float over = scrollX - rightBound;
needsInvalidate = mRightBottomEdge.onPull(Math.abs(over) / width);
}
scrollX = rightBound;
}
// Don't lose the rounded component
mLastMotionX += scrollX - (int) scrollX;
scrollTo((int) scrollX, getScrollY());
pageScrolled((int) scrollX);
}
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 infoForCurrentScrollPosition() {
final int size = getClientSize();
final float scrollOffset =
size > 0 ? (float) ((mOrientation == Orientation.VERTICAL) ? getScrollY() : getScrollX())
/ size : 0;
final float marginOffset = size > 0 ? (float) mPageMargin / size : 0;
int lastPos = -1;
float lastOffset = 0.f;
float lastSize = 0.f;
boolean first = true;
ItemInfo lastItem = null;
for (int i = 0; i < mItems.size(); i++) {
ItemInfo ii = mItems.get(i);
float offset;
if (!first && ii.position != lastPos + 1) {
// Create a synthetic item for a missing page.
ii = mTempItem;
ii.offset = lastOffset + lastSize + marginOffset;
ii.position = lastPos + 1;
ii.sizeFactor = mAdapter.getPageWidth(ii.position);
i--;
}
offset = ii.offset;
final float topLeftBound = offset;
final float bottomRightBound = offset + ii.sizeFactor + marginOffset;
if (first || scrollOffset >= topLeftBound) {
if (scrollOffset < bottomRightBound || i == mItems.size() - 1) {
return ii;
}
} else {
return lastItem;
}
first = false;
lastPos = ii.position;
lastOffset = offset;
lastSize = ii.sizeFactor;
lastItem = ii;
}
return lastItem;
}
private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaDimen) {
int targetPage;
if (Math.abs(deltaDimen) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
targetPage = velocity > 0 ? currentPage : currentPage + 1;
} else {
final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
targetPage = (int) (currentPage + 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 = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
}
return targetPage;
}
@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 (mOrientation == Orientation.VERTICAL) {
if (!mTopLeftEdge.isFinished()) {
final int restoreCount = canvas.save();
final int height = getHeight();
final int width = getWidth() - getPaddingLeft() - getPaddingRight();
canvas.translate(getPaddingLeft(), mFirstOffset * height);
mTopLeftEdge.setSize(width, height);
needsInvalidate |= mTopLeftEdge.draw(canvas);
canvas.restoreToCount(restoreCount);
}
if (!mRightBottomEdge.isFinished()) {
final int restoreCount = canvas.save();
final int height = getHeight();
final int width = getWidth() - getPaddingLeft() - getPaddingRight();
canvas.rotate(180);
canvas.translate(-width - getPaddingLeft(), -(mLastOffset + 1) * height);
mRightBottomEdge.setSize(width, height);
needsInvalidate |= mRightBottomEdge.draw(canvas);
canvas.restoreToCount(restoreCount);
}
} else {
if (!mTopLeftEdge.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);
mTopLeftEdge.setSize(height, width);
needsInvalidate |= mTopLeftEdge.draw(canvas);
canvas.restoreToCount(restoreCount);
}
if (!mRightBottomEdge.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);
mRightBottomEdge.setSize(height, width);
needsInvalidate |= mRightBottomEdge.draw(canvas);
canvas.restoreToCount(restoreCount);
}
}
} else {
mTopLeftEdge.finish();
mRightBottomEdge.finish();
}
if (needsInvalidate) {
// Keep animating
ViewCompat.postInvalidateOnAnimation(this);
}
}
@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) {
if (mOrientation == Orientation.VERTICAL) {
final int scrollY = getScrollY();
final int height = getHeight();
final float marginOffset = (float) mPageMargin / height;
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);
}
float drawAt;
if (pos == ii.position) {
drawAt = (ii.offset + ii.sizeFactor) * height;
offset = ii.offset + ii.sizeFactor + marginOffset;
} else {
float heightFactor = mAdapter.getPageWidth(pos);
drawAt = (offset + heightFactor) * height;
offset += heightFactor + marginOffset;
}
if (drawAt + mPageMargin > scrollY) {
mMarginDrawable.setBounds(mTopLeftPageBounds, (int) drawAt,
mBottomRightPageBounds, (int) (drawAt + mPageMargin + 0.5f));
mMarginDrawable.draw(canvas);
}
if (drawAt > scrollY + height) {
break; // No more visible, no sense in continuing
}
}
} else {
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);
}
float drawAt;
if (pos == ii.position) {
drawAt = (ii.offset + ii.sizeFactor) * width;
offset = ii.offset + ii.sizeFactor + marginOffset;
} else {
float widthFactor = mAdapter.getPageWidth(pos);
drawAt = (offset + widthFactor) * width;
offset += widthFactor + marginOffset;
}
if (drawAt + mPageMargin > scrollX) {
mMarginDrawable.setBounds((int) drawAt, mTopLeftPageBounds,
(int) (drawAt + mPageMargin + 0.5f), mBottomRightPageBounds);
mMarginDrawable.draw(canvas);
}
if (drawAt > scrollX + width) {
break; // No more visible, no sense in continuing
}
}
}
}
}
/**
* Start a fake drag of the pager.
*
* A fake drag can be useful if you want to synchronize the motion of the ViewPager
* with the touch scrolling of another view, while still letting the ViewPager
* control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.)
* Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call
* {@link #endFakeDrag()} to complete the fake drag and fling as necessary.
*
* During a fake drag the ViewPager will ignore all touch events. If a real drag
* is already in progress, this method will return false.
*
* @return true if the fake drag began successfully, false if it could not be started.
* @see #fakeDragBy(float)
* @see #endFakeDrag()
*/
public boolean beginFakeDrag() {
if (mIsBeingDragged) {
return false;
}
mFakeDragging = true;
setScrollState(SCROLL_STATE_DRAGGING);
if (mOrientation == Orientation.VERTICAL) {
mInitialMotionY = mLastMotionY = 0;
} else {
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);
if (mOrientation == Orientation.VERTICAL) {
int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(
velocityTracker, mActivePointerId);
mPopulatePending = true;
final int size = getClientSize();
final int scrollY = getScrollY();
final ItemInfo ii = infoForCurrentScrollPosition();
final int currentPage = ii.position;
final float pageOffset = (((float) scrollY / size) - ii.offset) / ii.sizeFactor;
final int totalDelta = (int) (mLastMotionY - mInitialMotionY);
int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
totalDelta);
setCurrentItemInternal(nextPage, true, true, initialVelocity);
} else {
int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
velocityTracker, mActivePointerId);
mPopulatePending = true;
final int size = getClientSize();
final int scrollX = getScrollX();
final ItemInfo ii = infoForCurrentScrollPosition();
final int currentPage = ii.position;
final float pageOffset = (((float) scrollX / size) - ii.offset) / ii.sizeFactor;
final int totalDelta = (int) (mLastMotionX - mInitialMotionX);
int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
totalDelta);
setCurrentItemInternal(nextPage, true, true, initialVelocity);
}
endDrag();
mFakeDragging = false;
}
/**
* Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first.
*
* @param offset Offset in pixels to drag by.
* @see #beginFakeDrag()
* @see #endFakeDrag()
*/
public void fakeDragBy(float offset) {
if (!mFakeDragging) {
throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
}
if (mOrientation == Orientation.VERTICAL) {
mLastMotionY += offset;
float oldScrollY = getScrollY();
float scrollY = oldScrollY - offset;
final int height = getClientSize();
float topBound = height * mFirstOffset;
float bottomBound = height * mLastOffset;
final ItemInfo firstItem = mItems.get(0);
final ItemInfo lastItem = mItems.get(mItems.size() - 1);
if (firstItem.position != 0) {
topBound = firstItem.offset * height;
}
if (lastItem.position != mAdapter.getCount() - 1) {
bottomBound = lastItem.offset * height;
}
if (scrollY < topBound) {
scrollY = topBound;
} else if (scrollY > bottomBound) {
scrollY = bottomBound;
}
// Don't lose the rounded component
mLastMotionY += scrollY - (int) scrollY;
scrollTo(getScrollX(), (int) scrollY);
pageScrolled((int) scrollY);
// Synthesize an event for the VelocityTracker.
final long time = SystemClock.uptimeMillis();
final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE,
0, mLastMotionY, 0);
mVelocityTracker.addMovement(ev);
ev.recycle();
} else {
mLastMotionX += offset;
float oldScrollX = getScrollX();
float scrollX = oldScrollX - offset;
final int width = getClientSize();
float leftBound = width * mFirstOffset;
float rightBound = width * mLastOffset;
final ItemInfo firstItem = mItems.get(0);
final ItemInfo lastItem = mItems.get(mItems.size() - 1);
if (firstItem.position != 0) {
leftBound = firstItem.offset * width;
}
if (lastItem.position != mAdapter.getCount() - 1) {
rightBound = lastItem.offset * width;
}
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());
pageScrolled((int) scrollX);
// 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;
if (mOrientation == Orientation.VERTICAL) {
mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex);
} else {
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);
}
}
}
}
}
public boolean internalCanScrollVertically(int direction) {
if (mAdapter == null) {
return false;
}
final int size = getClientSize();
final int scroll = (mOrientation == Orientation.VERTICAL) ? getScrollY() : getScrollX();
if (direction < 0) {
return (scroll > (int) (size * mFirstOffset));
} else if (direction > 0) {
return (scroll < (int) (size * 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 delta 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 delta, 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 (mOrientation == Orientation.VERTICAL) {
if (y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
canScroll(child, true, delta, x + scrollX - child.getLeft(),
y + scrollY - child.getTop())) {
return true;
}
} else {
if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
canScroll(child, true, delta, x + scrollX - child.getLeft(),
y + scrollY - child.getTop())) {
return true;
}
}
}
}
return checkV && ViewCompat.canScrollVertically(v, -delta);
}
@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 (Build.VERSION.SDK_INT >= 11) {
// The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD
// before Android 3.0. Ignore the tab key on those devices.
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_UP) {
// 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 (mOrientation == Orientation.VERTICAL) {
final int nextTop = getChildRectInPagerCoordinates(mTempRect, nextFocused).top;
final int currTop = getChildRectInPagerCoordinates(mTempRect, currentFocused).top;
if (currentFocused != null && nextTop >= currTop) {
handled = pageBack();
} else {
handled = nextFocused.requestFocus();
}
} else {
final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
if (currentFocused != null && nextLeft >= currLeft) {
handled = pageBack();
} else {
handled = nextFocused.requestFocus();
}
}
} else if (direction == View.FOCUS_DOWN) {
// 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 (mOrientation == Orientation.VERTICAL) {
final int nextDown = getChildRectInPagerCoordinates(mTempRect, nextFocused).bottom;
final int currDown = getChildRectInPagerCoordinates(mTempRect, currentFocused).bottom;
if (currentFocused != null && nextDown <= currDown) {
handled = pageForward();
} else {
handled = nextFocused.requestFocus();
}
} else {
final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;
final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;
if (currentFocused != null && nextLeft <= currLeft) {
handled = pageForward();
} else {
handled = nextFocused.requestFocus();
}
}
}
} else if (direction == FOCUS_UP || direction == FOCUS_BACKWARD) {
// Trying to move left and nothing there; try to page.
handled = pageBack();
} else if (direction == FOCUS_DOWN || direction == FOCUS_FORWARD) {
// Trying to move right and nothing there; try to page.
handled = pageForward();
}
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 pageBack() {
if (mCurItem > 0) {
setCurrentItem(mCurItem - 1, true);
return true;
}
return false;
}
boolean pageForward() {
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 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
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
// Dispatch scroll events from this ViewPager.
if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) {
return super.dispatchPopulateAccessibilityEvent(event);
}
// Dispatch all other accessibility events from the current page.
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;
}
@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);
}
class MyAccessibilityDelegate extends AccessibilityDelegateCompat {
@Override
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(host, event);
event.setClassName(ViewPager.class.getName());
final AccessibilityRecordCompat recordCompat = AccessibilityRecordCompat.obtain();
recordCompat.setScrollable(canScroll());
if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED
&& mAdapter != null) {
recordCompat.setItemCount(mAdapter.getCount());
recordCompat.setFromIndex(mCurItem);
recordCompat.setToIndex(mCurItem);
}
}
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName(ViewPager.class.getName());
info.setScrollable(canScroll());
if (mOrientation == Orientation.VERTICAL) {
if (internalCanScrollVertically(1)) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
}
if (internalCanScrollVertically(-1)) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
}
} else {
if (canScrollHorizontally(1)) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
}
if (canScrollHorizontally(-1)) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
}
}
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if (super.performAccessibilityAction(host, action, args)) {
return true;
}
switch (action) {
case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: {
if ((mOrientation == Orientation.VERTICAL && internalCanScrollVertically(1))
|| (mOrientation == Orientation.HORIZONTAL && canScrollHorizontally(1))) {
setCurrentItem(mCurItem + 1);
return true;
}
}
return false;
case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: {
if ((mOrientation == Orientation.VERTICAL && internalCanScrollVertically(-1))
|| (mOrientation == Orientation.HORIZONTAL && 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 height
*/
float heightFactor = 0.f;
/**
* 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;
}
}
// Following classes and the interface are needed for the Maven Central upload script to work properly.
// They are being introduced here, sort of temporarily (until I find a better solution for this issue).
/**
* Callbacks a {@link Parcelable} creator should implement.
*/
public interface ParcelableCompatCreatorCallbacks {
/**
* Create a new instance of the Parcelable class, instantiating it
* from the given Parcel whose data had previously been written by
* {@link Parcelable#writeToParcel Parcelable.writeToParcel()} and
* using the given ClassLoader.
*
* @param in The Parcel to read the object's data from.
* @param loader The ClassLoader that this object is being created in.
* @return Returns a new instance of the Parcelable class.
*/
public T createFromParcel(Parcel in, ClassLoader loader);
/**
* Create a new array of the Parcelable class.
*
* @param size Size of the array.
* @return Returns an array of the Parcelable class, with every entry
* initialized to null.
*/
public T[] newArray(int size);
}
/**
* Helper for accessing features in {@link android.os.Parcelable}
* introduced after API level 4 in a backwards compatible fashion.
*/
public static class ParcelableCompat {
/**
* Factory method for {@link Parcelable.Creator}.
*
* @param callbacks Creator callbacks implementation.
* @return New creator.
*/
public static Parcelable.Creator newCreator(
OrientedViewPager.ParcelableCompatCreatorCallbacks callbacks) {
if (android.os.Build.VERSION.SDK_INT >= 13) {
return ParcelableCompatCreatorHoneycombMR2Stub.instantiate(callbacks);
}
return new CompatCreator(callbacks);
}
public static class CompatCreator implements Parcelable.Creator {
final OrientedViewPager.ParcelableCompatCreatorCallbacks mCallbacks;
public CompatCreator(OrientedViewPager.ParcelableCompatCreatorCallbacks callbacks) {
mCallbacks = callbacks;
}
@Override
public T createFromParcel(Parcel source) {
return mCallbacks.createFromParcel(source, null);
}
@Override
public T[] newArray(int size) {
return mCallbacks.newArray(size);
}
}
}
static class ParcelableCompatCreatorHoneycombMR2Stub {
public static Parcelable.Creator instantiate(
OrientedViewPager.ParcelableCompatCreatorCallbacks callbacks) {
return new ParcelableCompatCreatorHoneycombMR2(callbacks);
}
}
static class ParcelableCompatCreatorHoneycombMR2 implements Parcelable.ClassLoaderCreator {
private final OrientedViewPager.ParcelableCompatCreatorCallbacks mCallbacks;
public ParcelableCompatCreatorHoneycombMR2(
OrientedViewPager.ParcelableCompatCreatorCallbacks callbacks) {
mCallbacks = callbacks;
}
public T createFromParcel(Parcel in) {
return mCallbacks.createFromParcel(in, null);
}
public T createFromParcel(Parcel in, ClassLoader loader) {
return mCallbacks.createFromParcel(in, loader);
}
public T[] newArray(int size) {
return mCallbacks.newArray(size);
}
}
}
================================================
FILE: library/src/main/java/com/gu/library/transformer/VerticalBaseTransformer.java
================================================
package com.gu.library.transformer;
import android.support.v4.view.ViewPager;
import android.view.View;
/**
* Created by Nate on 2016/7/22.
*/
public abstract class VerticalBaseTransformer implements ViewPager.PageTransformer {
/**
* Called each {@link #transformPage(View, float)}.
*
* @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.
*/
protected abstract void onTransform(View page, float position);
/**
* Apply a property transformation to the given page. For most use cases, this method should not be overridden.
* Instead use {@link #transformPage(View, float)} to perform typical transformations.
*
* @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.
*/
@Override
public void transformPage(View page, float position) {
onPreTransform(page, position);
onTransform(page, position);
onPostTransform(page, position);
}
/**
* If the position offset of a fragment is less than negative one or greater than one, returning true will set the
* fragment alpha to 0f. Otherwise fragment alpha is always defaulted to 1f.
*
* @return
*/
protected boolean hideOffscreenPages() {
return true;
}
/**
* Indicates if the default animations of the view pager should be used.
*
* @return
*/
protected boolean isPagingEnabled() {
return false;
}
/**
* Called each {@link #transformPage(View, float)} before {{@link #onTransform(View, float)}.
*
* The default implementation attempts to reset all view properties. This is useful when toggling transforms that do
* not modify the same page properties. For instance changing from a transformation that applies rotation to a
* transformation that fades can inadvertently leave a fragment stuck with a rotation or with some degree of applied
* alpha.
*
* @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.
*/
protected void onPreTransform(View page, float position) {
final float width = page.getWidth();
final float height = page.getHeight();
page.setRotationX(0);
page.setRotationY(0);
page.setRotation(0);
page.setScaleX(1);
page.setScaleY(1);
page.setPivotX(0);
page.setPivotY(0);
page.setTranslationX(0);
page.setTranslationY(isPagingEnabled() ? 0f : -height * position);
if (hideOffscreenPages()) {
page.setAlpha(position <= -1f || position >= 1f ? 0f : 1f);
} else {
page.setAlpha(1f);
}
/*final float normalizedposition = Math.abs(Math.abs(position) - 1);
page.setAlpha(normalizedposition);*/
}
/**
* Called each {@link #transformPage(View, float)} after {@link #onTransform(View, float)}.
*
* @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.
*/
protected void onPostTransform(View page, float position) {
}
/**
* Same as {@link Math#min(double, double)} without double casting, zero closest to infinity handling, or NaN support.
*
* @param val
* @param min
* @return
*/
protected static final float min(float val, float min) {
return val < min ? min : val;
}
}
================================================
FILE: library/src/main/java/com/gu/library/transformer/VerticalStackTransformer.java
================================================
package com.gu.library.transformer;
import android.content.Context;
import android.util.Log;
import android.view.View;
import com.gu.library.utils.ScreenUtils;
/**
* Created by Nate on 2016/7/22.
*/
public class VerticalStackTransformer extends VerticalBaseTransformer {
private Context context;
private int spaceBetweenFirAndSecWith = 10 * 2;//第一张卡片和第二张卡片宽度差 dp单位
private int spaceBetweenFirAndSecHeight = 10;//第一张卡片和第二张卡片高度差 dp单位
public VerticalStackTransformer(Context context) {
this.context = context;
}
public VerticalStackTransformer(Context context, int spaceBetweenFirAndSecWith, int spaceBetweenFirAndSecHeight) {
this.context = context;
this.spaceBetweenFirAndSecWith = spaceBetweenFirAndSecWith;
this.spaceBetweenFirAndSecHeight = spaceBetweenFirAndSecHeight;
}
@Override
protected void onTransform(View page, float position) {
if (position <= 0.0f) {
page.setAlpha(1.0f);
Log.e("onTransform", "position <= 0.0f ==>" + position);
page.setTranslationY(0f);
//控制停止滑动切换的时候,只有最上面的一张卡片可以点击
page.setClickable(true);
} else if (position <= 3.0f) {
Log.e("onTransform", "position <= 3.0f ==>" + position);
float scale = (float) (page.getWidth() - ScreenUtils.dp2px(context, spaceBetweenFirAndSecWith * position)) / (float) (page.getWidth());
//控制下面卡片的可见度
page.setAlpha(1.0f);
//控制停止滑动切换的时候,只有最上面的一张卡片可以点击
page.setClickable(false);
page.setPivotX(page.getWidth() / 2f);
page.setPivotY(page.getHeight() / 2f);
page.setScaleX(scale);
page.setScaleY(scale);
page.setTranslationY(-page.getHeight() * position + (page.getHeight() * 0.5f) * (1 - scale) + ScreenUtils.dp2px(context, spaceBetweenFirAndSecHeight) * position);
}
}
}
================================================
FILE: library/src/main/java/com/gu/library/utils/ScreenUtils.java
================================================
package com.gu.library.utils;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;
/**
* Created by Nate on 2015/9/10. 屏幕相关工具类,可以获取屏幕宽高度,还有截取屏幕
*/
public class ScreenUtils {
private ScreenUtils() {
/* cannot be instantiated */
throw new UnsupportedOperationException("cannot be instantiated");
}
/**
* 获得屏幕高度
*
* @param context
* @return
*/
public static int getScreenWidth(Context context) {
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.widthPixels;
}
/**
* 获得屏幕宽度
*
* @param context
* @return
*/
public static int getScreenHeight(Context context) {
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.heightPixels;
}
/**
* 获得状态栏的高度
*
* @param context
* @return
*/
public static int getStatusHeight(Context context) {
int statusHeight = -1;
try {
Class> clazz = Class.forName("com.android.internal.R$dimen");
Object object = clazz.newInstance();
int height = Integer.parseInt(clazz.getField("status_bar_height")
.get(object).toString());
statusHeight = context.getResources().getDimensionPixelSize(height);
} catch (Exception e) {
e.printStackTrace();
}
return statusHeight;
}
/**
* 获取当前屏幕截图,包含状态栏
*
* @param activity
* @return
*/
public static Bitmap snapShotWithStatusBar(Activity activity) {
View view = activity.getWindow().getDecorView();
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap bmp = view.getDrawingCache();
int width = getScreenWidth(activity);
int height = getScreenHeight(activity);
Bitmap bp = null;
bp = Bitmap.createBitmap(bmp, 0, 0, width, height);
view.destroyDrawingCache();
return bp;
}
/**
* 获取当前屏幕截图,不包含状态栏
*
* @param activity
* @return
*/
public static Bitmap snapShotWithoutStatusBar(Activity activity) {
View view = activity.getWindow().getDecorView();
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap bmp = view.getDrawingCache();
Rect frame = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
int statusBarHeight = frame.top;
int width = getScreenWidth(activity);
int height = getScreenHeight(activity);
Bitmap bp = null;
bp = Bitmap.createBitmap(bmp, 0, statusBarHeight, width, height
- statusBarHeight);
view.destroyDrawingCache();
return bp;
}
/**
* dp转px
*
* @param context
* @param dpVal
* @return
*/
public static int dp2px(Context context, float dpVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dpVal, context.getResources().getDisplayMetrics());
}
/**
* sp转px
*
* @param context
* @param spVal
* @return
*/
public static int sp2px(Context context, float spVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
spVal, context.getResources().getDisplayMetrics());
}
/**
* px转dp
*
* @param context
* @param pxVal
* @return
*/
public static float px2dp(Context context, float pxVal) {
final float scale = context.getResources().getDisplayMetrics().density;
return (pxVal / scale);
}
/**
* px转sp
*
* @param context
* @param pxVal
* @return
*/
public static float px2sp(Context context, float pxVal) {
return (pxVal / context.getResources().getDisplayMetrics().scaledDensity);
}
/**
* 动态设置图片宽高
*/
/**
* 动态设置图片宽高
*/
public static float[] getBitmapConfiguration(Bitmap bitmap, ImageView imageView, float screenRadio) {
int screenWidth = getScreenWidth(imageView.getContext());
float rawWidth = 0;
float rawHeight = 0;
float width = 0;
float height = 0;
if (bitmap == null) {
width = (float) (screenWidth / screenRadio);
height = (float) width;
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
} else {
rawWidth = bitmap.getWidth();
rawHeight = bitmap.getHeight();
if (rawHeight > 10 * rawWidth) {
imageView.setScaleType(ImageView.ScaleType.CENTER);
} else {
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
}
float radio = rawHeight / rawWidth;
width = (float) (screenWidth / screenRadio);
height = (float) (radio * width);
}
return new float[]{width, height};
}
}
================================================
FILE: library/src/test/java/com/gu/library/ExampleUnitTest.java
================================================
package com.gu.library;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* To work on unit tests, switch the Test Artifact in the Build Variants view.
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
================================================
FILE: settings.gradle
================================================
include ':app', ':library'