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

要实现多页滑动效果,主要是需要处理onTouchEvent和onInterceptTouchEvent,要处理好touch事件的子控件和父控件的传递问题。滚动控制可以利用android的Scroller来实现。
对于不清楚android Touch事件的传递过程的,先google一下。
这里提供两种做法:
1、自定义MFlipper控件,从ViewGroup继承,利用Scroller实现滚动,重点是onTouchEvent和onInterceptTouchEvent的重写,要注意什么时候该返回true,什么时候false。否则会导致界面滑动和界面内按钮点击事件相冲突。
由于采用了ViewGroup来管理子view,只适合于页面数较少而且较固定的情况,因为viewgroup需要一开始就调用addView,把所有view都加进去并layout,太多页面会有内存问题。如果是页面很多,而且随时动态增长的话,就需要考虑对view做cache和动态创建,动态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);
    }
}