要实现多页滑动效果,主要是需要处理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); } }