Android实现多页左右滑动效果,支持子view动态创建和cache

要实现多页滑动效果,主要是需要处理onTouchEventonInterceptTouchEvent,要处理好touch事件的子控件和父控件的传递问题。滚动控制可以利用androidScroller来实现。

对于不清楚android Touch事件的传递过程的,先google一下。

这里提供两种做法:

1、自定义MFlipper控件,从ViewGroup继承,利用Scroller实现滚动,重点是onTouchEventonInterceptTouchEvent的重写,要注意什么时候该返回true,什么时候false。否则会导致界面滑动和界面内按钮点击事件相冲突。

由于采用了ViewGroup来管理子view,只适合于页面数较少而且较固定的情况,因为viewgroup需要一开始就调用addView,把所有view都加进去并layout,太多页面会有内存问题。如果是页面很多,而且随时动态增长的话,就需要考虑对viewcache和动态创建,动态layout,具体做法参考下面的方法二;

 

2、从AdapterView继承,参考Android自带ListView的实现,实现子view动态创建和cache,滑动效果等。源码如下:

import android.content.Context;

import android.util.AttributeSet;

import android.util.Log;

import android.util.SparseArray;

import android.view.MotionEvent;

import android.view.VelocityTracker;

import android.view.View;

import android.view.ViewConfiguration;

import android.view.ViewGroup;

import android.widget.AdapterView;

import android.widget.BaseAdapter;

import android.widget.Gallery;

import android.widget.Scroller;

 

/**

 * 自定义一个横向滚动的AdapterView,类似与全屏的Gallery,但是一次只滚动一屏,而且每一屏支持子view的点击处理

 * @author weibinke

 *

 */

public class MultiPageSwitcher extends AdapterView<BaseAdapter> {

 

private BaseAdapter mAdapter = null;

private Scroller mScroller;

private int mTouchSlop;

private float mTouchStartX;

private float mLastMotionX;

private final static String TAG = "MultiPageSwitcher";

 

private int mLastScrolledOffset = 0;                

 

/** User is not touching the list */

    private static final int TOUCH_STATE_RESTING = 0;

 

    /** User is scrolling the list */

    private static final int TOUCH_STATE_SCROLL = 2;

 

    private int mTouchState = TOUCH_STATE_RESTING;

private int mHeightMeasureSpec;

private int mWidthMeasureSpec;

private int mSelectedPosition;

private int mFirstPosition;                                //第一个可见view的position

private int mCurrentSelectedPosition;

 

private VelocityTracker mVelocityTracker;

private static final int SNAP_VELOCITY = 600;

 

protected RecycleBin mRecycler = new RecycleBin();

 

private OnPostionChangeListener mOnPostionChangeListener = null;

 

public MultiPageSwitcher(Context context, AttributeSet attrs) {

super(context, attrs);

mScroller = new Scroller(context);

mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();

}

 

@Override

protected void onLayout(boolean changed, int left, int top, int right,

int bottom) {

// TODO Auto-generated method stub

MLog.d("MultiPageSwitcher.onlayout start");

super.onLayout(changed, left, top, right, bottom);

 

if (mAdapter == null) {

return ;

}

 

recycleAllViews();

detachAllViewsFromParent();

mRecycler.clear();

 

fillAllViews();

 

MLog.d("MultiPageSwitcher.onlayout end");

}

 

/**

 * 从当前可见的view向左边填充

 */

private void fillToGalleryLeft() {

        int itemSpacing = 0;

        int galleryLeft = 0;

       

        // Set state for initial iteration

        View prevIterationView = getChildAt(0);

        int curPosition;

        int curRightEdge;

       

        if (prevIterationView != null) {

            curPosition = mFirstPosition - 1;

            curRightEdge = prevIterationView.getLeft() - itemSpacing;

        } else {

            // No children available!

            curPosition = 0;

            curRightEdge = getRight() - getLeft();

        }

               

        while (curRightEdge > galleryLeft && curPosition >= 0) {

            prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,

                    curRightEdge, false);

 

            // Remember some state

            mFirstPosition = curPosition;

           

            // Set state for next iteration

            curRightEdge = prevIterationView.getLeft() - itemSpacing;

            curPosition--;

        }

    }

   

    private void fillToGalleryRight() {

        int itemSpacing = 0;

        int galleryRight = getRight() - getLeft();

        int numChildren = getChildCount();

        int numItems = mAdapter.getCount();

       

        // Set state for initial iteration

        View prevIterationView = getChildAt(numChildren - 1);

        int curPosition;

        int curLeftEdge;

       

        if (prevIterationView != null) {

            curPosition = mFirstPosition + numChildren;

            curLeftEdge = prevIterationView.getRight() + itemSpacing;

        } else {

            mFirstPosition = curPosition = numItems - 1;

            curLeftEdge = 0;

        }

               

        while (curLeftEdge < galleryRight && curPosition < numItems) {

            prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,

                    curLeftEdge, true);

 

            // Set state for next iteration

            curLeftEdge = prevIterationView.getRight() + itemSpacing;

            curPosition++;

        }

    }

 

/**

 *填充view

 */

private void fillAllViews(){

//先创建第一个view,使其居中显示

if (mSelectedPosition >= mAdapter.getCount()&& mSelectedPosition > 0) {

//处理被记录被删除导致当前选中位置超出记录数的情况

mSelectedPosition = mAdapter.getCount() - 1;

if(mOnPostionChangeListener != null){

mCurrentSelectedPosition = mSelectedPosition;

mOnPostionChangeListener.onPostionChange(this, mCurrentSelectedPosition);

}

}

 

mFirstPosition = mSelectedPosition;

mCurrentSelectedPosition = mSelectedPosition;

 

View child = makeAndAddView(mSelectedPosition, 0, 0, true);

 

int offset = getWidth() / 2 - (child.getLeft() + child.getWidth() / 2);

child.offsetLeftAndRight(offset);

 

fillToGalleryLeft();

fillToGalleryRight();

 

}

 

/**

     * Obtain a view, either by pulling an existing view from the recycler or by

     * getting a new one from the adapter. If we are animating, make sure there

     * is enough information in the view's layout parameters to animate from the

     * old to new positions.

     *

     * @param position Position in the gallery for the view to obtain

     * @param offset Offset from the selected position

     * @param x X-coordintate indicating where this view should be placed. This

     *        will either be the left or right edge of the view, depending on

     *        the fromLeft paramter

     * @param fromLeft Are we posiitoning views based on the left edge? (i.e.,

     *        building from left to right)?

     * @return A view that has been added to the gallery

     */

    private View makeAndAddView(int position, int offset, int x,

            boolean fromLeft) {

 

        View child;

 

//        child = mRecycler.get(position);

//        if (child != null) {

//            // Position the view

//            setUpChild(child, offset, x, fromLeft);

//

//            return child;

//        }

//

//        // Nothing found in the recycler -- ask the adapter for a view

        child = mAdapter.getView(position, null, this);

 

        // Position the view

        setUpChild(child, offset, x, fromLeft);

 

        return child;

    }

   

    @Override

    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {

        /*

         * Gallery expects Gallery.LayoutParams.

         */

        return new Gallery.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,

                ViewGroup.LayoutParams.WRAP_CONTENT);

    }

 

    /**

     * Helper for makeAndAddView to set the position of a view and fill out its

     * layout paramters.

     *

     * @param child The view to position

     * @param offset Offset from the selected position

     * @param x X-coordintate indicating where this view should be placed. This

     *        will either be the left or right edge of the view, depending on

     *        the fromLeft paramter

     * @param fromLeft Are we posiitoning views based on the left edge? (i.e.,

     *        building from left to right)?

     */

    private void setUpChild(View child, int offset, int x, boolean fromLeft) {

 

        // Respect layout params that are already in the view. Otherwise

        // make some up...

        Gallery.LayoutParams lp = (Gallery.LayoutParams)

            child.getLayoutParams();

        if (lp == null) {

            lp = (Gallery.LayoutParams) generateDefaultLayoutParams();

        }

 

        addViewInLayout(child, fromLeft ? -1 : 0, lp);

 

        child.setSelected(offset == 0);

 

        // Get measure specs

        int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,

                0, lp.height);

        int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,

                0, lp.width);

 

        // Measure child

        child.measure(childWidthSpec, childHeightSpec);

 

        int childLeft;

        int childRight;

 

        // Position vertically based on gravity setting

        int childTop = 0;

        int childBottom = childTop + child.getMeasuredHeight();

 

        int width = child.getMeasuredWidth();

        if (fromLeft) {

            childLeft = x;

            childRight = childLeft + width;

        } else {

            childLeft = x - width;

            childRight = x;

        }

 

        child.layout(childLeft, childTop, childRight, childBottom);

    }

   

   

    @Override

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

            // TODO Auto-generated method stub

            super.onMeasure(widthMeasureSpec, heightMeasureSpec);

            

            mWidthMeasureSpec = widthMeasureSpec;

            mHeightMeasureSpec = heightMeasureSpec;

    }

 

    @Override

    public int getCount() {

            // TODO Auto-generated method stub

            return mAdapter.getCount();

    }

 

@Override

public BaseAdapter getAdapter() {

// TODO Auto-generated method stub

return mAdapter;

}

 

@Override

public void setAdapter(BaseAdapter adapter) {

// TODO Auto-generated method stub

mAdapter = adapter;

 

removeAllViewsInLayout();

 

requestLayout();

}

 

@Override

public View getSelectedView() {

// TODO Auto-generated method stub

return null;

}

 

@Override

public void setSelection(int position) {

// TODO Auto-generated method stub

}

 

@Override

public boolean onInterceptTouchEvent(MotionEvent event) {

if (!mScroller.isFinished()) {

return true;

}

 

final int action = event.getAction();

MLog.d("onInterceptTouchEvent action = "+event.getAction());

 

if (MotionEvent.ACTION_DOWN == action) {

startTouch(event);

 

return false;

}else if (MotionEvent.ACTION_MOVE == action) {

return startScrollIfNeeded(event);

}else if (MotionEvent.ACTION_UP == action || MotionEvent.ACTION_CANCEL == action) {

mTouchState = TOUCH_STATE_RESTING;

 

return false;

}

return false;

}

 

@Override

public boolean onTouchEvent(MotionEvent event) {

 

if (!mScroller.isFinished()) {

return true;

}

 

if (mVelocityTracker == null) {

mVelocityTracker = VelocityTracker.obtain();

}

 

mVelocityTracker.addMovement(event);

 

MLog.d("onTouchEvent action = "+event.getAction());

final int action = event.getAction();

final float x = event.getX();

 

if (MotionEvent.ACTION_DOWN == action) {

startTouch(event);

 

}else if (MotionEvent.ACTION_MOVE == action) {

if (mTouchState == TOUCH_STATE_RESTING) {

startScrollIfNeeded(event);

}else if (mTouchState == TOUCH_STATE_SCROLL) {

int deltaX = (int)(x - mLastMotionX);

mLastMotionX = x;

 

scrollDeltaX(deltaX);

 

}

}else if (MotionEvent.ACTION_UP == action || MotionEvent.ACTION_CANCEL == action) {

if (mTouchState == TOUCH_STATE_SCROLL) {

onUp(event);

}

}

return true;

}

 

 

private void scrollDeltaX(int deltaX){

 

//先把现有的view坐标移动

for (int i = 0; i < getChildCount(); i++) {

getChildAt(i).offsetLeftAndRight(deltaX);

}

 

boolean toLeft = (deltaX < 0);

detachOffScreenChildren(toLeft);

 

if (deltaX < 0) {

//sroll to right

fillToGalleryRight();

}else {

fillToGalleryLeft();

}

 

invalidate();

 

int position = calculteCenterItem() + mFirstPosition;

if (mCurrentSelectedPosition != position) {

mCurrentSelectedPosition = position;

if (mOnPostionChangeListener != null) {

mOnPostionChangeListener.onPostionChange(this, mCurrentSelectedPosition);

}

}

}

 

