Skip to content

Instantly share code, notes, and snippets.

@goodev
Created September 24, 2015 07:13
Show Gist options
  • Save goodev/0b32980f9ec4cbde7f32 to your computer and use it in GitHub Desktop.
Save goodev/0b32980f9ec4cbde7f32 to your computer and use it in GitHub Desktop.
package se.emilsjolander.flipview;
import se.emilsjolander.flipview.Recycler.Scrap;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Camera;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.ListAdapter;
import android.widget.Scroller;
public class FlipView extends FrameLayout {
public interface OnFlipListener {
public void onFlippedToPage(FlipView v, int position, long id);
}
/**
*
* @author emilsjolander
*
* Class to hold a view and its corresponding info
*/
static class Page {
View v;
int position;
int viewType;
boolean valid;
}
// this will be the postion when there is not data
private static final int INVALID_PAGE_POSITION = -1;
// "null" flip distance
private static final int INVALID_FLIP_DISTANCE = -1;
private static final int PEAK_ANIM_DURATION = 600;// in ms
private static final int MAX_SINGLE_PAGE_FLIP_ANIM_DURATION = 300;// in ms
// for normalizing width/height
private static final int FLIP_DISTANCE_PER_PAGE = 180;
private static final int MAX_SHADOW_ALPHA = 180;// out of 255
private static final int MAX_SHADE_ALPHA = 130;// out of 255
private static final int MAX_SHINE_ALPHA = 100;// out of 255
// value for no pointer
private static final int INVALID_POINTER = -1;
// constant used by the attributes
@SuppressWarnings("unused")
private static final int VERTICAL_FLIP = 0;
// constant used by the attributes
@SuppressWarnings("unused")
private static final int HORIZONTAL_FLIP = 1;
private DataSetObserver dataSetObserver = new DataSetObserver() {
@Override
public void onChanged() {
dataSetChanged();
}
@Override
public void onInvalidated() {
dataSetInvalidated();
}
};
private Scroller mScroller;
private final Interpolator flipInterpolator = new DecelerateInterpolator();
private ValueAnimator mPeakAnim;
private TimeInterpolator mPeakInterpolator = new AccelerateDecelerateInterpolator();
private boolean mIsFlippingVertically = true;
private boolean mIsFlipping;
private boolean mIsUnableToFlip;
private boolean mIsFlippingEnabled = true;
private boolean mLastTouchAllowed = true;
private int mTouchSlop;
// keep track of pointer
private float mLastX = -1;
private float mLastY = -1;
private int mActivePointerId = INVALID_POINTER;
// velocity stuff
private VelocityTracker mVelocityTracker;
private int mMinimumVelocity;
private int mMaximumVelocity;
// views get recycled after they have been pushed out of the active queue
private Recycler mRecycler = new Recycler();
private ListAdapter mAdapter;
private int mPageCount = 0;
private Page mPreviousPage = new Page();
private Page mCurrentPage = new Page();
private Page mNextPage = new Page();
private OnFlipListener mOnFlipListener;
private float mFlipDistance = INVALID_FLIP_DISTANCE;
private int mCurrentPageIndex = INVALID_PAGE_POSITION;
private int mLastDispatchedPageEventIndex = 0;
private long mCurrentPageId = 0;
// clipping rects
private Rect mTopRect = new Rect();
private Rect mBottomRect = new Rect();
private Rect mRightRect = new Rect();
private Rect mLeftRect = new Rect();
// used for transforming the canvas
private Camera mCamera = new Camera();
private Matrix mMatrix = new Matrix();
// paints drawn above views when flipping
private Paint mShadowPaint = new Paint();
private Paint mShadePaint = new Paint();
private Paint mShinePaint = new Paint();
public FlipView(Context context) {
this(context, null);
}
public FlipView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlipView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
final Context context = getContext();
final ViewConfiguration configuration = ViewConfiguration.get(context);
mScroller = new Scroller(context, flipInterpolator);
mTouchSlop = configuration.getScaledPagingTouchSlop();
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
mShadowPaint.setColor(Color.BLACK);
mShadowPaint.setStyle(Style.FILL);
mShadePaint.setColor(Color.BLACK);
mShadePaint.setStyle(Style.FILL);
mShinePaint.setColor(Color.WHITE);
mShinePaint.setStyle(Style.FILL);
}
private void dataSetChanged() {
final int currentPage = mCurrentPageIndex;
int newPosition = currentPage;
// if the adapter has stable ids, try to keep the page currently on
// stable.
if (mAdapter.hasStableIds() && currentPage != INVALID_PAGE_POSITION) {
newPosition = getNewPositionOfCurrentPage();
} else if (currentPage == INVALID_PAGE_POSITION) {
newPosition = 0;
}
// remove all the current views
recycleActiveViews();
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
mRecycler.invalidateScraps();
mPageCount = mAdapter.getCount();
// put the current page within the new adapter range
newPosition = Math.min(mPageCount - 1, newPosition == INVALID_PAGE_POSITION ? 0 : newPosition);
if (newPosition != INVALID_PAGE_POSITION) {
// TODO pretty confusing
// this will be correctly set in setFlipDistance method
mCurrentPageIndex = INVALID_PAGE_POSITION;
mFlipDistance = INVALID_FLIP_DISTANCE;
flipTo(newPosition);
} else {
mFlipDistance = INVALID_FLIP_DISTANCE;
mPageCount = 0;
setFlipDistance(0);
}
}
private int getNewPositionOfCurrentPage() {
// check if id is on same position, this is because it will
// often be that and this way you do not need to iterate the whole
// dataset. If it is the same position, you are done.
if (mCurrentPageId == mAdapter.getItemId(mCurrentPageIndex)) {
return mCurrentPageIndex;
}
// iterate the dataset and look for the correct id. If it
// exists, set that position as the current position.
for (int i = 0; i < mAdapter.getCount(); i++) {
if (mCurrentPageId == mAdapter.getItemId(i)) {
return i;
}
}
// Id no longer is dataset, keep current page
return mCurrentPageIndex;
}
private void dataSetInvalidated() {
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(dataSetObserver);
mAdapter = null;
}
mRecycler = new Recycler();
removeAllViews();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getDefaultSize(0, widthMeasureSpec);
int height = getDefaultSize(0, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
int width = getDefaultSize(0, widthMeasureSpec);
int height = getDefaultSize(0, heightMeasureSpec);
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec);
}
}
@Override
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
child.measure(parentWidthMeasureSpec, parentHeightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
layoutChildren();
mTopRect.top = 0;
mTopRect.left = 0;
mTopRect.right = getWidth();
mTopRect.bottom = getHeight() / 2;
mBottomRect.top = getHeight() / 2;
mBottomRect.left = 0;
mBottomRect.right = getWidth();
mBottomRect.bottom = getHeight();
mLeftRect.top = 0;
mLeftRect.left = 0;
mLeftRect.right = getWidth() / 2;
mLeftRect.bottom = getHeight();
mRightRect.top = 0;
mRightRect.left = getWidth() / 2;
mRightRect.right = getWidth();
mRightRect.bottom = getHeight();
}
private void layoutChildren() {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
layoutChild(child);
}
}
private void layoutChild(View child) {
child.layout(0, 0, getWidth(), getHeight());
}
private void setFlipDistance(float flipDistance) {
if (mPageCount < 1) {
mFlipDistance = 0;
mCurrentPageIndex = INVALID_PAGE_POSITION;
mCurrentPageId = -1;
recycleActiveViews();
return;
}
if (flipDistance == mFlipDistance) {
return;
}
mFlipDistance = flipDistance;
int currentPageIndex = Math.round(mFlipDistance / FLIP_DISTANCE_PER_PAGE);
if (currentPageIndex == -1) {
mFlipDistance = mFlipDistance + FLIP_DISTANCE_PER_PAGE * (mAdapter.getCount());
currentPageIndex = mAdapter.getCount() - 1;
} else if (currentPageIndex == mAdapter.getCount()) {
mFlipDistance = mFlipDistance - FLIP_DISTANCE_PER_PAGE * (mAdapter.getCount());
currentPageIndex = 0;
}
if (mCurrentPageIndex != currentPageIndex) {
mCurrentPageIndex = currentPageIndex;
mCurrentPageId = mAdapter.getItemId(mCurrentPageIndex);
// TODO be smarter about this. Dont remove a view that will be added
// again on the next line.
recycleActiveViews();
// add the new active views
if (mCurrentPageIndex >= 0) {
int pre = mCurrentPageIndex - 1;
if (pre < 0) {
pre = mAdapter.getCount() - 1;
}
fillPageForIndex(mPreviousPage, pre);
addView(mPreviousPage.v);
}
if (mCurrentPageIndex >= 0 && mCurrentPageIndex < mPageCount) {
fillPageForIndex(mCurrentPage, mCurrentPageIndex);
addView(mCurrentPage.v);
}
if (mCurrentPageIndex <= mPageCount - 1) {
int next = mCurrentPageIndex + 1;
if (next >= mAdapter.getCount()) {
next = 0;
}
fillPageForIndex(mNextPage, next);
addView(mNextPage.v);
}
}
invalidate();
}
private void fillPageForIndex(Page p, int i) {
p.position = i;
p.viewType = mAdapter.getItemViewType(p.position);
p.v = getView(p.position, p.viewType);
p.valid = true;
}
private void recycleActiveViews() {
// remove and recycle the currently active views
if (mPreviousPage.valid) {
removeView(mPreviousPage.v);
mRecycler.addScrapView(mPreviousPage.v, mPreviousPage.position, mPreviousPage.viewType);
mPreviousPage.valid = false;
}
if (mCurrentPage.valid) {
removeView(mCurrentPage.v);
mRecycler.addScrapView(mCurrentPage.v, mCurrentPage.position, mCurrentPage.viewType);
mCurrentPage.valid = false;
}
if (mNextPage.valid) {
removeView(mNextPage.v);
mRecycler.addScrapView(mNextPage.v, mNextPage.position, mNextPage.viewType);
mNextPage.valid = false;
}
}
private View getView(int index, int viewType) {
// get the scrap from the recycler corresponding to the correct view
// type
Scrap scrap = mRecycler.getScrapView(index, viewType);
// get a view from the adapter if a scrap was not found or it is
// invalid.
View v = null;
if (scrap == null || !scrap.valid) {
v = mAdapter.getView(index, scrap == null ? null : scrap.v, this);
} else {
v = scrap.v;
}
// return view
return v;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!mIsFlippingEnabled) {
return false;
}
if (mPageCount < 1) {
return false;
}
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mIsFlipping = false;
mIsUnableToFlip = false;
mActivePointerId = INVALID_POINTER;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
return false;
}
if (action != MotionEvent.ACTION_DOWN) {
if (mIsFlipping) {
return true;
} else if (mIsUnableToFlip) {
return false;
}
}
switch (action) {
case MotionEvent.ACTION_MOVE:
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
break;
}
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
if (pointerIndex == -1) {
mActivePointerId = INVALID_POINTER;
break;
}
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float dx = x - mLastX;
final float xDiff = Math.abs(dx);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float dy = y - mLastY;
final float yDiff = Math.abs(dy);
if ((mIsFlippingVertically && yDiff > mTouchSlop && yDiff > xDiff)
|| (!mIsFlippingVertically && xDiff > mTouchSlop && xDiff > yDiff)) {
mIsFlipping = true;
mLastX = x;
mLastY = y;
} else if ((mIsFlippingVertically && xDiff > mTouchSlop) || (!mIsFlippingVertically && yDiff > mTouchSlop)) {
mIsUnableToFlip = true;
}
break;
case MotionEvent.ACTION_DOWN:
mActivePointerId = ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK;
mLastX = MotionEventCompat.getX(ev, mActivePointerId);
mLastY = MotionEventCompat.getY(ev, mActivePointerId);
mIsFlipping = !mScroller.isFinished() | mPeakAnim != null;
mIsUnableToFlip = false;
mLastTouchAllowed = true;
break;
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
if (!mIsFlipping) {
trackVelocity(ev);
}
return mIsFlipping;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!mIsFlippingEnabled) {
return false;
}
if (mPageCount < 1) {
return false;
}
if (!mIsFlipping && !mLastTouchAllowed) {
return false;
}
final int action = ev.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL
|| action == MotionEvent.ACTION_OUTSIDE) {
mLastTouchAllowed = false;
} else {
mLastTouchAllowed = true;
}
trackVelocity(ev);
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
// start flipping immediately if interrupting some sort of animation
if (endScroll() || endPeak()) {
mIsFlipping = true;
}
// Remember where the motion event started
mLastX = ev.getX();
mLastY = ev.getY();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
break;
case MotionEvent.ACTION_MOVE:
if (!mIsFlipping) {
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex == -1) {
mActivePointerId = INVALID_POINTER;
break;
}
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float xDiff = Math.abs(x - mLastX);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mLastY);
if ((mIsFlippingVertically && yDiff > mTouchSlop && yDiff > xDiff)
|| (!mIsFlippingVertically && xDiff > mTouchSlop && xDiff > yDiff)) {
mIsFlipping = true;
mLastX = x;
mLastY = y;
}
}
if (mIsFlipping) {
// Scroll to follow the motion event
final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (activePointerIndex == -1) {
mActivePointerId = INVALID_POINTER;
break;
}
final float x = MotionEventCompat.getX(ev, activePointerIndex);
final float deltaX = mLastX - x;
final float y = MotionEventCompat.getY(ev, activePointerIndex);
final float deltaY = mLastY - y;
mLastX = x;
mLastY = y;
float deltaFlipDistance = 0;
if (mIsFlippingVertically) {
deltaFlipDistance = deltaY;
} else {
deltaFlipDistance = deltaX;
}
deltaFlipDistance /= ((isFlippingVertically() ? getHeight() : getWidth()) / FLIP_DISTANCE_PER_PAGE);
setFlipDistance(mFlipDistance + deltaFlipDistance);
// final int minFlipDistance = 0;
// final int maxFlipDistance = (mPageCount - 1)
// * FLIP_DISTANCE_PER_PAGE;
// final boolean isOverFlipping = mFlipDistance <
// minFlipDistance || mFlipDistance > maxFlipDistance;
// if (isOverFlipping) {
// mIsOverFlipping = true;
// setFlipDistance(mOverFlipper.calculate(mFlipDistance,
// minFlipDistance, maxFlipDistance));
// if (mOnOverFlipListener != null) {
// float overFlip = mOverFlipper.getTotalOverFlip();
// mOnOverFlipListener.onOverFlip(this, mOverFlipMode,
// overFlip < 0, Math.abs(overFlip),
// FLIP_DISTANCE_PER_PAGE);
// }
// } else if (mIsOverFlipping) {
// mIsOverFlipping = false;
// if (mOnOverFlipListener != null) {
// // TODO in the future should only notify flip distance 0
// // on the correct edge (previous/next)
// mOnOverFlipListener.onOverFlip(this, mOverFlipMode,
// false, 0, FLIP_DISTANCE_PER_PAGE);
// mOnOverFlipListener.onOverFlip(this, mOverFlipMode,
// true, 0, FLIP_DISTANCE_PER_PAGE);
// }
// }
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mIsFlipping) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocity = 0;
if (isFlippingVertically()) {
velocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker, mActivePointerId);
} else {
velocity = (int) VelocityTrackerCompat.getXVelocity(velocityTracker, mActivePointerId);
}
smoothFlipTo(getNextPage(velocity));
mActivePointerId = INVALID_POINTER;
endFlip();
// mOverFlipper.overFlipEnded();
}
break;
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int index = MotionEventCompat.getActionIndex(ev);
final float x = MotionEventCompat.getX(ev, index);
final float y = MotionEventCompat.getY(ev, index);
mLastX = x;
mLastY = y;
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, index);
final float y = MotionEventCompat.getY(ev, index);
mLastX = x;
mLastY = y;
break;
}
if (mActivePointerId == INVALID_POINTER) {
mLastTouchAllowed = false;
}
return true;
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (mPageCount < 1) {
return;
}
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
setFlipDistance(mScroller.getCurrY());
}
if (mIsFlipping || !mScroller.isFinished() || mPeakAnim != null) {
showAllPages();
drawPreviousHalf(canvas);
drawNextHalf(canvas);
drawFlippingHalf(canvas);
invalidate();
} else {
endScroll();
setDrawWithLayer(mCurrentPage.v, false);
hideOtherPages(mCurrentPage);
drawChild(canvas, mCurrentPage.v, 0);
// dispatch listener event now that we have "landed" on a page.
// TODO not the prettiest to have this with the drawing logic,
// should change.
if (mLastDispatchedPageEventIndex != mCurrentPageIndex) {
mLastDispatchedPageEventIndex = mCurrentPageIndex;
postFlippedToPage(mCurrentPageIndex);
}
}
// if overflip is GLOW mode and the edge effects needed drawing, make
// sure to invalidate
// if (mOverFlipper.draw(canvas)) {
// // always invalidate whole screen as it is needed 99% of the time.
// // This is because of the shadows and shines put on the non-flipping
// // pages
// invalidate();
// }
}
private void hideOtherPages(Page p) {
if (mPreviousPage != p && mPreviousPage.valid && mPreviousPage.v.getVisibility() != GONE) {
mPreviousPage.v.setVisibility(GONE);
}
if (mCurrentPage != p && mCurrentPage.valid && mCurrentPage.v.getVisibility() != GONE) {
mCurrentPage.v.setVisibility(GONE);
}
if (mNextPage != p && mNextPage.valid && mNextPage.v.getVisibility() != GONE) {
mNextPage.v.setVisibility(GONE);
}
p.v.setVisibility(VISIBLE);
}
private void showAllPages() {
if (mPreviousPage.valid && mPreviousPage.v.getVisibility() != VISIBLE) {
mPreviousPage.v.setVisibility(VISIBLE);
}
if (mCurrentPage.valid && mCurrentPage.v.getVisibility() != VISIBLE) {
mCurrentPage.v.setVisibility(VISIBLE);
}
if (mNextPage.valid && mNextPage.v.getVisibility() != VISIBLE) {
mNextPage.v.setVisibility(VISIBLE);
}
}
/**
* draw top/left half
*
* @param canvas
*/
private void drawPreviousHalf(Canvas canvas) {
canvas.save();
canvas.clipRect(isFlippingVertically() ? mTopRect : mLeftRect);
final float degreesFlipped = getDegreesFlipped();
final Page p = degreesFlipped > 90 ? mPreviousPage : mCurrentPage;
// if the view does not exist, skip drawing it
if (p.valid) {
setDrawWithLayer(p.v, true);
drawChild(canvas, p.v, 0);
}
drawPreviousShadow(canvas);
canvas.restore();
}
/**
* draw top/left half shadow
*
* @param canvas
*/
private void drawPreviousShadow(Canvas canvas) {
final float degreesFlipped = getDegreesFlipped();
if (degreesFlipped > 90) {
final int alpha = (int) (((degreesFlipped - 90) / 90f) * MAX_SHADOW_ALPHA);
mShadowPaint.setAlpha(alpha);
canvas.drawPaint(mShadowPaint);
}
}
/**
* draw bottom/right half
*
* @param canvas
*/
private void drawNextHalf(Canvas canvas) {
canvas.save();
canvas.clipRect(isFlippingVertically() ? mBottomRect : mRightRect);
final float degreesFlipped = getDegreesFlipped();
final Page p = degreesFlipped > 90 ? mCurrentPage : mNextPage;
// if the view does not exist, skip drawing it
if (p.valid) {
setDrawWithLayer(p.v, true);
drawChild(canvas, p.v, 0);
}
drawNextShadow(canvas);
canvas.restore();
}
/**
* draw bottom/right half shadow
*
* @param canvas
*/
private void drawNextShadow(Canvas canvas) {
final float degreesFlipped = getDegreesFlipped();
if (degreesFlipped < 90) {
final int alpha = (int) ((Math.abs(degreesFlipped - 90) / 90f) * MAX_SHADOW_ALPHA);
mShadowPaint.setAlpha(alpha);
canvas.drawPaint(mShadowPaint);
}
}
private void drawFlippingHalf(Canvas canvas) {
canvas.save();
mCamera.save();
final float degreesFlipped = getDegreesFlipped();
if (degreesFlipped > 90) {
canvas.clipRect(isFlippingVertically() ? mTopRect : mLeftRect);
if (mIsFlippingVertically) {
mCamera.rotateX(degreesFlipped - 180);
} else {
mCamera.rotateY(180 - degreesFlipped);
}
} else {
canvas.clipRect(isFlippingVertically() ? mBottomRect : mRightRect);
if (mIsFlippingVertically) {
mCamera.rotateX(degreesFlipped);
} else {
mCamera.rotateY(-degreesFlipped);
}
}
mCamera.getMatrix(mMatrix);
positionMatrix();
canvas.concat(mMatrix);
setDrawWithLayer(mCurrentPage.v, true);
drawChild(canvas, mCurrentPage.v, 0);
drawFlippingShadeShine(canvas);
mCamera.restore();
canvas.restore();
}
/**
* will draw a shade if flipping on the previous(top/left) half and a shine
* if flipping on the next(bottom/right) half
*
* @param canvas
*/
private void drawFlippingShadeShine(Canvas canvas) {
final float degreesFlipped = getDegreesFlipped();
if (degreesFlipped < 90) {
final int alpha = (int) ((degreesFlipped / 90f) * MAX_SHINE_ALPHA);
mShinePaint.setAlpha(alpha);
canvas.drawRect(isFlippingVertically() ? mBottomRect : mRightRect, mShinePaint);
} else {
final int alpha = (int) ((Math.abs(degreesFlipped - 180) / 90f) * MAX_SHADE_ALPHA);
mShadePaint.setAlpha(alpha);
canvas.drawRect(isFlippingVertically() ? mTopRect : mLeftRect, mShadePaint);
}
}
/**
* Enable a hardware layer for the view.
*
* @param v
* @param drawWithLayer
*/
private void setDrawWithLayer(View v, boolean drawWithLayer) {
if (isHardwareAccelerated()) {
if (v.getLayerType() != LAYER_TYPE_HARDWARE && drawWithLayer) {
v.setLayerType(LAYER_TYPE_HARDWARE, null);
} else if (v.getLayerType() != LAYER_TYPE_NONE && !drawWithLayer) {
v.setLayerType(LAYER_TYPE_NONE, null);
}
}
}
private void positionMatrix() {
mMatrix.preScale(0.25f, 0.25f);
mMatrix.postScale(4.0f, 4.0f);
mMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2);
mMatrix.postTranslate(getWidth() / 2, getHeight() / 2);
}
private float getDegreesFlipped() {
float localFlipDistance = mFlipDistance % FLIP_DISTANCE_PER_PAGE;
// fix for negative modulo. always want a positive flip degree
if (localFlipDistance < 0) {
localFlipDistance += FLIP_DISTANCE_PER_PAGE;
}
return (localFlipDistance / FLIP_DISTANCE_PER_PAGE) * 180;
}
private void postFlippedToPage(final int page) {
post(new Runnable() {
@Override
public void run() {
if (mOnFlipListener != null) {
mOnFlipListener.onFlippedToPage(FlipView.this, page, mAdapter.getItemId(page));
}
}
});
}
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;
mLastX = MotionEventCompat.getX(ev, newPointerIndex);
mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
if (mVelocityTracker != null) {
mVelocityTracker.clear();
}
}
}
/**
*
* @param deltaFlipDistance
* The distance to flip.
* @return The duration for a flip, bigger deltaFlipDistance = longer
* duration. The increase if duration gets smaller for bigger values
* of deltaFlipDistance.
*/
private int getFlipDuration(int deltaFlipDistance) {
float distance = Math.abs(deltaFlipDistance);
return (int) (MAX_SINGLE_PAGE_FLIP_ANIM_DURATION * Math.sqrt(distance / FLIP_DISTANCE_PER_PAGE));
}
/**
*
* @param velocity
* @return the page you should "land" on
*/
private int getNextPage(int velocity) {
int nextPage;
if (velocity > mMinimumVelocity) {
nextPage = getCurrentPageFloor();
} else if (velocity < -mMinimumVelocity) {
nextPage = getCurrentPageCeil();
} else {
nextPage = getCurrentPageRound();
}
return Math.min(Math.max(nextPage, 0), mPageCount - 1);
}
private int getCurrentPageRound() {
return Math.round(mFlipDistance / FLIP_DISTANCE_PER_PAGE);
}
private int getCurrentPageFloor() {
return (int) Math.floor(mFlipDistance / FLIP_DISTANCE_PER_PAGE);
}
private int getCurrentPageCeil() {
return (int) Math.ceil(mFlipDistance / FLIP_DISTANCE_PER_PAGE);
}
/**
*
* @return true if ended a flip
*/
private boolean endFlip() {
final boolean wasflipping = mIsFlipping;
mIsFlipping = false;
mIsUnableToFlip = false;
mLastTouchAllowed = false;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
return wasflipping;
}
/**
*
* @return true if ended a scroll
*/
private boolean endScroll() {
final boolean wasScrolling = !mScroller.isFinished();
mScroller.abortAnimation();
return wasScrolling;
}
/**
*
* @return true if ended a peak
*/
private boolean endPeak() {
final boolean wasPeaking = mPeakAnim != null;
if (mPeakAnim != null) {
mPeakAnim.cancel();
mPeakAnim = null;
}
return wasPeaking;
}
private void peak(boolean next, boolean once) {
final float baseFlipDistance = mCurrentPageIndex * FLIP_DISTANCE_PER_PAGE;
if (next) {
mPeakAnim = ValueAnimator.ofFloat(baseFlipDistance, baseFlipDistance + FLIP_DISTANCE_PER_PAGE / 4);
} else {
mPeakAnim = ValueAnimator.ofFloat(baseFlipDistance, baseFlipDistance - FLIP_DISTANCE_PER_PAGE / 4);
}
mPeakAnim.setInterpolator(mPeakInterpolator);
mPeakAnim.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setFlipDistance((Float) animation.getAnimatedValue());
}
});
mPeakAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
endPeak();
}
});
mPeakAnim.setDuration(PEAK_ANIM_DURATION);
mPeakAnim.setRepeatMode(ValueAnimator.REVERSE);
mPeakAnim.setRepeatCount(once ? 1 : ValueAnimator.INFINITE);
mPeakAnim.start();
}
private void trackVelocity(MotionEvent ev) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
}
/* ---------- API ---------- */
/**
*
* @param adapter
* a regular ListAdapter, not all methods if the list adapter are
* used by the flipview
*
*/
public void setAdapter(ListAdapter adapter) {
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(dataSetObserver);
}
// remove all the current views
removeAllViews();
mAdapter = adapter;
mPageCount = adapter == null ? 0 : mAdapter.getCount();
if (adapter != null) {
mAdapter.registerDataSetObserver(dataSetObserver);
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
mRecycler.invalidateScraps();
}
// TODO pretty confusing
// this will be correctly set in setFlipDistance method
mCurrentPageIndex = INVALID_PAGE_POSITION;
mFlipDistance = INVALID_FLIP_DISTANCE;
setFlipDistance(0);
}
public ListAdapter getAdapter() {
return mAdapter;
}
public int getPageCount() {
return mPageCount;
}
public int getCurrentPage() {
return mCurrentPageIndex;
}
public void flipTo(int page) {
if (page < 0 || page > mPageCount - 1) {
throw new IllegalArgumentException("That page does not exist");
}
endFlip();
setFlipDistance(page * FLIP_DISTANCE_PER_PAGE);
}
public void flipBy(int delta) {
flipTo(mCurrentPageIndex + delta);
}
public void smoothFlipTo(int page) {
if (page < 0 || page > mPageCount - 1) {
throw new IllegalArgumentException("That page does not exist");
}
final int start = (int) mFlipDistance;
final int delta = page * FLIP_DISTANCE_PER_PAGE - start;
endFlip();
mScroller.startScroll(0, start, 0, delta, getFlipDuration(delta));
invalidate();
}
public void smoothFlipBy(int delta) {
smoothFlipTo(mCurrentPageIndex + delta);
}
/**
* Hint that there is a next page will do nothing if there is no next page
*
* @param once
* if true, only peak once. else peak until user interacts with
* view
*/
public void peakNext(boolean once) {
if (mCurrentPageIndex < mPageCount - 1) {
peak(true, once);
}
}
/**
* Hint that there is a previous page will do nothing if there is no
* previous page
*
* @param once
* if true, only peak once. else peak until user interacts with
* view
*/
public void peakPrevious(boolean once) {
if (mCurrentPageIndex > 0) {
peak(false, once);
}
}
/**
*
* @return true if the view is flipping vertically, can only be set via xml
* attribute "orientation"
*/
public boolean isFlippingVertically() {
return mIsFlippingVertically;
}
/**
* The OnFlipListener will notify you when a page has been fully turned.
*
* @param onFlipListener
*/
public void setOnFlipListener(OnFlipListener onFlipListener) {
mOnFlipListener = onFlipListener;
}
}
package se.emilsjolander.flipview;
import android.annotation.TargetApi;
import android.os.Build;
import android.util.SparseArray;
import android.view.View;
public class Recycler {
static class Scrap {
View v;
boolean valid;
public Scrap(View scrap, boolean valid) {
this.v = scrap;
this.valid = valid;
}
}
/** Unsorted views that can be used by the adapter as a convert view. */
private SparseArray<Scrap>[] scraps;
private SparseArray<Scrap> currentScraps;
private int viewTypeCount;
void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
// do nothing if the view type count has not changed.
if (currentScraps != null && viewTypeCount == scraps.length) {
return;
}
// noinspection unchecked
@SuppressWarnings("unchecked")
SparseArray<Scrap>[] scrapViews = new SparseArray[viewTypeCount];
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new SparseArray<Scrap>();
}
this.viewTypeCount = viewTypeCount;
currentScraps = scrapViews[0];
this.scraps = scrapViews;
}
/** @return A view from the ScrapViews collection. These are unordered. */
Scrap getScrapView(int position, int viewType) {
if (viewTypeCount == 1) {
return retrieveFromScrap(currentScraps, position);
} else if (viewType >= 0 && viewType < scraps.length) {
return retrieveFromScrap(scraps[viewType], position);
}
return null;
}
/**
* Put a view into the ScrapViews list. These views are unordered.
*
* @param scrap
* The view to add
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
void addScrapView(View scrap, int position, int viewType) {
// create a new Scrap
Scrap item = new Scrap(scrap, true);
if (viewTypeCount == 1) {
currentScraps.put(position, item);
} else {
scraps[viewType].put(position, item);
}
if (Build.VERSION.SDK_INT >= 14) {
scrap.setAccessibilityDelegate(null);
}
}
static Scrap retrieveFromScrap(SparseArray<Scrap> scrapViews, int position) {
int size = scrapViews.size();
if (size > 0) {
// See if we still have a view for this position.
Scrap result = scrapViews.get(position, null);
if (result != null) {
scrapViews.remove(position);
return result;
}
int index = size - 1;
result = scrapViews.valueAt(index);
scrapViews.removeAt(index);
result.valid = false;
return result;
}
return null;
}
void invalidateScraps() {
for (SparseArray<Scrap> array : scraps) {
for (int i = 0; i < array.size(); i++) {
array.valueAt(i).valid = false;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment