package com.droidful.flinggallery;

import android.content.Context;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.widget.Adapter;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

public class FlingGallery extends FrameLayout
{
        // Constants

        private final int swipe_min_distance = 120;
    private final int swipe_max_off_path = 250;
    private final int swipe_threshold_veloicty = 400;

    // Properties

        private int mViewPaddingWidth = 0;
    private int mAnimationDuration = 250;
    private float mSnapBorderRatio = 0.5f;
    private boolean mIsGalleryCircular = true;

    // Members

    private int mGalleryWidth = 0;
    private boolean mIsTouched = false;
    private boolean mIsDragging = false;
    private float mCurrentOffset = 0.0f;
    private long mScrollTimestamp = 0;
    private int mFlingDirection = 0;
    private int mCurrentPosition = 0;
    private int mCurrentViewNumber = 0;

    private Context mContext;
    private Adapter mAdapter;
    private FlingGalleryView[] mViews;
    private FlingGalleryAnimation mAnimation;
    private GestureDetector mGestureDetector;
    private Interpolator mDecelerateInterpolater;

    public FlingGallery(Context context)
        {
                super(context);

                mContext = context;
                mAdapter = null;

        mViews = new FlingGalleryView[3];
        mViews[0] = new FlingGalleryView(0, this);
        mViews[1] = new FlingGalleryView(1, this);
        mViews[2] = new FlingGalleryView(2, this);

                mAnimation = new FlingGalleryAnimation();
                mGestureDetector = new GestureDetector(new 
FlingGestureDetector());
                mDecelerateInterpolater = 
AnimationUtils.loadInterpolator(mContext,
android.R.anim.decelerate_interpolator);
        }

        public void setPaddingWidth(int viewPaddingWidth)
        {
                mViewPaddingWidth = viewPaddingWidth;
        }

        public void setAnimationDuration(int animationDuration)
        {
                mAnimationDuration = animationDuration;
        }

        public void setSnapBorderRatio(float snapBorderRatio)
        {
                mSnapBorderRatio = snapBorderRatio;
        }