private void onUp(MotionEvent event){

 

 

final VelocityTracker velocityTracker = mVelocityTracker;  

velocityTracker.computeCurrentVelocity(1000);  

int velocityX = (int) velocityTracker.getXVelocity();  

 

MLog.d( "onUp velocityX:"+velocityX);

 

if (velocityX < -SNAP_VELOCITY && mSelectedPosition < mAdapter.getCount() - 1) {

if (scrollToChild(mSelectedPosition + 1)) {

mSelectedPosition ++;

}

}else if (velocityX > SNAP_VELOCITY && mSelectedPosition > 0) {

if (scrollToChild(mSelectedPosition - 1)) {

mSelectedPosition --;

}

}else{

int position = calculteCenterItem();

int newpostion = mFirstPosition + position;

if (scrollToChild(newpostion)) {

mSelectedPosition = newpostion;

}

}

 

if (mVelocityTracker != null) {  

            mVelocityTracker.recycle();  

            mVelocityTracker = null;  

        }  

 

mTouchState = TOUCH_STATE_RESTING;

}

 

/**

 * 计算最接近中心点的view

 * @return

 */

private int calculteCenterItem(){

View child = null;

int lastpostion = 0;

int lastclosestDistance = 0;

int viewCenter = getLeft() + getWidth() / 2;

for (int i = 0; i < getChildCount(); i++) {

child = getChildAt(i);

if (child.getLeft() < viewCenter && child.getRight() > viewCenter ) {

lastpostion = i;

break;

}else {

int childClosestDistance = Math.min(Math.abs(child.getLeft() - viewCenter), Math.abs(child.getRight() - viewCenter));

if (childClosestDistance < lastclosestDistance) {

lastclosestDistance = childClosestDistance;

lastpostion = i;

}

}

}

 

return lastpostion;

}

 

public void moveNext(){

if (!mScroller.isFinished()) {

return;

}

 

if (0 <= mSelectedPosition && mSelectedPosition < mAdapter.getCount() - 1) {

if (scrollToChild(mSelectedPosition + 1)) {

mSelectedPosition ++;

}else {

makeAndAddView(mSelectedPosition + 1, 1, getWidth(), true);

if (scrollToChild(mSelectedPosition + 1)) {

mSelectedPosition ++;

}

}

}

}

 

public void movePrevious(){

if (!mScroller.isFinished()) {

return;

}

 

if (0 < mSelectedPosition && mSelectedPosition < mAdapter.getCount()) {

if (scrollToChild(mSelectedPosition -1)) {

mSelectedPosition --;

}else {

makeAndAddView(mSelectedPosition - 1, -1, 0, false);

mFirstPosition = mSelectedPosition - 1;

if (scrollToChild(mSelectedPosition - 1)) {

mSelectedPosition --;

}

}

}

}

 

private boolean scrollToChild(int position){

MLog.d( "scrollToChild positionm,FirstPosition,childcount:"+position + "," + mFirstPosition+ "," + getChildCount());

View child = getChildAt(position - mFirstPosition );

if (child != null) {

int distance = getWidth() / 2 - (child.getLeft() + child.getWidth() / 2);

 

mLastScrolledOffset = 0;

mScroller.startScroll(0, 0, distance, 0,200);

invalidate();

 

return true;

}

 

MLog.d( "scrollToChild some error happened");

 

 

return false;

}

 

@Override

public void computeScroll() {

// TODO Auto-generated method stub

if (mScroller.computeScrollOffset()) {

int scrollX = mScroller.getCurrX();

//                        Mlog.d("MuticomputeScroll ," + scrollX);

 

scrollDeltaX(scrollX - mLastScrolledOffset);

mLastScrolledOffset = scrollX;

postInvalidate();

}

}

 

