Android群英传学习笔记,介绍了android坐标系、视图坐标系、onTouch事件分发、layout方法、scrollBy方法、Scroller、ViewDragHelper 等等内容。
在介绍如何实现View滑动之前先了解一下Android的坐标系,我们在初中数学就学过坐标系,有原点和X轴Y轴,不过屏幕上的坐标系稍微有点区别,移动设备一般将屏幕的左上角定义为原点,向右为X轴正方向,向下为Y轴正方向,如下图:
与屏幕坐标系相同,View也有自己的坐标系,我们可以称之为视图坐标系,描述了本身和父布局的位置关系,原点在View的左上角:
View自身坐标获取方法
getTop():获取到的,是view自身的顶边到其父布局顶边的距离
getLeft():获取到的,是view自身的左边到其父布局左边的距离
getRight():获取到的,是view自身的右边到其父布局左边的距离
getBottom():获取到的,是view自身的底边到其父布局顶边的距离
MotionEvent坐标获取
getX():获取点击事件相对控件左边的x轴坐标,即点击事件距离控件左边的距离
getY():获取点击事件相对控件顶边的y轴坐标,即点击事件距离控件顶边的距离
getRawX():获取点击事件相对整个屏幕左边的x轴坐标,即点击事件距离整个屏幕左边的距离
getRawY():获取点击事件相对整个屏幕顶边的y轴坐标,即点击事件距离整个屏幕顶边的距离
说了这么多方法都不如一张图最直接:原图链接
学好触控事件是掌握后续内容的重要基础,触控事件回调的MotionEvent封装了一些常用的事件常量,定义了一些常见类型动作。
/**
* A pressed gesture has started, the motion contains the initial starting location.
*/
public static final int ACTION_DOWN = 0;
/**
* A pressed gesture has finished, the motion contains the final release location as well as any intermediate
* points since the last down or move event.
*/
public static final int ACTION_UP = 1;
/**
* A change has happened during a
* press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}).
*/
public static final int ACTION_MOVE = 2;
/**
* The current gesture has been aborted.
*/
public static final int ACTION_CANCEL = 3;
/**
* A movement has happened outside of the normal bounds of the UI element.
*/
public static final int ACTION_OUTSIDE = 4;
/**
* A non-primary pointer has gone down.
*/
public static final int ACTION_POINTER_DOWN = 5;
/**
* A non-primary pointer has gone up.
*/
public static final int ACTION_POINTER_UP = 6;
我们让View滑动的大概思路是重写View的onTouchEvent(MotionEvent event)方法,来控制View的移动,这个代码模板基本固定的,show me the code :
@Override
public boolean onTouch(View v, MotionEvent event) {
// 记录当前point所在的位置
x = (int) event.getX();
y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//处理按下事件
break;
case MotionEvent.ACTION_MOVE:
//处理移动事件
break;
case MotionEvent.ACTION_UP:
//处理松开事件
break;
}
// 事件处理完毕
return true;
}
该方法return true 代表触控事件到这里就处理完毕了,不必要再继续传递,不懂的可以去再回顾一下Android的触摸事件分发机制。下面我们就可以进入主题,来看一下有哪些方法可以移动View。
我们了解了Android坐标系和触控事件,接着我们可以模拟实现View的滑动了,思路是:当发生onTouch事件时,记录下位置,当手指移动时,记录移动的坐标,获得一个相对偏移量,然后修改View的位置,不断重复下去就实现了View的模拟滑动。那么,怎么改动View的位置呢,下面有介绍几种方法可以设置View的位置。
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
int x = (int) event.getX();
int y = (int) event.getY();
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
break;
效果如图:
看命名就知道这个方法的作用,这是系统提供的对View上下、左右同时进行移动的API,效果与上相同。就不赘述了。
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
break;
通过改变View的LayoutParams布局参数,就可以移动View的位置,这里通常修改View的Margin属性,代码如下:
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
params.leftMargin = getLeft() + offsetX;
params.topMargin = getTop() + offsetY;
break;
其实根据父布局的类型,可以设置LinearLayout.LayoutParams或者RelativeLayout.LayoutParams,不过这样就必须先知道父布局的类型,不如ViewGroup.MarginLayoutParams来的方便。
前者表示移动到具体的坐标点位置,后者表示在原有的位置基础上再移动一个偏移量。但是与之前三个方法不同的是,前三个方法都是移动View自己本身,而这两个方法移动的都是View里面的内容,如果放在ViewGroup中使用,则移动的是ViewGroup里面所有的子View。
那我们的思路就要换一下了,我们为了移动View,那我们就来移动View所在的ViewGroup,但是要注意的是,移动的偏移量要取反,为什么呢?这是因为本来是该View移动dx、dy,现在View保持不动,让ViewGroup移动,则根据相对运动原理,就相当于ViewGroup移动了-dx、-dy。
下面我们来举个简单的例子解释scrollBy。如下图:ViewGroup里面是可视区域,第二个小人的坐标是(200,100),现在我们把ViewGroup移到第二个小人的位置,scrollBy(200,100),效果如第二张图:在可视区域内,相当于第二个小人的偏移量为(-200,-100)。
这么解释一定明白多了,我们看一下实现代码:
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View)getParent()).scrollBy(-offsetX,-offsetY);
break;
效果如图:
通过Scroller类来实现一些平滑的动画效果,可以设置动画时间等等,简直就是滑动利器!现在我们来实现一个效果:View跟着手指滑动,当松开手指时就让View回到原始位置。
scroller = new Scroller(context);
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()) {
offsetLeftAndRight(scroller.getCurrX()-getLeft());
offsetTopAndBottom(scroller.getCurrY()-getTop());
invalidate();
}
}
scroller.startScroll(getLeft(),getTop(),-getLeft()+initX,-getTop()+initY,2000);
startScroll前两个参数是起始位置,后两个参数为终点位置,第五个参数是动画持续时间,可以省略。
效果如图:
这个后续再单独写一篇介绍属性动画的,跳过。
在开发自定义ViewGroup的时候,经常要根据业务需求实现onInterceptTouchEvent和onTouch(很繁琐啊!有木有!),不过Google在support库中为我们提供了一个超级强大的类ViewDragHelper,可以实现诸多滑动布局,侧滑菜单就是之一。这里奉上官方介绍,可能需要翻墙?
下面我们举个实现侧滑菜单的例子:自定义ViewGroup布局,然后里面有MenuView和MainView,滑动MainView超过一定距离就显示MenuView。
private View mMenuView,mMainView;
private int mWidth;
//布局完成后调用
@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();//侧滑菜单的宽度
}
//构造函数
public ViewDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mViewDragHelper = ViewDragHelper.create(this,callback);
}
其中,构造函数里的callback是我们要自己实现的业务逻辑。也是该类的重要核心内容!
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return mViewDragHelper.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
private final ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
//触摸的布局是否为MainView
return mMainView==child;
}
@Override
public int clampViewPositionVertical(View child,int top,int dy) {
//不需要检测垂直滑动,直接返回0
return 0;
}
@Override
public int clampViewPositionHorizontal(View child,int left,int dx) {
return left;
}
@Override
public void onViewReleased(View child,float xVel,float yVel) {
super.onViewReleased(child,xVel,yVel);
//核心逻辑:滑动MainView超过一定距离就显示MenuView
if (mMainView.getLeft() < mWidth) {
mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
ViewCompat.postInvalidateOnAnimation(ViewDragLayout.this);
} else {
mViewDragHelper.smoothSlideViewTo(mMainView,mWidth,0);
ViewCompat.postInvalidateOnAnimation(ViewDragLayout.this);
}
}
};
来看一下效果:
ViewDragHelper.Callback中定义有大量的回调方法,就不一一介绍了。
到这里,我们介绍的View滑动方法就学习完了,最后我们来实现一个滑动ViewGroup,来模拟微信下拉的粘性动画,直接上代码:
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (scroller.computeScrollOffset())
scroller.forceFinished(true);
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
int scrollY = y - lastY;
offsetTopAndBottom(scrollY/3-getTop());
break;
case MotionEvent.ACTION_UP:
scroller.startScroll(getLeft(),getTop(),0,-getTop()+initY,duration);
invalidate();
break;
}
return true;
}
在ACTION_DOWN里判断动画有没有结束,可以强制结束,这样就可以连续向下拖动。在ACTION_MOVE里设置偏移量,除以3可以调节偏移量滑动的比例。最后在松手时回到原位置,一起来看一下效果吧。
参考: