实现手指滑动切换视图的自定义布局类ScrollLayout

该类的功能是实现随手指滑动切换页面的功能,类似Gallery(但是Gallery限制太多,比如每页布局必须相同)。有的同学可能会想到我们可 以在 onTouchEvent (MotionEvent event)方法中进行判断,当左右滑动时,执行startActivity(Context context)方法达到切换页面的效果。但是使用这种方法进行切换是没有过度效果的,只是刷的一下就过去了,而使用这个继承了ViewGroup的布局 就可以达到这个效果了。如下图所示:

ScrollLayout类代码

public class ScrollLayout extends ViewGroup {

         // private float startX;

         // private float startY;

         public static boolean startTouch = true;

         // private boolean canMove = true;

         private static final String TAG = "ScrollLayout";

         private Scroller mScroller;

         /*

          * 速度追踪器,主要是为了通过当前滑动速度判断当前滑动是否为fling

          */

         private VelocityTracker mVelocityTracker;

         /*

          * 记录当前屏幕下标,取值范围是:0 到 getChildCount()-1

          */

         private static int mCurScreen;

         // private int mDefaultScreen = 1;

         /*

          * Touch状态值 0:静止 1:滑动

          */

         private static final int TOUCH_STATE_REST = 0;

         private static final int TOUCH_STATE_SCROLLING = 1;

         /*

          * 记录当前touch事件状态--滑动(TOUCH_STATE_SCROLLING)、静止(TOUCH_STATE_REST 默认)

          */

         private int mTouchState = TOUCH_STATE_REST;

         private static final int SNAP_VELOCITY = 600;

         /*

          * 记录touch事件中被认为是滑动事件前的最大可滑动距离

          */

         private int mTouchSlop;

         /*

          * 记录滑动时上次手指所处的位置

          */

         private float mLastMotionX;

         private float mLastMotionY;

         private OnScrollToScreenListener onScrollToScreen = null;

         public ScrollLayout(Context context, AttributeSet attrs) {

                   this(context, attrs, 0);

         }

         public ScrollLayout(Context context, AttributeSet attrs, int defStyle) {

                   super(context, attrs, defStyle);

                   mScroller = new Scroller(context);

                   // mCurScreen = mDefaultScreen;

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

                   System.out.println("aaaaaaaaaaaaaaaaaaaaa" + mTouchSlop);

         }

         @Override

