packagecom.xu81.testflip;
importandroid.content.Context;
importandroid.view.GestureDetector;
importandroid.view.KeyEvent;
importandroid.view.MotionEvent;
importandroid.view.View;
importandroid.view.animation.Animation;
importandroid.view.animation.AnimationUtils;
importandroid.view.animation.Interpolator;
importandroid.view.animation.Transformation;
importandroid.widget.Adapter;
importandroid.widget.FrameLayout;
importandroid.widget.LinearLayout;
// TODO:
// 1. In order to improve performance Cache screen bitmap and use for animation
// 2. Establish superfluous memory allocations and delay or replace with reused objects
// Probably need to make sure we are not allocating objects (strings, etc.) in loops
publicclassFlingGalleryextendsFrameLayout
{
// Constants
privatefinalintswipe_min_distance =120;
privatefinalintswipe_max_off_path =250;
privatefinalintswipe_threshold_veloicty =400;
// Properties
privateintmViewPaddingWidth =0;
privateintmAnimationDuration =250;
privatefloatmSnapBorderRatio =0.5f;
privatebooleanmIsGalleryCircular =true;
// Members
privateintmGalleryWidth =0;
privatebooleanmIsTouched =false;
privatebooleanmIsDragging =false;
privatefloatmCurrentOffset =0.0f;
privatelongmScrollTimestamp =0;
privateintmFlingDirection =0;
privateintmCurrentPosition =0;
privateintmCurrentViewNumber =0;
privateContext mContext;
privateAdapter mAdapter;
privateFlingGalleryView[] mViews;
privateFlingGalleryAnimation mAnimation;
privateGestureDetector mGestureDetector;
privateInterpolator mDecelerateInterpolater;
publicFlingGallery(Context context)
{
super(context);
mContext = context;
mAdapter = null;
mViews = newFlingGalleryView[3];
mViews[0] =newFlingGalleryView(0,this);
mViews[1] =newFlingGalleryView(1,this);
mViews[2] =newFlingGalleryView(2,this);
mAnimation = newFlingGalleryAnimation();
mGestureDetector = newGestureDetector(newFlingGestureDetector());
mDecelerateInterpolater = AnimationUtils.loadInterpolator(mContext, android.R.anim.decelerate_interpolator);
}
publicvoidsetPaddingWidth(intviewPaddingWidth)
{
mViewPaddingWidth = viewPaddingWidth;
}
publicvoidsetAnimationDuration(intanimationDuration)
{
mAnimationDuration = animationDuration;
}
publicvoidsetSnapBorderRatio(floatsnapBorderRatio)
{
mSnapBorderRatio = snapBorderRatio;
}
publicvoidsetIsGalleryCircular(booleanisGalleryCircular)
{
if(mIsGalleryCircular != isGalleryCircular)
{
mIsGalleryCircular = isGalleryCircular;
if(mCurrentPosition == getFirstPosition())
{
// We need to reload the view immediately to the left to change it to circular view or blank
mViews[getPrevViewNumber(mCurrentViewNumber)].recycleView(getPrevPosition(mCurrentPosition));
}
if(mCurrentPosition == getLastPosition())
{
// We need to reload the view immediately to the right to change it to circular view or blank
mViews[getNextViewNumber(mCurrentViewNumber)].recycleView(getNextPosition(mCurrentPosition));
}
}
}
publicintgetGalleryCount()
{
return(mAdapter ==null) ?0: mAdapter.getCount();
}
publicintgetFirstPosition()
{
return0;
}
publicintgetLastPosition()
{
return(getGalleryCount() ==0) ?0: getGalleryCount() -1;
}
privateintgetPrevPosition(intrelativePosition)
{
intprevPosition = relativePosition -1;
if(prevPosition
{
prevPosition = getFirstPosition() - 1;
if(mIsGalleryCircular ==true)
{
prevPosition = getLastPosition();
}
}
returnprevPosition;
}
privateintgetNextPosition(intrelativePosition)
{
intnextPosition = relativePosition +1;
if(nextPosition > getLastPosition())
{
nextPosition = getLastPosition() + 1;
if(mIsGalleryCircular ==true)
{
nextPosition = getFirstPosition();
}
}
returnnextPosition;
}
privateintgetPrevViewNumber(intrelativeViewNumber)
{
return(relativeViewNumber ==0) ?2: relativeViewNumber -1;
}
privateintgetNextViewNumber(intrelativeViewNumber)
{
return(relativeViewNumber ==2) ?0: relativeViewNumber +1;
}
@Override
protectedvoidonLayout(booleanchanged,intleft,inttop,intright,intbottom)
{
super.onLayout(changed, left, top, right, bottom);
// Calculate our view width
mGalleryWidth = right - left;
if(changed ==true)
{
// Position views at correct starting offsets
mViews[0].setOffset(0,0, mCurrentViewNumber);
mViews[1].setOffset(0,0, mCurrentViewNumber);
mViews[2].setOffset(0,0, mCurrentViewNumber);
}
}
publicvoidsetAdapter(Adapter adapter)
{
mAdapter = adapter;
mCurrentPosition = 0;
mCurrentViewNumber = 0;
// Load the initial views from adapter
mViews[0].recycleView(mCurrentPosition);
mViews[1].recycleView(getNextPosition(mCurrentPosition));
mViews[2].recycleView(getPrevPosition(mCurrentPosition));
// Position views at correct starting offsets
mViews[0].setOffset(0,0, mCurrentViewNumber);
mViews[1].setOffset(0,0, mCurrentViewNumber);
mViews[2].setOffset(0,0, mCurrentViewNumber);
}
privateintgetViewOffset(intviewNumber,intrelativeViewNumber)
{
// Determine width including configured padding width
intoffsetWidth = mGalleryWidth + mViewPaddingWidth;
// Position the previous view one measured width to left
if(viewNumber == getPrevViewNumber(relativeViewNumber))
{
returnoffsetWidth;
}
// Position the next view one measured width to the right
if(viewNumber == getNextViewNumber(relativeViewNumber))
{
returnoffsetWidth * -1;
}
return0;
}
voidmovePrevious()
{
// Slide to previous view
mFlingDirection = 1;
processGesture();
}
voidmoveNext()
{
// Slide to next view
mFlingDirection = -1;
processGesture();
}
@Override
publicbooleanonKeyDown(intkeyCode, KeyEvent event)
{
switch(keyCode)
{
caseKeyEvent.KEYCODE_DPAD_LEFT:
movePrevious();
returntrue;
caseKeyEvent.KEYCODE_DPAD_RIGHT:
moveNext();
returntrue;
caseKeyEvent.KEYCODE_DPAD_CENTER:
caseKeyEvent.KEYCODE_ENTER:
}
returnsuper.onKeyDown(keyCode, event);
}
publicbooleanonGalleryTouchEvent(MotionEvent event)
{
booleanconsumed = mGestureDetector.onTouchEvent(event);
if(event.getAction() == MotionEvent.ACTION_UP)
{
if(mIsTouched || mIsDragging)
{
processScrollSnap();
processGesture();
}
}
returnconsumed;
}
voidprocessGesture()
{
intnewViewNumber = mCurrentViewNumber;
intreloadViewNumber =0;
intreloadPosition =0;
mIsTouched = false;
mIsDragging = false;
if(mFlingDirection >0)
{
if(mCurrentPosition > getFirstPosition() || mIsGalleryCircular ==true)
{
// Determine previous view and outgoing view to recycle
newViewNumber = getPrevViewNumber(mCurrentViewNumber);
mCurrentPosition = getPrevPosition(mCurrentPosition);
reloadViewNumber = getNextViewNumber(mCurrentViewNumber);
reloadPosition = getPrevPosition(mCurrentPosition);
}
}
if(mFlingDirection <0)
{
if(mCurrentPosition
{
// Determine the next view and outgoing view to recycle
newViewNumber = getNextViewNumber(mCurrentViewNumber);
mCurrentPosition = getNextPosition(mCurrentPosition);
reloadViewNumber = getPrevViewNumber(mCurrentViewNumber);
reloadPosition = getNextPosition(mCurrentPosition);
}
}
if(newViewNumber != mCurrentViewNumber)
{
mCurrentViewNumber = newViewNumber;
// Reload outgoing view from adapter in new position
mViews[reloadViewNumber].recycleView(reloadPosition);
}
// Ensure input focus on the current view
mViews[mCurrentViewNumber].requestFocus();
// Run the slide animations for view transitions
mAnimation.prepareAnimation(mCurrentViewNumber);
this.startAnimation(mAnimation);
// Reset fling state
mFlingDirection = 0;
}
voidprocessScrollSnap()
{
// Snap to next view if scrolled passed snap position
floatrollEdgeWidth = mGalleryWidth * mSnapBorderRatio;
introllOffset = mGalleryWidth - (int) rollEdgeWidth;
intcurrentOffset = mViews[mCurrentViewNumber].getCurrentOffset();
if(currentOffset <= rollOffset * -1)
{
// Snap to previous view
mFlingDirection = 1;
}
if(currentOffset >= rollOffset)
{
// Snap to next view
mFlingDirection = -1;
}
}
privateclassFlingGalleryView
{
privateintmViewNumber;
privateFrameLayout mParentLayout;
privateFrameLayout mInvalidLayout =null;
privateLinearLayout mInternalLayout =null;
privateView mExternalView =null;
publicFlingGalleryView(intviewNumber, FrameLayout parentLayout)
{
mViewNumber = viewNumber;
mParentLayout = parentLayout;
// Invalid layout is used when outside gallery
mInvalidLayout = newFrameLayout(mContext);
mInvalidLayout.setLayoutParams(newLinearLayout.LayoutParams(
LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
// Internal layout is permanent for duration
mInternalLayout = newLinearLayout(mContext);
mInternalLayout.setLayoutParams(newLinearLayout.LayoutParams(
LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
mParentLayout.addView(mInternalLayout);
}
publicvoidrecycleView(intnewPosition)
{
if(mExternalView !=null)
{
mInternalLayout.removeView(mExternalView);
}
if(mAdapter !=null)
{
if(newPosition >= getFirstPosition() && newPosition <= getLastPosition())
{
mExternalView = mAdapter.getView(newPosition, mExternalView, mInternalLayout);
}
else
{
mExternalView = mInvalidLayout;
}
}
if(mExternalView !=null)
{
mInternalLayout.addView(mExternalView, newLinearLayout.LayoutParams(
LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
}
}
publicvoidsetOffset(intxOffset,intyOffset,intrelativeViewNumber)
{
// Scroll the target view relative to its own position relative to currently displayed view
mInternalLayout.scrollTo(getViewOffset(mViewNumber, relativeViewNumber) + xOffset, yOffset);
}
publicintgetCurrentOffset()
{
// Return the current scroll position
returnmInternalLayout.getScrollX();
}
publicvoidrequestFocus()
{
mInternalLayout.requestFocus();
}
}
privateclassFlingGalleryAnimationextendsAnimation
{
privatebooleanmIsAnimationInProgres;
privateintmRelativeViewNumber;
privateintmInitialOffset;
privateintmTargetOffset;
privateintmTargetDistance;
publicFlingGalleryAnimation()
{
mIsAnimationInProgres = false;
mRelativeViewNumber = 0;
mInitialOffset = 0;
mTargetOffset = 0;
mTargetDistance = 0;
}
publicvoidprepareAnimation(intrelativeViewNumber)
{
// If we are animating relative to a new view
if(mRelativeViewNumber != relativeViewNumber)
{
if(mIsAnimationInProgres ==true)
{
// We only have three views so if requested again to animate in same direction we must snap
intnewDirection = (relativeViewNumber == getPrevViewNumber(mRelativeViewNumber)) ?1: -1;
intanimDirection = (mTargetDistance <0) ?1: -1;
// If animation in same direction
if(animDirection == newDirection)
{
// Ran out of time to animate so snap to the target offset
mViews[0].setOffset(mTargetOffset,0, mRelativeViewNumber);
mViews[1].setOffset(mTargetOffset,0, mRelativeViewNumber);
mViews[2].setOffset(mTargetOffset,0, mRelativeViewNumber);
}
}
// Set relative view number for animation
mRelativeViewNumber = relativeViewNumber;
}
// Note: In this implementation the targetOffset will always be zero
// as we are centering the view; but we include the calculations of
// targetOffset and targetDistance for use in future implementations
mInitialOffset = mViews[mRelativeViewNumber].getCurrentOffset();
mTargetOffset = getViewOffset(mRelativeViewNumber, mRelativeViewNumber);
mTargetDistance = mTargetOffset - mInitialOffset;
// Configure base animation properties
this.setDuration(mAnimationDuration);
this.setInterpolator(mDecelerateInterpolater);
// Start/continued animation
mIsAnimationInProgres = true;
}
@Override
protectedvoidapplyTransformation(floatinterpolatedTime, Transformation transformation)
{
// Ensure interpolatedTime does not over-shoot then calculate new offset
interpolatedTime = (interpolatedTime > 1.0f) ?1.0f : interpolatedTime;
intoffset = mInitialOffset + (int) (mTargetDistance * interpolatedTime);
for(intviewNumber =0; viewNumber <3; viewNumber++)
{
// Only need to animate the visible views as the other view will always be off-screen
if((mTargetDistance >0&& viewNumber != getNextViewNumber(mRelativeViewNumber)) ||
(mTargetDistance
{
mViews[viewNumber].setOffset(offset, 0, mRelativeViewNumber);
}
}
}
@Override
publicbooleangetTransformation(longcurrentTime, Transformation outTransformation)
{
if(super.getTransformation(currentTime, outTransformation) ==false)
{
// Perform final adjustment to offsets to cleanup animation
mViews[0].setOffset(mTargetOffset,0, mRelativeViewNumber);
mViews[1].setOffset(mTargetOffset,0, mRelativeViewNumber);
mViews[2].setOffset(mTargetOffset,0, mRelativeViewNumber);
// Reached the animation target
mIsAnimationInProgres = false;
returnfalse;
}
// Cancel if the screen touched
if(mIsTouched || mIsDragging)
{
// Note that at this point we still consider ourselves to be animating
// because we have not yet reached the target offset; its just that the
// user has temporarily interrupted the animation with a touch gesture
returnfalse;
}
returntrue;
}
}
privateclassFlingGestureDetectorextendsGestureDetector.SimpleOnGestureListener
{
@Override
publicbooleanonDown(MotionEvent e)
{
// Stop animation
mIsTouched = true;
// Reset fling state
mFlingDirection = 0;
returntrue;
}
@Override
publicbooleanonScroll(MotionEvent e1, MotionEvent e2,floatdistanceX,floatdistanceY)
{
if(e2.getAction() == MotionEvent.ACTION_MOVE)
{
if(mIsDragging ==false)
{
// Stop animation
mIsTouched = true;
// Reconfigure scroll
mIsDragging = true;
mFlingDirection = 0;
mScrollTimestamp = System.currentTimeMillis();
mCurrentOffset = mViews[mCurrentViewNumber].getCurrentOffset();
}
floatmaxVelocity = mGalleryWidth / (mAnimationDuration /1000.0f);
longtimestampDelta = System.currentTimeMillis() - mScrollTimestamp;
floatmaxScrollDelta = maxVelocity * (timestampDelta /1000.0f);
floatcurrentScrollDelta = e1.getX() - e2.getX();
if(currentScrollDelta
if(currentScrollDelta > maxScrollDelta) currentScrollDelta = maxScrollDelta;
intscrollOffset = Math.round(mCurrentOffset + currentScrollDelta);
// We can't scroll more than the width of our own frame layout
if(scrollOffset >= mGalleryWidth) scrollOffset = mGalleryWidth;
if(scrollOffset <= mGalleryWidth * -1) scrollOffset = mGalleryWidth * -1;
mViews[0].setOffset(scrollOffset,0, mCurrentViewNumber);
mViews[1].setOffset(scrollOffset,0, mCurrentViewNumber);
mViews[2].setOffset(scrollOffset,0, mCurrentViewNumber);
}
returnfalse;
}
@Override
publicbooleanonFling(MotionEvent e1, MotionEvent e2,floatvelocityX,floatvelocityY)
{
if(Math.abs(e1.getY() - e2.getY()) <= swipe_max_off_path)
{
if(e2.getX() - e1.getX() > swipe_min_distance && Math.abs(velocityX) > swipe_threshold_veloicty)
{
movePrevious();
}
if(e1.getX() - e2.getX() > swipe_min_distance && Math.abs(velocityX) > swipe_threshold_veloicty)
{
moveNext();
}
}
returnfalse;
}
@Override
publicvoidonLongPress(MotionEvent e)
{
// Finalise scrolling
mFlingDirection = 0;
processGesture();
}
@Override
publicvoidonShowPress(MotionEvent e)
{
}
@Override
publicbooleanonSingleTapUp(MotionEvent e)
{
// Reset fling state
mFlingDirection = 0;
returnfalse;
}
}
}