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.content.Context; import android.content.res.TypedArray; 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); } public interface OnOverFlipListener { public void onOverFlip(FlipView v, OverFlipMode mode, boolean overFlippingPrevious, float overFlipDistance, float flipDistancePerPage); } /** * * @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 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; private boolean mIsOverFlipping; // 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 View mEmptyView; private OnFlipListener mOnFlipListener; private OnOverFlipListener mOnOverFlipListener; private float mFlipDistance = INVALID_FLIP_DISTANCE; private int mCurrentPageIndex = INVALID_PAGE_POSITION; private int mLastDispatchedPageEventIndex = 0; private long mCurrentPageId = 0; private OverFlipMode mOverFlipMode; private OverFlipper mOverFlipper; // 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); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlipView); // 0 is vertical, 1 is horizontal mIsFlippingVertically = a.getInt(R.styleable.FlipView_orientation, VERTICAL_FLIP) == VERTICAL_FLIP; setOverFlipMode(OverFlipMode.values()[a.getInt( R.styleable.FlipView_overFlipMode, 0)]); a.recycle(); 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); } updateEmptyStatus(); } 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; final int currentPageIndex = (int) Math.round(mFlipDistance / FLIP_DISTANCE_PER_PAGE); 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) { fillPageForIndex(mPreviousPage, mCurrentPageIndex - 1); addView(mPreviousPage.v); } if (mCurrentPageIndex >= 0 && mCurrentPageIndex < mPageCount) { fillPageForIndex(mCurrentPage, mCurrentPageIndex); addView(mCurrentPage.v); } if (mCurrentPageIndex < mPageCount - 1) { fillPageForIndex(mNextPage, mCurrentPageIndex + 1); 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; } @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); } 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); } private void updateEmptyStatus() { boolean empty = mAdapter == null || mPageCount == 0; if (empty) { if (mEmptyView != null) { mEmptyView.setVisibility(View.VISIBLE); setVisibility(View.GONE); } else { setVisibility(View.VISIBLE); } } else { if (mEmptyView != null) { mEmptyView.setVisibility(View.GONE); } setVisibility(View.VISIBLE); } } /* ---------- 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); updateEmptyStatus(); } 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; } /** * The OnOverFlipListener will notify of over flipping. This is a great * listener to have when implementing pull-to-refresh * * @param onOverFlipListener */ public void setOnOverFlipListener(OnOverFlipListener onOverFlipListener) { this.mOnOverFlipListener = onOverFlipListener; } /** * * @return the overflip mode of this flipview. Default is GLOW */ public OverFlipMode getOverFlipMode() { return mOverFlipMode; } /** * Set the overflip mode of the flipview. GLOW is the standard seen in all * andriod lists. RUBBER_BAND is more like iOS lists which list you flip * past the first/last page but adding friction, like a rubber band. * * @param overFlipMode */ public void setOverFlipMode(OverFlipMode overFlipMode) { this.mOverFlipMode = overFlipMode; mOverFlipper = OverFlipperFactory.create(this, mOverFlipMode); } /** * @param emptyView * The view to show when either no adapter is set or the adapter * has no items. This should be a view already in the view * hierarchy which the FlipView will set the visibility of. */ public void setEmptyView(View emptyView) { mEmptyView = emptyView; updateEmptyStatus(); } }