         protected void onLayout(boolean changed, int l, int t, int r, int b) {

                   int childLeft = 0;

                   final int childCount = getChildCount();

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

                            final View childView = getChildAt(i);

                            if (childView.getVisibility() != View.GONE) {

                                     final int childWidth = childView.getMeasuredWidth();

                                     childView.layout(childLeft, 0, childLeft + childWidth,

                                     childView.getMeasuredHeight());

                                     childLeft += childWidth;

                            }

                   }

         } 

         @Override

        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

                   Log.e(TAG, "onMeasure");

                   super.onMeasure(widthMeasureSpec, heightMeasureSpec);

                   final int width = MeasureSpec.getSize(widthMeasureSpec);

                   final int widthMode = MeasureSpec.getMode(widthMeasureSpec);

                   if (widthMode != MeasureSpec.EXACTLY) {

                            throw new IllegalStateException(

                                               "ScrollLayout only canmCurScreen run at EXACTLY mode!");

                   }

                   final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

                   if (heightMode != MeasureSpec.EXACTLY) {

                            throw new IllegalStateException(

                                               "ScrollLayout only can run at EXACTLY mode!");

                   }

                   // The children are given the same width and height as the scrollLayout

                  final int count = getChildCount();

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

                            getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);

                   }

                   // Log.e(TAG, "moving to screen "+mCurScreen);

                   scrollTo(mCurScreen * width, 0);

                   doScrollAction(mCurScreen);

         }

         /**

          * 方法名称:snapToDestination 方法描述:根据当前位置滑动到相应界面

          *

          * @param whichScreen

          */ 

        public void snapToDestination() {

                   final int screenWidth = getWidth(); 

                   final int destScreen = (getScrollX() + screenWidth / 2) / screenWidth;

                   snapToScreen(destScreen);

         }

         /**

          * 方法名称:snapToScreen 方法描述:滑动到到第whichScreen(从0开始)个界面,有过渡效果

          * @param whichScreen

          */

         public void snapToScreen(int whichScreen) {

                   // get the valid layout page

                   whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));

                   if (getScrollX() != (whichScreen * getWidth())) {

                            final int delta = whichScreen * getWidth() - getScrollX();

                            mScroller.startScroll(getScrollX(), 0, delta, 0,

                                               Math.abs(delta) * 2);

                           mCurScreen = whichScreen;

                            doScrollAction(mCurScreen);

                            invalidate(); // Redraw the layout

                   }

         }

         /**

          * 方法名称:setToScreen 方法描述:指定并跳转到第whichScreen(从0开始)个界面 

          * @param whichScreen

          */

        public void setToScreen(int whichScreen) {

                   whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));

                   mCurScreen = whichScreen;

                   scrollTo(whichScreen * getWidth(), 0);

                   doScrollAction(whichScreen);

         }

         public int getCurScreen() {

                   return mCurScreen;

         }

         @Override

        public void computeScroll() {

                   // TODO Auto-generated method stub

                   if (mScroller.computeScrollOffset()) {

                            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());

                            postInvalidate();

                   }

         }

         @Override

        public boolean onTouchEvent(MotionEvent event) { 

                   if (mVelocityTracker == null) {

                            mVelocityTracker = VelocityTracker.obtain();

                   } 

                   mVelocityTracker.addMovement(event);

                   final int action = event.getAction();

                   final float x = event.getX();

                   // final float y = event.getY();

                   switch (action) {

                   case MotionEvent.ACTION_DOWN:

                            Log.e(TAG, "event down!");

                            if (!mScroller.isFinished()) {

                                     mScroller.abortAnimation();

                            }

                           mLastMotionX = x;

                            break;

                   case MotionEvent.ACTION_MOVE: 

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

                            mLastMotionX = x;

                            scrollBy(deltaX, 0);                       

                            break;

                   case MotionEvent.ACTION_UP: 

                            Log.e(TAG, "event : up");

                            Log.e(TAG, "event : up"); 

                            final VelocityTracker velocityTracker = mVelocityTracker;

                            velocityTracker.computeCurrentVelocity(1000);

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

                            Log.e(TAG, "velocityX:" + velocityX);

                            if (velocityX > SNAP_VELOCITY && mCurScreen > 0) {

                                     // Fling enough to move left

                                     Log.e(TAG, "snap left");

                                     snapToScreen(mCurScreen - 1);

                            } else if (velocityX < -SNAP_VELOCITY

                                               && mCurScreen < getChildCount() - 1) {

                                     // Fling enough to move right

                                     Log.e(TAG, "snap right");

                                     snapToScreen(mCurScreen + 1);

                            } else {

                                    snapToDestination();

                            }

                            if (mVelocityTracker != null) {

                                     mVelocityTracker.recycle();

                                     mVelocityTracker = null;

                            } 

                            mTouchState = TOUCH_STATE_REST;

                            break;

                   case MotionEvent.ACTION_CANCEL:

                            mTouchState = TOUCH_STATE_REST;

                           break;

                   }

                   return true;

         }

         @Override

         public boolean onInterceptTouchEvent(MotionEvent ev) {

                   // TODO Auto-generated method stub

                   Log.e(TAG, "onInterceptTouchEvent-slop:" + mTouchSlop);

                   final int action = ev.getAction();

                   if ((action == MotionEvent.ACTION_MOVE)

                                     && (mTouchState != TOUCH_STATE_REST)) {

                            return true;

                   }

                   final float x = ev.getX();

                   final float y = ev.getY();

                   switch (action) {

                   case MotionEvent.ACTION_DOWN:

                            mLastMotionX = x;

                            mLastMotionY = y;

                            mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST

                                               : TOUCH_STATE_SCROLLING; 

                            break;

                   case MotionEvent.ACTION_MOVE:

                            final int xDiff = (int) Math.abs(mLastMotionX - x);

                            if (xDiff > mTouchSlop) { 

                                     if (Math.abs(mLastMotionY - y) / Math.abs(mLastMotionX - x) < 1)

                                              mTouchState = TOUCH_STATE_SCROLLING;

                           }

                           break;

                   case MotionEvent.ACTION_CANCEL:

                   case MotionEvent.ACTION_UP:

                            mTouchState = TOUCH_STATE_REST;

                            break;

                   }

                   return mTouchState != TOUCH_STATE_REST;

         }

         /** 

          * 方法名称:doScrollAction 方法描述:当滑动切换界面时执行相应操作

          * @param index

          */

         private void doScrollAction(int whichScreen) {

                   if (onScrollToScreen != null) {

                           onScrollToScreen.doAction(whichScreen);

                   }

        }

         /** 

          * 方法名称:setOnScrollToScreen 方法描述:设置内部接口的实现类实例 

          * @param index 

          */

         public void setOnScrollToScreen(

                            OnScrollToScreenListener paramOnScrollToScreen) {

                   onScrollToScreen = paramOnScrollToScreen;

         }

         /** 

          * 接口名称:OnScrollToScreen 接口描述:当滑动到某个界面时可以调用该接口下的doAction()方法执行某些操作 

          * @author wader

          */

         public abstract interface OnScrollToScreenListener {

                   public void doAction(int whichScreen);

         }

         /**

          * 指定默认页面位置

          * @param position

          */

         public void setDefaultScreen(int position) {

                   mCurScreen = position;

         }

}

