一、两种坐标系
1、Android坐标系:它是以手机屏幕左上角为坐标原点,向右为X轴正方向,向下为Y轴正方向。
2、视图坐标系:描述的是子视图在父视图中的位置。坐标原点是父视图的左上角,向右为X轴正方向,向下为Y轴正方向。
二、获取坐标与相对距离的方法
可以分为两类:
1、View提供的方法
getTop():获取到的是自身上边到父布局顶边的距离
getLeft():获取到的是自身左边到父布局左边的距离
getRight():获取到的是自身右边到父布局左边的距离
getBottom():获取到的是自身底边到父布局顶边的距离
2、MotionEvent提供的方法:
getX():获取点击事件点距离控件左边的距离
getY():获取点击事件点距离控件顶边的距离
getRawX():获取点击事件点距离屏幕左边的距离
getRawY():获取点击事件点距离屏幕顶边的距离
三、实现滑动效果的几种方式:
我们按照这个例子进行实现
1、Layout方法
我们应该知道,当View进行绘制的时候,会调用其onLayout()方法进行显示位置设置,因此,我们可以通过修改view的top,left,right,bottom四个属性进行位置改变。
首先,我们通过MotionEvent获取到触摸点的坐标,因为涉及到拖动,所以,我们每次回调onTouchEvent都需要获取一次;
int x = (int) event.getX();
int y = (int) event.getY();
然后,需要在ACTION_DOWN的事件中记录下初始按下的坐标,方便计算偏移量
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
最后,在ACTION_MOVE事件中,计算偏移量,并通过layout()方法,在目前top,left,right,bottom的基础上加上偏移量,从而改变view位置,实现滑动
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
break;
这里使用getX和getY来获取坐标值,即通过视图坐标来获取偏移量,我们也同样可以使用getRawX和getRawY来获取坐标,通过绝对坐标系来计算偏移量,代码如下
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
//重新初始化坐标
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
使用绝对坐标系,有一点要注意,就是每次执行完ACTION_MOVE之后都要重新初始化坐标,才可以准确的计算偏移量。
2、offsetLeftAndRight()和offsetTopAndBottom()
系统提供了一个对左右、上下移动的API封装,只需要将计算出的偏移量传递给这两个方法,就可以完成layout()方法同样的效果。
offsetLeftAndRight(offsetX); //同时对left和right进行偏移
offsetTopAndBottom(offsetY); //同时对top和bottom进行偏移
偏移量的计算方式同上。
3、LayoutParams
通过修改view的Margin属性,也可以实现效果,因此我们可以用ViewGroup下的MarginLayoutParams来实现。而且用MarginLayoutParams更加便捷,不需要考虑view所在父布局的类型。
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
params.leftMargin = getLeft() + offsetX;
params.topMargin = getTop() + offsetY;
setLayoutParams(params);
经常有朋友会将scrollTo和scrollBy混淆,所以我们把它放到一起进行举例,而Scroller类与它们有着千丝万缕的联系。下面我们就上面的效果进行实现,即一个随手指移动的滑块,当我拖动到某个位置后,抬起手指,滑块回滚到初始位置。
scrollTo是指移动到一个指定的位置,scrollBy是移动的增量,即相对位置。而且scrollTo与scrollBy有以下几个值得注意:
1、View与ViewGroup调用是有却区别,View调用,移动的是它的内容,如TextView的文本。ViewGroup移动的则是它内部的子View
2、通过scrollTo与scrollBy移动,看似是view在移动,实际上移动的是可视区域,而View在视图(画布)的位置其实是不变的。这也可以解释,为什么通过scrollTo与scrollBy移动后的view,他们的getTop、getLeft等获得的值不改变的原因。
3、既然是可视区域在移动,那么当我们向正方向移动时,view看上去是向负方向移动。因此我们在移动通过scrollTo与scrollBy移动View时,需要将计算的偏移量进行正负取反操作
好,下面来看实现代码
public class MyViewTwo extends View {
private int lastX;
private int lastY;
private Scroller ms;
public MyViewTwo(Context context) {
super(context);
ms = new Scroller(context);
}
public MyViewTwo(Context context, AttributeSet attrs) {
super(context, attrs);
ms = new Scroller(context);
}
public MyViewTwo(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ms = new Scroller(context);
}
@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) getParent()).scrollBy(-offsetX, -offsetY);
break;
case MotionEvent.ACTION_UP:
View vg = (View) getParent();
ms.startScroll(vg.getScrollX(), vg.getScrollY(), -vg.getScrollX(), -vg.getScrollY(), 200);
invalidate();
break;
}
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if (ms.computeScrollOffset()) {
((View) getParent()).scrollTo(ms.getCurrX(), ms.getCurrY());
invalidate();
}
}
}
然后,需要说的就是Scroller类与computeScroll()方法。
我们知道不管是scrollTo还是scrollBy,他的移动都是跳跃式的,简单说就是突然出现在指定的位置上,而没有一个从初始位置到指定位置的滑动过程,这就导致了滑动看起来不圆滑顺畅,看上去非常突兀。那为什么我们拖动滑块的时候,没有跳跃的感觉,那是因为我们拖动过程中,一直在执行onTouchEvent的ACTION_MOVE分支,其移动的位置一直在重复的进行计算改变,而且回调出发的频率较高,导致移动端距离被碎片化成一个个小段,导致肉眼看上去不是很明显。当时当我们抬起手指,滑块复位到初始位置时,如果只是指定初始位置,就会出现跳跃现象。通过Scroller类,我们就可以平滑的实现滑动效果。
使用Scroller类可以分为三个步骤:
1、初始化Scroller类,我们通过它的构造器就可以拿到Scroller对象
Scroller ms = new Scroller(context);
2、重写computeScroll()方法,实现模拟滑动。它是Scroller的核心,系统在绘制View的时候,绘制draw()方法中调用该方法,这个方法实际上调用的就是scrollTo方法,结合Scroller对象,帮助获取当前滚动值。我们通过不间断的滚动一小段距离来实现整体的平滑滚动。
@Override
public void computeScroll() {
super.computeScroll();
if (ms.computeScrollOffset()) {
((View) getParent()).scrollTo(ms.getCurrX(), ms.getCurrY());
invalidate();
}
}
Scroller提供了computeScrollOffset()方法来判断滚动完成状态,true说明滚动尚未完成。
Scroller提供了getCurrX与getCurrY方法来获取当前需要滚动的坐标偏移量。
需要注意到是,我们需要主动调用重绘方法invalidate()来间接调用computeScroll,直到滚动完成,computeScrollOffset返回false,中断循环,完成整改平滑滚动过程。
3、startScroll开启模拟过程
case MotionEvent.ACTION_UP:
View vg = (View) getParent();
ms.startScroll(vg.getScrollX(), vg.getScrollY(), -vg.getScrollX(), -vg.getScrollY(), 200);
invalidate();
break;
startScroll拥有两个重载方法:
public void startScroll(int startX,int startY,int dx,int dy)
public void startScroll(int startX,int startY,int dx,int dy,int duration)
首先,前四个参数分别代表:起始X坐标,起始Y坐标,X轴偏移量,Y轴偏移量,至于duration,顾名思义,就是指定完成的时长。
这里我们通过其父容器,获得到View的移动距离getScrollX与getScrollY,并将其取反作为其回到初始位置的偏移量。
需要注意的是,我们同样需要主动调用invalidate方法通知View进行重绘,间接调用computeScroll方法
demo下载地址:点击打开链接