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);
}
}