自定义布局-ScrollLayout
原文地址:http://www.cnblogs.com/wader2011/archive/2011/10/10/2205142.html
该类的功能是实现随手指滑动切换页面的功能,类似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
<?xml version="1.0" encoding="utf-8"?> <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"> </LinearLayout> <FrameLayout android:background="#00FF00" android:layout_width="fill_parent" android:layout_height="fill_parent"> </FrameLayout> <FrameLayout android:background="#0000FF" android:layout_width="fill_parent" android:layout_height="fill_parent"> </FrameLayout> </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中我们是这样做的: 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; 我们判断滑动方向与水平方向的夹角是否大于45度,小于45度((Math.abs(mLastMotionY - y) / Math.abs(mLastMotionX - x) < 1))则判定用户想要水平滑动,这时我们截获触屏事件不再向下传递(不再传递给child)而是通过onTouchEvent(MotionEvent event)自行处理,在ScrollLayout中就是切换界面的操作。大于45度则判定用户想要垂直滑动,比如滑动界面中的ListView。 4. 建议大家研究下Android的事件拦截和处理机制,虽然不难但很重要。这在ScrollLayout类中也发挥了重要作用。 android.view.VelocityTracker类简介 1. 说明: 这个类帮助我们追踪触摸事件(如:滑动)的速率从而实现fling(快速滑动)和其他一些这样的手势。 2. 用法: (1)通过VelocityTracker.obtain()方法获取该类的实例。假设该实例为mVelocityTracker。 (2) 通过addMovement(MotionEvent)方法将你接收到的MotionEvent(事件)添加到mVelocityTracker实例中开始追踪(速度)。 (3) 若要获得当前手势速率,要先调用computeCurrentVelocity(int)方法计算当前速率。然后分别调用getXVelocity()和getYVelocity()便可获得水平和垂直方向上的速率。 注:以上操作通常情况下在onTouchEvent (MotionEvent event)方法中执行,该方法可以提供事件参数。