在Android中,将屏幕左上角的顶点作为Android坐标系的原点,从这个点向右是X轴正方向,向下是Y轴正方向。
系统提供了getLocatinoOnScreen(int location[]);
这样的方法来获取Android坐标系中点的位置。
视图坐标系描述子视图在父视图中的位置关系,视图坐标系的原点是父视图的左上角。通过getX()
和getY()
获得的是视图坐标系中的坐标。
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;
getTop()
:获取到的是View自身的顶边到其父布局顶边的距离
getLeft()
:获取到的是View自身的左边到其父布局左边的距离
getRight()
:获取到的是View自身的右边到期父布局左边的距离
getBottom()
:获取到的是View自身的底边到其父布局顶边的距离
getX()
:获取点击事件距离控件左边的距离,即视图坐标
getY()
:获取点击事件距离控件顶边的距离,即视图坐标
getRawX()
:获取点击事件距离整个屏幕左边的距离,即绝对坐标
getRawY()
:获取点击事件距离整个屏幕顶边的距离,即绝对坐标
可以参考下图:
用View中的layout(int left,int top,int right,int bottom)
方法在每次触摸移动后加上相应的偏移量就能实现滑动:
@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;
layout(getLeft() + offsetX, getTop() + offsetY,
getRight() + offsetX, getBottom() + offsetY);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
或者使用getRawX()
和getRawY()
方法来获取坐标,不过要记得在ACTION_MOVE
逻辑的最后为lastX
和lastY
重新赋值。
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
layout(getLeft() + offsetX, getTop() + offsetY,
getRight() + offsetX, getBottom() + offsetY);
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
offsetLeftAndRight()
与offsetTopAndBottom()
这个方法相当于系统提供的左右、上下移动的API的封装,计算出偏移量后,只需要使用如下的代码完成View的重新布局:
//对left和right进行偏移
offsetLeftAndRight(offsetX);
//对top和bottom进行偏移
offsetTopAndBottom(offsetY);
LayoutParams
LayoutParams
保存了一个View的布局参数,因此可以在程序中通过改变LayoutParams
来动态地修改一个布局的位置参数。从而达到改变View位置的效果。使用getLayoutParams()
来获取一个View的LayoutParams
然后通过setLayoutParams()
来改变View的LayoutParams
,代码如下所示:
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
scrollTo
和scrollBy
scrollTo(x,y)
表示移动到一个具体的具体的坐标点(x,y),scrollBy(dx,dy)
表示移动的偏移量为dx、dy;
使用scrollBy
或scrollTo
时需要通过父布局来进行移动,因为这两个方法移动的是View的内容或者ViewGroup的子view。因此通过父View调用该方法移动的才是子View,还有一点是此时移动的方向正好和offset的方向相反,需要将其取反。代码如下:
((View)getParent()).scrollBy(-offsetX,-offsetY);
Scroller
scrollBy()
和scrollTo()
方法实现的移动都是瞬间完成的,而Scroller
类可以实现平滑移动的效果。使用Scroller
都要重写computeScroll()
方法,系统在绘制View时会在draw()
方法中调用它,这个方法实际上就是使用scrollTo()
的方法,再结合Scroller
对象帮助获取当前的滚动值,来实现滚动效果。下面是computeScroll()
的代码:
@Override
public void computeScroll(){
super.computeScroll();
if(mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(),
mScroller.getCurrY());
invalidate();
}
}
computeScrollOffset()
方法判断是否完成了整个滑动过程,过程结束后该方法会返回false,getCurrX()和getCurrY()
来获得当前的滑动坐标。通过invalidate()
方法重绘来循环调用computeScroll()
方法。
使用Scroller
的startScroll()
方法来开启滑动过程,Scroller
类提供了两个重载的方法:
startScroll(int startX,int startY,int dx,int dy,int duration);
startScroll(int startX,int startY,int dx,int dy);
下面的代码实现了View跟随手指滑动,当手指离开屏幕时,View自动回到原位置的效果:
int lastX;
int lastY;
View parent = ((View)getParent());
@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;
.scrollBy(-offsetX,-offsetY);
break;
case MotionEvent.ACTION_UP:
mScroller.startScroll(
parent.getScrollX(),
parent.getScrollY(),
-parent.getScrollX(),
-parent.getScrollY());
invalidate();
break;
}
}
使用属性动画也能完成对View的移动
通过ViewDragHelper
可以实现各种不同的滑动、拖放需求。通过实现类似QQ侧滑栏的布局来了解一下ViewDragHelper
首先初始化ViewDragHelper:
mViewDragHelper = ViewDragHelper.create(this,mCallback);
第一个参数是要监听的View,通常是一个ViewGroup,即ParentView,第二个参数是一个Callback回调,这个回调是整个ViewDragHelper的逻辑核心。
然后还要重写事件拦截方法,将事件传递给ViewDragHelper来处理:
@Override
public boolean onInterceptTouchEvent(MotionEvent event){
return mViewDragHelper.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotoinEvent event){
mViewDragHelper.processTouchEvent(event);
return true;
}
处理computeScroll()
@Override
public void computeScroll(){
if(mViewDragHelper.continueSetling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}
处理callback
回调
private ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback(){
@Override
public boolean tryCaptureView(View child,int pointerId){
return mMainView == child;
}
}
通过这个方法我们可以指定创建ViewDragHelper
时,参数parentView
中哪一个子view可以被移动,在这个实例中自定义了一个ViewGroup,里面有两个子view——mMenuView和mMainView,当指定了上述代码时,只有mMainView是可以被拖动的。
然后处理具体的滑动方法——clampViewPositionVertical()
和clampViewPositionHorizontal()
,分别对应垂直和水平滑动。必须重写这两个方法实现滑动。它们的默认返回值是0,即不发生滑动,只重写其中一个,就会实现对应方向上的滑动。代码如下所示:
@Override
public int clampViewPositionVertical(View child,int top,int dy){
return top;
}
@Override
public int clampViewPositionHorizontal(View child,int left,int dx){
return left;
}
参数top和left分别代表在对应方向上的child移动的距离,dy、dx则表示相对于前一次的增量。通常情况下只需要返回top和left即可,但当需要更加精确地计算padding等属性时,就需要针对left和top进行一些处理,并返回合适的值。
下面是实现水平滑动的代码:
private ViewDragHelpe.Callback mCallback = new ViewDragHelper.Callback(){
@Override
public boolean tryCaptureView(View child,int pointerId){
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;
}
}
下面实现手指离开屏幕后View返回原来位置的效果:
在ViewDragHelper.Callback 中重写onViewReleased
方法
@Override
public void onViewReleased(View releasedChild,float xvel,float yvel){
super,onViewReleased(releasedChild,xvel,yvel);
if(mMainView.getLeft() < mWidth / 3){
mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}else{
mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
上述代码让MainView移动后左边距小于其三分之一时回到原位置,否则就滑动到300的位置。
下面是自定义DragViewGroup的完整代码:
package com.chaoyang805.chapter5scrolldemo.view;
import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
/** * Created by chaoyang805 on 2015/12/16. */
public class DragViewGroup extends FrameLayout {
private ViewDragHelper mHelper;
private View mMenuView;
private View mMainView;
private int mWidth;
public DragViewGroup(Context context) {
this(context, null);
}
public DragViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mHelper = ViewDragHelper.create(this,mCallback);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mHelper.processTouchEvent(event);
return true;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mWidth = mMenuView.getMeasuredWidth();
}
@Override
public void computeScroll() {
if (mHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
private ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return mMainView == child;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
if (left < 0 && dx < 0) {
return 0;
}
return left;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
if (mMainView.getLeft() < mWidth / 3) {
mHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
} else {
mHelper.smoothSlideViewTo(mMainView,300,0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
};
}
ViewDragHelper.Callback
中其他事件介绍:
//这个事件在用户触摸到View后回调
onViewCaptured(View capturedChild,int activePointerId);
//这个事件在拖拽状态改变时的回调,比如idle、dragging等
onViewDragStateChanged(int state);
//这个事件在位置改变时回调,常用于滑动时更改scale进行缩放等效果
onViewPositionChanged(View changedView,int left,int top,int dx,int dy);