private void startTouch(MotionEvent event){

mTouchStartX = event.getX();

 

mTouchState = mScroller.isFinished()? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLL;

 

mLastMotionX = mTouchStartX;

}

 

private boolean startScrollIfNeeded(MotionEvent event){

final int xPos = (int)event.getX();

        mLastMotionX = event.getX();

        if (xPos < mTouchStartX - mTouchSlop

                || xPos > mTouchStartX + mTouchSlop

              ) {

            // we've moved far enough for this to be a scroll

            mTouchState = TOUCH_STATE_SCROLL;

            return true;

        }

        return false;

}

 

/**

     * Detaches children that are off the screen (i.e.: Gallery bounds).

     *

     * @param toLeft Whether to detach children to the left of the Gallery, or

     *            to the right.

     */

    private void detachOffScreenChildren(boolean toLeft) {

        int numChildren = getChildCount();

        int start = 0;

        int count = 0;

       

        int firstPosition = mFirstPosition;

        if (toLeft) {

            final int galleryLeft = 0;

            for (int i = 0; i < numChildren; i++) {

                final View child = getChildAt(i);

                if (child.getRight() >= galleryLeft) {

                    break;

                } else {

                    count++;

                    mRecycler.put(firstPosition + i, child);

                }

            }

        } else {

            final int galleryRight = getWidth();

            for (int i = numChildren - 1; i >= 0; i--) {

                final View child = getChildAt(i);

                if (child.getLeft() <= galleryRight) {

                    break;

                } else {

                    start = i;

                    count++;

                    mRecycler.put(firstPosition + i, child);

                }

            }

        }

 

        detachViewsFromParent(start, count);

       

        if (toLeft) {

            mFirstPosition += count;

        }

       

        mRecycler.clear();

    }

   

    public void setOnPositionChangeListen(OnPostionChangeListener onPostionChangeListener){

            mOnPostionChangeListener = onPostionChangeListener;

    }

   

    public int getCurrentSelectedPosition(){

            return mCurrentSelectedPosition;

    }

   

    /**

     * 刷新数据,本来想用AdapterView.AdapterDataSetObserver机制来实现的,但是整个逻辑移植比较麻烦,就暂时用这个替代了

     */

    public void updateData(){

            requestLayout();

    }

   

    private void recycleAllViews() {

        int childCount = getChildCount();

        final RecycleBin recycleBin = mRecycler;

 

        // All views go in recycler

        for (int i=0; i<childCount; i++) {

            View v = getChildAt(i);

            int index = mFirstPosition + i;

            recycleBin.put(index, v);

        } 

    }

   

    class RecycleBin {

        private SparseArray<View> mScrapHeap = new SparseArray<View>();

 

        public void put(int position, View v) {

                if (mScrapHeap.get(position) != null) {

Log.e(TAG,"RecycleBin put error.");

}

            mScrapHeap.put(position, v);

        }

       

        View get(int position) {

            // System.out.print("Looking for " + position);

            View result = mScrapHeap.get(position);

            if (result != null) {

                    MLog.d("RecycleBin get hit.");

                mScrapHeap.delete(position);

            } else {

                    MLog.d("RecycleBin get Miss.");

            }

            return result;

        }

       

        View peek(int position) {

            // System.out.print("Looking for " + position);

            return mScrapHeap.get(position);

        }

       

        void clear() {

            final SparseArray<View> scrapHeap = mScrapHeap;

            final int count = scrapHeap.size();

            for (int i = 0; i < count; i++) {

                final View view = scrapHeap.valueAt(i);

                if (view != null) {

                    removeDetachedView(view, true);

                }

            }

            scrapHeap.clear();

        }

    }   

    public interface OnPostionChangeListener{

            abstract public void onPostionChange(View v,int position);

    } 

}

posted @ 2012-05-30 14:48  brainy  阅读(4806)  评论(12编辑  收藏  举报