实现手指滑动切换视图的自定义布局类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的默认显示第几个屏幕的内容
}
}
关键点总结
- 该类和LinearLayout、RelativeLayout等布局都是ViewGroup的子类。LinearLayout可以在属性中指定view的排列方式——横向或纵向,而我们自己写的这个类是通过onLayout(boolean changed, int l, int t, int r, int b)方法来自行指定排列方向的。我们这里指定的是横向,大家可以根据需要改为纵向。
- 该类中用到了一些我们不常用的类,如VelocityTracker和Scroller,大家可以参考Android开发文档研究一下,在文章末尾我对VelocityTracker做了下简单介绍。
- 需 要注意的是:我们需在滑动时做一个简单但很重要的判断,即我们需要简单的判断用户的意图——想横向滑动还是想纵向滑动。相信大家都有所体会,就是我们的手 指在屏幕上滑动时不可能是完全水平或完全垂直的,这样会造成屏幕过于灵敏——我们本想上下滑动却触发了左右切换界面的操作。一个很明显的例子就是当滑动界 面中存在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。
- 建议大家研究下Android的事件拦截和处理机制,虽然不难但很重要。这在ScrollLayout类中也发挥了重要作用。
android.view.VelocityTracker类简介
- 说明:
- 这个类帮助我们追踪触摸事件(如:滑动)的速率从而实现fling(快速滑动)和其他一些这样的手势。
2. 用法:
(1)通过VelocityTracker.obtain()方法获取该类的实例。假设该实例为mVelocityTracker。
(2) 通过addMovement(MotionEvent)方法将你接收到的MotionEvent(事件)添加到mVelocityTracker实例中开始追踪(速度)。
(3) 若要获得当前手势速率,要先调用computeCurrentVelocity(int)方法计算当前速率。然后分别调用getXVelocity()和getYVelocity()便可获得水平和垂直方向上的速率。
_注:以上操作通常情况下在__onTouchEvent(MotionEvent event)_方法中执行,该方法可以提供事件参数。