滑动一个View,本质上来说就是移动一个View。改变其当前所处的位置,就是通过不断地改变View的坐标来实现这一效果,所有要实现View的滑动,就必须监听用户触摸的事件,并根据事件传入的坐标,动态且不断地改变View的坐标,从而实现View跟随用户触摸的滑动而滑动。
Android的窗口坐标体系和屏幕的触控事件-MotionEvent
(一)Android坐标系
在Android中,将屏幕最左上角的顶点作为Android坐标系的原点,从这个点向右是X轴正方向,向下是Y轴正方形。Android坐标系如下图: 在触控事件中使用getRawX(),getRawY()方法获得的坐标是Android坐标系中的坐标。
(二)视图坐标系
视图坐标系:它描述了子视图在父视图中的位置关系。这两坐标系不矛盾也不负责,它们的作用是相辅相成的。与Android坐标系类似,视图坐标系同样是向右为X轴正方向,以原点向下为Y轴正方向,它以父视图左上角为坐标原点。如图所示:在触控事件中,通过getX(),getY()所获得的坐标是视图坐标系中的坐标。
(三)触控事件-----MotionEvent
在MotionEvent中封装的一些常用的事件常量,它定义了触控事件的不同类型。
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.getAtion()方法来获取触控事件的类型,并使用switch-cas方法来进行筛选。模式固定: 如下:
@Override
public boolean onTouchEvent(MotionEvent event){
//获取当前输入点的X,Y坐标(视图坐标)
int x = (int)event.getX();
int y = (int)event.getY();
switch(event.getAction()){
case MotionEvent.Action_DOWN: //处理输入的按下事件
break;
case MotionEvent.Action_Move: //处理输入的移动事件
break;
case MotionEvent.Action_UP: //处理输入的离开事件
break;
}
return true;
}
(四)在Android中,系统提供了多种方法来获取坐标值,相对距离。结合Android坐标系来看看怎样使用它们。如下图:
这个方法可以分为两个类别:
(1)View提供的获取坐标方法:
getTop():获取到的是View自身的顶边到其父布局顶边的距离
getLeft():获取到的是View自身的左边到其父布局左边的距离
getRight():获取到的是View自身的右边到其父布局左边的距离
getBottom():获取到的是View自身的底边到其父布局顶边的距离
(2)MotionEvent提供的方法:
getX():获取点击事件距离控件左边的距离,即视图坐标
getY():获取点击事件距离控件顶边的距离,即视图坐标
getRawX():获取点击事件距离整个屏幕左边的距离,即绝对坐标
getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标
(五)实现滑动的七种方法
思想基本是:当触摸View时,系统记下当前触摸点的坐标,当手指移动时,系统记下移动后的触摸点坐标,从而获取到相对于前一次坐标点的偏移量,并通过偏移量来修改View的坐标,这样不断重复,从而实现滑动过程。
(方法一)layout方法
编写自定义View类
public class DragView1 extends View{
private int lastX;
private int lastY;
public DragView1(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
public DragView1(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public DragView1(Context context) {
super(context);
initView();
}
private void initView(){
//给View设置颜色背景,便于观察
setBackgroundColor(Color.BLUE);
}
//视图坐标方式
@Override
public boolean onTouchEvent(MotionEvent event) {
//通过视图坐标系获得视图坐标
int x =(int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) { //获取触控事件的类型
case MotionEvent.ACTION_DOWN: //单点触摸按下的事件
//记录触摸点坐标
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE: //触摸点移动动作
//计算偏移量
int offsetX = x-lastX;
int offsetY= y-lastY;
//View都会调用layout()方法来对自己重新布局,从而达到移动View的效果.
//在当前left,top,right,bottom的基础上加上偏移量
/**layout(int l, int t, int r, int b) 方法
* 该方法是View的放置方法,调用该方法需要传入放置View的矩形空间
* 左上角left,top值,右下角:right,bottom值。这四个值相对于
* 父控件而言
* */
layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
break;
}
return true;
}
}
(方法二)offsetLeftAndRight()与offsetTopAndBottom()
这个方法相当于系统提供的一个对左右、上下移动的API对封装。当计算出偏移量后,只需要使用如下代码就可以完成View的重新布局,效果与使用Layout方法一样,代码如下:
offsetLeftAndRight(offsetx); //同时对left和right进行偏移
offsetTopAndBottm(offsetY); //同时对top和bottom进行偏移
offsetX和offsetY的计算方法与方法一中的一样。
(方法三)LayoutParams
LayoutParams保存了一个View的布局参数。因此可以在程序中,通过改变LayoutParams来动态地修改一个布局的位置参数。从而达到改变View位置的效果。可以在程序中使用getLayoutParams()来获取一个View的LayoutParams。计算偏移量的方法与方法一也是一样的。当获取到偏移量之后,就可以通过setLayoutParams来改变其LayoutParams。代码如下:
LinerLayout.LayoutParams layoutParams=(LinearLayout.LayoutParams)getLayoutParams();
layoutParams.leftMargin=getLeft()+offsetX;
layoutParams.topMargin=getTop()+offsetY;
setLayoutParams(layoutParams);
注意:通过getLayoutParams()获取LayoutParams时,需要根据View所在父布局的类型来设置不同的类型,比如这里将View放在LinearLayout中,那么就可以使用LinearLayou.LayoutParams。如果在RelativeLayout中,就要使用RelativeLayout.LayoutParams。当然,这一切的前提必须要有一个父布局,不然系统无法获取LayoutParams。
除了使用布局的LayoutParams之外,还可以使用ViewGroup.MarginLayoutParams来实现这一功能。
//视图坐标方式
@Override
public boolean onTouchEvent(MotionEvent event) {
//通过视图坐标系获得视图坐标
int rawX =(int) event.getX();
int rawY = (int) event.getY();
switch (event.getAction()) { //获取触控事件的类型
case MotionEvent.ACTION_DOWN: //单点触摸按下的事件
//记录触摸点坐标
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE: //触摸点移动动作
//计算偏移量
int offsetX = rawX-lastX;
int offsetY= rawY-lastY;
<span style="color:#ff0000;"> ViewGroup.MarginLayoutParams layoutParams=(MarginLayoutParams) getLayoutParams();
// LinearLayout.LayoutParams layoutParams= (LayoutParams) getLayoutParams</span>();
layoutParams.leftMargin=getLeft()+offsetX;
layoutParams.topMargin=getTop()+offsetY;
setLayoutParams(layoutParams);
break;
}
return true;
}
通过代码可以发现,使用ViewGroup.MarginLayoutParams更加的方便,不需要考虑父布局的类型。
(方法四)scrollTo与scrollBy
在一个View中,系统提供了scrollTo,scrollBy两种方式来改变一个View的位置,scrollTo(x,y)表示移动到一个具体的坐标点(x,y),scrollBy(dx,dy)表示移动的增量为dx,dy。
scrollTo,scrollBy方法移动的是View的content,即让View的内容移动,如果在ViewGroup中使用scrollTo、scrollBy方法,那么移动的将是所有子View,但如果在View中使用,那么移动的将是View的内容。
int offsetX = rawX-lastX;
int offsetY= rawY-lastY;
((View)getParent()).scrollBy(-offsetX, -offsetY);
(方法五)Scroller类:通过Scroller类可以实现平滑移动的效果。
使用Scroller类需要如下三个步骤:
(1)初始化Scroller
首先通过它的构造方法来创建一个Scroller对象, 代码如下:
//初始化Scroller
mScroller = new Scroller(context);
(2)在重写computerScroller()方法,实现模拟滑动, 这个方法是Scroller类的核心,系统在绘制View的时候会在draw()方法中调用该方法。这个方法实际上就是使用scrollTo方法。再结合Scroller对象,帮助获取到当前的滚动值,则就可以通过不断瞬间移动一个小的距离来实现整体上的平滑移动效果。代码如下:
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){ //判断Scroller是否执行完毕
((View)getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//通过重绘来不断调用computeScroll
invalidate();
}
Scroller类提供了computerScrollOffset()方法判断是否完成了整个滑动,同时也提供了getCurrX(),getCurrY()方法来获得当前的滑动坐标。
注意:
invalidate()方法,因为只能在computeScroll()方法中获取模拟过程中的scrollX和scrollY坐标,但computeScroll()方法是不会自动调用的,只能通过invalidate()--->draw()--->computeScroller()来间接调用computeScroll()方法,所有需要在代码中调用invalidate()方法,实现循环获取scrollX和scrollY的目的,当模拟过程结束时,mScroller.computeScrollOffset()方法会返回false,从而中断循环,完成整个平滑移动的过程。
(3)startScroll 开启模拟过程
public void startScroll(int startX,int startY,int dx, int dy,int duration)
public void startScroll(int startX,int startY,int dx, int dy,int duration),它们的区别就是一个具有指定的持续时长,一个没有。其它四个坐标是:起始坐标和偏移量。在获取坐标是,可以使用getScrollX()和getScrollY()方法来获取父视图中content所滑动到点的坐标,这个值是负的。
代码如下:
public class DragView5 extends View{
private Scroller mScroller;
private int lastX;
private int lastY;
public DragView5(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
public DragView5(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public DragView5(Context context) {
super(context);
initView(context);
}
private void initView(Context context){
setBackgroundColor(Color.BLUE);
//初始化Scroller
mScroller = new Scroller(context);
}
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){ //判断Scroller是否执行完毕
((View)getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//通过重绘来不断调用computeScroll
invalidate();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x =(int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: // 单点触摸按下事件
lastX = (int) event.getX();
lastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE: //处理输入的移动事件
int offsetX = x - lastX;
int offsetY = y-lastY;
((View)getParent()).scrollBy(-offsetX, -offsetY);
break;
case MotionEvent.ACTION_UP: //单点触摸离开动作
//手指离开时,执行滑动过程
View viewGroup = (View) getParent();
mScroller.startScroll(viewGroup.getScrollX(), viewGroup.getScrollY(), -viewGroup.getScrollX(),-viewGroup.getScrollY());
invalidate();
break;
}
return true;
}
}
(方法六)属性动画来控制一个View的移动效果。
(方法七)ViewDragHelper, 使用ViewDragHelper创建一个滑动布局。
public class DragViewGroup extends FrameLayout{
private ViewDragHelper mvViewDragHelper;
private View mMenuView, mMainView;
private int mWidth;
public DragViewGroup(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
public DragViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public DragViewGroup(Context context) {
super(context);
initView();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView=getChildAt(0);
mMainView=getChildAt(1);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth =mMenuView.getMeasuredWidth();
}
private void initView(){ //初始化ViewDragHeloer
mvViewDragHelper = ViewDragHelper.create(this, callback);
}
private ViewDragHelper.Callback callback =new ViewDragHelper.Callback() {
//何时开始检查触摸事件
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果当前触摸的child是mMainView时开始检测
return mMainView==child;
}
//处理水平滑动
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
};
//处理垂直滑动
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
};
// 当拖拽状态改变,比如idle,dragging
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}
// 当位置改变的时候调用,常用与滑动时更改scale等
@Override
public void onViewPositionChanged(View changedView,
int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
//拖动结合后调用
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//手指抬起后缓慢移动到指定位置
if(mMainView.getLeft() <500){
//关闭菜单
//相当于Scroller的startScroll方法
mvViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}else {
//打开菜单
mvViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
};
};
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mvViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//将触摸事件传递给ViewDragHelper
mvViewDragHelper.processTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
if(mvViewDragHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}
}