        public void setIsGalleryCircular(boolean isGalleryCircular)
        {
                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));
                        }
                }
        }

        public int getGalleryCount()
        {
                return (mAdapter == null) ? 0 : mAdapter.getCount();
        }

        public int getFirstPosition()
        {
                return 0;
        }

        public int getLastPosition()
        {
                return (getGalleryCount() == 0) ? 0 : getGalleryCount() - 1;
        }

        private int getPrevPosition(int relativePosition)
        {
                int prevPosition = relativePosition - 1;

                if (prevPosition < getFirstPosition())
                {
                        prevPosition = getFirstPosition() - 1;

                        if (mIsGalleryCircular == true)
                        {
                                prevPosition = getLastPosition();
                        }
                }

                return prevPosition;
        }

        private int getNextPosition(int relativePosition)
        {
                int nextPosition = relativePosition + 1;

                if (nextPosition > getLastPosition())
                {
                        nextPosition = getLastPosition() + 1;

                        if (mIsGalleryCircular == true)
                        {
                                nextPosition = getFirstPosition();
                        }
                }

                return nextPosition;
        }

        private int getPrevViewNumber(int relativeViewNumber)
        {
                return (relativeViewNumber == 0) ? 2 : relativeViewNumber - 1;
        }

        private int getNextViewNumber(int relativeViewNumber)
        {
                return (relativeViewNumber == 2) ? 0 : relativeViewNumber + 1;
        }

        @Override
        protected void onLayout(boolean changed, int left, int top, int
right, int bottom)
        {
                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);
            }
        }

        public void setAdapter(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);
    }

        private int getViewOffset(int viewNumber, int relativeViewNumber)
        {
                // Determine width including configured padding width
                int offsetWidth = mGalleryWidth + mViewPaddingWidth;

                // Position the previous view one measured width to left
                if (viewNumber == getPrevViewNumber(relativeViewNumber))
                {
                        return offsetWidth;
                }

                // Position the next view one measured width to the right
                if (viewNumber == getNextViewNumber(relativeViewNumber))
                {
                        return offsetWidth * -1;
                }

                return 0;
        }

        void movePrevious()
        {
                // Slide to previous view
                mFlingDirection = 1;
                processGesture();
        }

        void moveNext()
        {
                // Slide to next view
                mFlingDirection = -1;
                processGesture();
        }

         @Override
         public boolean onKeyDown(int keyCode, KeyEvent event)
         {
            switch (keyCode)
            {
            case KeyEvent.KEYCODE_DPAD_LEFT:
                movePrevious();
                return true;

            case KeyEvent.KEYCODE_DPAD_RIGHT:
                moveNext();
                return true;

            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
            }

            return super.onKeyDown(keyCode, event);
        }

        public boolean onGalleryTouchEvent(MotionEvent event)
        {
                boolean consumed = mGestureDetector.onTouchEvent(event);

                if (event.getAction() == MotionEvent.ACTION_UP)
                {
                        if (mIsTouched || mIsDragging)
                        {
                                processScrollSnap();
                                processGesture();
                        }
                }

        return consumed;
    }

        void processGesture()
        {
                int newViewNumber = mCurrentViewNumber;
                int reloadViewNumber = 0;
                int reloadPosition = 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 < getLastPosition() || 
mIsGalleryCircular ==
true)
                        {
                                // 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;
        }

        void processScrollSnap()
        {
                // Snap to next view if scrolled passed snap position
                float rollEdgeWidth = mGalleryWidth * mSnapBorderRatio;
                int rollOffset = mGalleryWidth - (int) rollEdgeWidth;
                int currentOffset = 
mViews[mCurrentViewNumber].getCurrentOffset();

                if (currentOffset <= rollOffset * -1)
                {
                        // Snap to previous view
                        mFlingDirection = 1;
                }

                if (currentOffset >= rollOffset)
                {
                        // Snap to next view
                        mFlingDirection = -1;
                }
        }

        private class FlingGalleryView
        {
                private int mViewNumber;
                private FrameLayout mParentLayout;

                private FrameLayout mInvalidLayout = null;
                private LinearLayout mInternalLayout = null;
                private View mExternalView = null;

                public FlingGalleryView(int viewNumber, FrameLayout 
parentLayout)
                {
                        mViewNumber = viewNumber;
                        mParentLayout = parentLayout;

                        // Invalid layout is used when outside gallery
                        mInvalidLayout = new FrameLayout(mContext);
                        mInvalidLayout.setLayoutParams(new 
LinearLayout.LayoutParams(
                        LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));

                        // Internal layout is permanent for duration
                        mInternalLayout = new LinearLayout(mContext);
                        mInternalLayout.setLayoutParams(new 
LinearLayout.LayoutParams(
                        LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));

                        mParentLayout.addView(mInternalLayout);
                }

                public void recycleView(int newPosition)
                {
                        if (mExternalView != null)
                        {
                                mInternalLayout.removeView(mExternalView);
                        }

                        if (mAdapter != null)
                        {
                                mExternalView = mInvalidLayout;

                                if (newPosition >= getFirstPosition() && 
newPosition <=
getLastPosition())
                                {
                                        mExternalView = 
mAdapter.getView(newPosition, mExternalView,
mInternalLayout);
                                }
                        }

                        if (mExternalView != null)
                        {
                                mInternalLayout.addView(mExternalView, new
LinearLayout.LayoutParams(
                        LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
                        }
                }

                public void setOffset(int xOffset, int yOffset, int
relativeViewNumber)
                {
                        // Scroll the target view relative to its own position 
relative to
currently displayed view
                        mInternalLayout.scrollTo(getViewOffset(mViewNumber,
relativeViewNumber) + xOffset, yOffset);
                }

                public int getCurrentOffset()
                {
                        // Return the current scroll position
                        return mInternalLayout.getScrollX();
                }

                public void requestFocus()
                {
                        mInternalLayout.requestFocus();
                }
        }

    private class FlingGalleryAnimation extends Animation
    {
        private boolean mIsAnimationInProgres;
        private int mRelativeViewNumber;
        private int mInitialOffset;
        private int mTargetOffset;
        private int mTargetDistance;

        public FlingGalleryAnimation()
        {
                mIsAnimationInProgres = false;
                mRelativeViewNumber = 0;
                mInitialOffset = 0;
                mTargetOffset = 0;
                mTargetDistance = 0;
        }

        public void prepareAnimation(int relativeViewNumber)
        {
                // 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
                                        int newDirection = (relativeViewNumber 
==
getPrevViewNumber(mRelativeViewNumber)) ? 1 : -1;
                                int animDirection = (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
        protected void applyTransformation(float interpolatedTime,
Transformation transformation)
        {
                // Ensure interpolatedTime does not over-shoot then calculate
new offset
                interpolatedTime = (interpolatedTime > 1.0f) ? 1.0f :
interpolatedTime;
                        int offset = mInitialOffset + (int) (mTargetDistance *
interpolatedTime);

                        for (int viewNumber = 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 < 0 && viewNumber !=
getPrevViewNumber(mRelativeViewNumber)))
                                {
                                        mViews[viewNumber].setOffset(offset, 0, 
mRelativeViewNumber);
                                }
                        }
        }

        @Override
        public boolean getTransformation(long currentTime,
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;

                                return false;
                }

                // 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

                        return false;
                }

                return true;
        }
    }

        private class FlingGestureDetector extends
GestureDetector.SimpleOnGestureListener
    {
        @Override
        public boolean onDown(MotionEvent e)
        {
                // Stop animation
                mIsTouched = true;

                // Reset fling state
                mFlingDirection = 0;
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float
distanceX, float distanceY)
        {
                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();
                        }

                    float maxVelocity = mGalleryWidth / (mAnimationDuration /
1000.0f);
                        long timestampDelta = System.currentTimeMillis() -
mScrollTimestamp;
                        float maxScrollDelta = maxVelocity * (timestampDelta /
1000.0f);
                        float currentScrollDelta = e1.getX() - e2.getX();

                        if (currentScrollDelta < maxScrollDelta * -1)
currentScrollDelta = maxScrollDelta * -1;
                        if (currentScrollDelta > maxScrollDelta) 
currentScrollDelta
= maxScrollDelta;
                        int scrollOffset = 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);
                }

            return false;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float
velocityX, float velocityY)
        {
            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();
                }
            }

            return false;
        }

        @Override
        public void onLongPress(MotionEvent e)
        {
                // Finalise scrolling
                mFlingDirection = 0;
            processGesture();
        }

        @Override
        public void onShowPress(MotionEvent e)
        {
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e)
        {
                // Reset fling state
                mFlingDirection = 0;
            return false;
        }
    }
}

-- 
You received this message because you are subscribed to the Google
Groups "Android Developers" group.
To post to this group, send email to android-developers@googlegroups.com
To unsubscribe from this group, send email to
android-developers+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/android-developers?hl=en

Reply via email to