Created
September 24, 2015 07:13
-
-
Save goodev/0b32980f9ec4cbde7f32 to your computer and use it in GitHub Desktop.
https://github.com/emilsjolander/android-FlipView circularFlipView
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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