分享

android中的左右滑动

 windli笔记 2011-10-12
iphone中有很多应用都能够左右滑动,非常cool,关键是实现起来非常简单。android比起来就差远了,网上有不少帖子。 我在这边重新分享下自己的经验吧,将实现细节详细解释下。

FlingGallery这个类摘自网上,有少许修改。
 
package com.nuomi.ui;  
 
import java.util.HashSet;  
import java.util.Set;  
 
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  
{  
      
    private Set<OnGalleryChangeListener> listeners;  
    private final int swipe_min_distance = 120;  
    private final int swipe_max_off_path = 250;  
    private final int swipe_threshold_veloicty = 400;  
 
    private int mViewPaddingWidth = 0;  
    private int mAnimationDuration = 250;  
    private float mSnapBorderRatio = 0.5f;  
    private boolean mIsGalleryCircular = true;  
 
    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);  
 
        listeners = new HashSet<OnGalleryChangeListener>();  
          
        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 addGalleryChangeListener(OnGalleryChangeListener listener){  
        listeners.add(listener);  
    }  
      
    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();  
            }  
        }  
        NotifyGalleryChange();  
        return prevPosition;  
    }  
 
    private int getNextPosition(int relativePosition)  
    {  
        int nextPosition = relativePosition + 1;  
 
        if (nextPosition > getLastPosition())  
        {  
            nextPosition = getLastPosition() + 1;  
 
            if (mIsGalleryCircular == true)  
            {  
                nextPosition = getFirstPosition();  
            }  
        }  
        NotifyGalleryChange();  
        return nextPosition;  
    }  
 
    //  
    private void NotifyGalleryChange() {  
        for (OnGalleryChangeListener listener :listeners) {  
            listener.onGalleryChange(mCurrentPosition);  
        }  
    }  
 
    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)  
        {  
            // 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.FILL_PARENT, LayoutParams.FILL_PARENT));  
 
            // Internal layout is permanent for duration  
            mInternalLayout = new LinearLayout(mContext);  
            mInternalLayout.setLayoutParams(new LinearLayout.LayoutParams(   
                    LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));  
 
            mParentLayout.addView(mInternalLayout);  
        }  
 
        public void recycleView(int newPosition)  
        {  
            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, new LinearLayout.LayoutParams(   
                    LayoutParams.FILL_PARENT, LayoutParams.FILL_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;  
        }  
          
    }  
 
    public GestureDetector getMGestureDetector() {  
        return mGestureDetector;  
    }  
      

由于我需要在滑动页面时,改动title中的文字,这里采用了观察者模式,加了个OnGalleryChangeListener,有同样需求的同学可以参考下。
public interface OnGalleryChangeListener {  
      
    public void onGalleryChange(int currentItem);  

在Activity中,
FlingGallery gallery = new FlingGallery(this);  
 
        gallery.setAdapter(new ArrayAdapter<String>(getApplicationContext(),  
                android.R.layout.simple_list_item_1, new String[xxxx]) {  
            public View getView(int position, View convertView, ViewGroup parent) {  
                               // 返回滑动的deal  
                return dealViews[position];  
            }  
        });  
        gallery.addGalleryChangeListener(new OnGalleryChangeListener() {  
 
            @Override 
            public void onGalleryChange(int currentItem) {  
                // 干些想干的事件  
 
            }  
 
        }); 
将gallery加到Activity中的最终需要显示的类中。
Adapter中的getView方法中,将需要用来滑动的view添加进来。在GalleryChangeListener中,可以干一些自己想要的滑动后的事情。我在这里改动了标题的文字,进行了我的view中图片的lazyloading。
这里提一下另一个问题。我的这个类,最终嵌入到了tab中,需要在你的tabActivity中dispatchKeyEvent一下,将按键事件分发下去。
最开始,我的滑动的view写得比较通用,所以包含了ScrollView来满足比较长的屏幕,导致手势监听会出一些问题,会出现抖动。当时的解决方案是针对不同屏幕尽量保证一屏能够显示,在res目录下,增加layout-800x480之类的目录,针对每个不同屏幕设计单独的layout,放弃上下滑动,效果也不错。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多