相对于Android 2.X版本中的长按、点击操作,从Android 4.X开始,滑动操作出现在了Android中
1. 滑动效果是如何产生的
滑动一个View,本质上就是移动一个View,通过不断地改变View的坐标来实现这一效果
1.1. Android坐标系
屏幕最左上角的顶点作为Android坐标系的原点,从这个点向右是X轴正方向,从这个点向下是Y轴正方向。
系统提供了getLocationOnScreen(int location[])
方法来获取Android坐标系中点的位置。另外,在触控事件中使用getRawX()
、getRawY()
方法所获得的坐标同样是Android坐标系中的坐标。
1.2. 视图坐标系
描述子视图在父视图中的位置关系,以父视图左上角为坐标原点,以原点向右为X轴正方向,以原点向下为Y轴正方向
在触控事件中,通过getX()
、getY()
所获得的坐标就是视图坐标系中的坐标
1.3. 触控事件——MotionEvent
MontionEvent中封装的一些常用的事件常量
// 单点触摸按下动作
public static final int ACTION_DOWN = 0;
// 单点触摸离开动作
public static final int ACTION_UP = 1;
// 触摸点移动动作
public static final int ACTION_MOVE = 2;
// 触摸动作取消
public static final int ACTION_CANCEL = 3;
// 触摸动作超出边界
public static final int ACTION_OUTSIDE = 4;
// 多点触摸按下动作
public static final int ACTION_POINTER_DOWN = 5;
// 多点离开动作
public static final int ACTION_POINTER_UP = 6;
通常在onTouchEvent(MotionEvent event)
方法中通过event.getAction()
方法来获取触控事件的类型
Android系统提供了非常多的方法来获取坐标值、相对距离等,如图。这些方法可以分成两类:
* View提供的获取坐标方法:getTop()
getLeft()
getRight()
getBottom()
* MotionEvent提供的方法:getX()
getY()
getRawX()
getRawY()
2. 实现滑动的七种方法
基本思想:当触摸View时,系统记下当前触摸点坐标;当手指移动时,系统记下移动后的触摸点坐标,从而获取到相对于前一次坐标点的偏移量,并通过偏移量来修改View的坐标,这样不断重复,从而实现滑动过程。
2.1. layout方法
当MotionEvent.ACTION_DOWN
时,记录触摸点坐标;当MotionEvent.ACTION_MOVE
时:
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
// 在当前left、top、right、bottom的基础上加上偏移量
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
或
// 计算偏移量
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
// 在当前left、top、right、bottom的基础上加上偏移量
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
// 重新设置初始坐标
lastX = rawX;
lastY = rawY;
使用绝对坐标系,在每次执行完ACTION_MOVE
的逻辑后,一定要重新设置初始坐标,因为从触摸按下直至触摸离开,触摸点的相对位置不会改变,而绝对位置会一直改变
2.2. offsetLeftAndRight()
与offsetTopAndBottom()
// 同时对left和right进行偏移
offsetLeftAndRight(offsetX);
// 同时对top和bottom进行偏移
offsetTopAndBottom(offsetY);
2.3. LayoutParams
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
// 或 LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams(); // 这里LinearLayout是父布局的类型
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
2.4. scrollTo
与scrollBy
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollBy(-offsetX, -offsetY);
scrollTo
、scrollBy
方法移动的是View的content,即让View的内容移动,如果在ViewGroup中使用scrollTo
、scrollBy
方法,移动的将是所有子View
scrollBy
方法移动的是屏幕(可视区域),而不是View,如图为scrollBy(20, 10)
的效果
因此,如果将scrollBy
中的参数dx和dy设置为正数,那么content将向坐标轴负方向移动,如果将scrollBy
中的参数dx和dy设置为负数,那么content将向坐标轴正方向移动
在使用绝对坐标时,可以通过使用scrollTo
方法来实现这一效果
2.5. Scroller
不管使用scrollTo
还是scrollBy
方法,子View的平移都是瞬间发生的。Google建议使用自然的过渡动画来实现移动效果。通过Scroller类可以实现平滑移动的效果。
虽然scrollBy
方法是让子View瞬间从某点移动到另一个点,但是由于在ACTION_MOVE
事件中不断获取手指移动的微小的偏移量,从而在整体上获得一个平滑移动的效果。Scroller类的实现原理与其类似。
使用Scroller类需要如下三个步骤:
* 初始化Scroller
* 重写computeScroll()
方法,实现模拟滑动
@Override
public void computeScroll() {
super.computeScroll();
// 判断Scroller是否执行完毕
if (mScroller.computeScrollOffset()) {
((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// 通过重绘来不断调用computeScroll
invalidate();
}
}
computeScroll()
方法不会自动调用,只能通过invalidate()
→draw()
→computeScroll()
来间接调用.
* startScroll
开启模拟过程(此例子为手指移动时scrollBy
,手指离开后startScroll
)
View viewGroup = ((View) getParent());
mScroller.startScroll(viewGroup.getScrollX(), viewGroup.getScrollY(), -viewGroup.getScrollX(), -viewGroup.getScrollY());
invalidate(); //invalidate()→draw()→computeScroll()
2.6. 属性动画
详见第7章
2.7. ViewDragHelper
例:实现类似QQ滑动侧边栏的布局,初始时显示内容界面,当用户手指滑动超过一段距离时,内容界面侧滑显示菜单界面。
* 初始化ViewDragHelper
ViewDragHelper通常定义在一个ViewGroup的内部,并通过其静态工厂方法进行初始化
mViewDragHelper = ViewDragHelper.create(this, callback);
- 拦截事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递给ViewDragHelper,此操作必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}
- 处理
computeScroll()
使用ViewDragHelper同样需要重写computeScroll()
方法,因为ViewDragHelper内部也是通过Scroller来实现平滑移动的
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
- 处理回调Callback
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
// 何时开始检测触摸事件
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果当前触摸的child是mMainView时开始检测
return mMainView == child;
}
// 处理垂直滑动
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
// 处理水平滑动
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
};
继续优化:在ViewDragHelper.Callback
中,通过重写onViewReleased()
方法,可以实现当手指离开屏幕后实现的操作。这个方法内部是通过Scroller类来实现的,这也是前面重写computeScroll()
方法的原因
// 拖动结束后调用
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//手指抬起后缓慢移动到指定位置
if (mMainView.getLeft() < 500) {
//关闭菜单
//相当于Scroller的startScroll方法
mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
} else {
//打开菜单
mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
关闭菜单时,将MainView还原到初始状态,即坐标为(0, 0)的点,打开菜单时,则将MainView移动到(300, 0)坐标。
在自定义ViewGroup的onFinishInflate()
方法中,定义MenuView和MainView
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}