布局文件main.xml

 <diy.ts.wader.widget.ScrollLayout 

   xmlns:android="http://schemas.android.com/apk/res/android"  

   android:id="@+id/ScrollLayoutTest" 

   android:layout_width="fill_parent" 

   android:layout_height="fill_parent"

 <LinearLayout 

   android:background="#FF0000" 

   android:layout_width="fill_parent" 

   android:layout_height="fill_parent">

 

 <FrameLayout 

   android:background="#00FF00" 

   android:layout_width="fill_parent" 

   android:layout_height="fill_parent">

 

  <FrameLayout 

   android:background="#0000FF"  

   android:layout_width="fill_parent" 

   android:layout_height="fill_parent">

     

 </diy.ts.wader.widget.ScrollLayout> 

Activity代码MainActivity.java

public class MainActivity extends Activity {

    private ScrollLayout viewContainer;

    private OnScrollToScreenListener scrollListener;// 滑动切换屏幕显示内容时的事件回调接口

    @Override

    public void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       setContentView(R.layout.main);

       viewContainer = (ScrollLayout) findViewById(R.id.ScrollLayoutTest);

       scrollListener = new OnScrollToScreenListener() {

           @Override

           public void doAction(int whichScreen) {// 在这里执行滑动切换屏幕显示内容时你想做的操作

              Toast.makeText(MainActivity.this,

                     "滑动到了第" + whichScreen + "个屏幕", 300).show();

           }

       };

      viewContainer.setOnScrollToScreen(scrollListener);// 设置滑动时的监听

       viewContainer.setDefaultScreen(1);// 设置ScrollLayout的默认显示第几个屏幕的内容

    }

}

关键点总结
  1. 该类和LinearLayout、RelativeLayout等布局都是ViewGroup的子类。LinearLayout可以在属性中指定view的排列方式——横向或纵向,而我们自己写的这个类是通过onLayout(boolean changed, int l, int t, int r, int b)方法来自行指定排列方向的。我们这里指定的是横向,大家可以根据需要改为纵向。
  2. 该类中用到了一些我们不常用的类,如VelocityTracker和Scroller,大家可以参考Android开发文档研究一下,在文章末尾我对VelocityTracker做了下简单介绍。
  3. 需 要注意的是:我们需在滑动时做一个简单但很重要的判断,即我们需要简单的判断用户的意图——想横向滑动还是想纵向滑动。相信大家都有所体会,就是我们的手 指在屏幕上滑动时不可能是完全水平或完全垂直的,这样会造成屏幕过于灵敏——我们本想上下滑动却触发了左右切换界面的操作。一个很明显的例子就是当滑动界 面中存在ListView。所以这个简单的判断是很重要的,在ScrollLayout中我们是这样做的:
  4. case MotionEvent.ACTION_MOVE:
  5.                     final int xDiff = (int) Math.abs(mLastMotionX - x);
  6.                     if (xDiff > mTouchSlop) {
  7.                              if (Math.abs(mLastMotionY - y) / Math.abs(mLastMotionX - x) < 1)
  8.                              mTouchState = TOUCH_STATE_SCROLLING;
  9.                     }
  10.                     break;

   我们判断滑动方向与水平方向的夹角是否大于45度,小于45度((Math.abs(mLastMotionY - y) / Math.abs(mLastMotionX - x) < 1))则判定用户想要水平滑动,这时我们截获触屏事件不再向下传递(不再传递给child)而是通过onTouchEvent(MotionEvent event)自行处理,在ScrollLayout中就是切换界面的操作。大于45度则判定用户想要垂直滑动,比如滑动界面中的ListView。

  1. 建议大家研究下Android的事件拦截和处理机制,虽然不难但很重要。这在ScrollLayout类中也发挥了重要作用。
android.view.VelocityTracker类简介
  1. 说明:
  2. 这个类帮助我们追踪触摸事件(如:滑动)的速率从而实现fling(快速滑动)和其他一些这样的手势。

2. 用法:

(1)通过VelocityTracker.obtain()方法获取该类的实例。假设该实例为mVelocityTracker。

     (2) 通过addMovement(MotionEvent)方法将你接收到的MotionEvent(事件)添加到mVelocityTracker实例中开始追踪(速度)。

    (3) 若要获得当前手势速率,要先调用computeCurrentVelocity(int)方法计算当前速率。然后分别调用getXVelocity()和getYVelocity()便可获得水平和垂直方向上的速率。

 _注:以上操作通常情况下在__onTouchEvent(MotionEvent event)_方法中执行,该方法可以提供事件参数。