目录
一:View的位置参数
二:获取点击事件发生的x和y坐标
三:TouchSlop
四:VelocityTracker
五:GestureDetector:
六:Scroller:弹性滑动对象,用于实现View的弹性滑动;
七:View的滑动
八:自定义view滑动冲突
九:在Activity中获取宽高
(1)Android坐标系:将屏幕左上角的顶点作为Android坐标系的原点,这个原点向右是X轴正方向,向下是Y轴正方向。
在触控事件中,通过getRawX()和getRawY()方法获得的坐标就是Android坐标系的坐标;
(2)View坐标系:是View到其父控件的坐标;
width=getRight-getLeft;【获取View自身的宽】
height=getBottom-getTop;【获取View自身的高】
通过getTop、getLeft、getBottom、getRight方法可以获得View到其父控件的距离;
MotoinEvent通过MotionEvent对象,我们可以得到点击事件发生的x和y坐标。
getRawX获取相对屏幕左上角的x坐标,getX相对于当前View左上角的坐标。
通过MotionEvent对象,我们可以获取手指接触屏幕的操作。ACTION_DOWN、ACTION_MOVE、ACTION_UP。
TouchSlop是系统认为的滑动最小距离;通俗的说,如果滑动距离小于TouchSlop,系统就不认为这是滑动;
TouchSlop和设备有关,获取代码:
int mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
VelocityTracker:速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。
手指逆着坐标系的正方向滑动,所产生的速度为负值,顺着正反向滑动,所产生的速度为正值。
4.1:在View的onTouchEvent方法中追踪当前事件的速度:
VelocityTracker mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(event);
4.2:当我们想获取当前速度是,采用如下方法:
mVelocityTracker.computeCurrentVelocity(1000);//设置时间间隔,单位ms
int velocityY = (int) mVelocityTracker.getYVelocity();
int velocityX= (int) mVelocityTracker.getXVelocity();
4.3:当我们不需要的时候,需要clear方法重置并回收内存;
mVelocityTracker.clear();
mVelocityTracker.recycle();
GestureDetector:手势监测,用于辅助检测用户的单击、滑动、长按、双击等行为;
1.创建一个GestureDetector对象,可以实现OnGestureListener或者setOnDoubleTapListener(监听双击行为);
GestureDetector mGestureDetector = new GestureDetector(new GestureDetector.OnGestureListener() {
});
mGestureDetector.setIsLongpressEnabled(false);//解决长按屏幕后无法拖动的现象;
2.接着在View的onTouchEvent方法中,添加如下实现:
boolean consum = mGestureDetector.onTouchEvent(event);
return consum;
做完上面两步,我们可以有选择地实现OnGestureListener和OnDoubleTapListener中的方法。
onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)、onDoubleTap(双击)。
建议:如果只是监听滑动相关的,建议在onTouchEvent中实现。如果要监听双击这种行为,那么就使用OnGestureListener。
当使用View的scrollTo/scrollBy方法进行滑动的时候,这个过程是瞬间完成的。这个没有过渡效果的滑动体验很差。
Scroller就可以实现过渡滑动的效果。Scroller本身是不能实现View的滑动的,它需要与View的computeScroll方法结合才能实现弹性滑动的效果。
如何使用Scroller呢?它的典型代码是固定的。
Scroller scroller = new Scroller(getContext());
private void smoothScrollTo(int destX, int dextY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
//1000ms滑向dextX
scroller.startScroll(scrollX,0,delta,0,1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
原理:smoothScrollTo方法中调用invalidate方法,invalidate会导致View重绘,调用draw方法。draw方法中调用computeScroll方法,computeScroll是个空实现,需要我们重写。computeScroll会去向scroller获取当前的scrollx和scrolly,调用scrollTo来实现滑动。接着调用postinvalidate进行第二次重绘。如此反复,直到整个滑动过程结束。
7.1:View的滑动主要有三种实现方式
1、通过View本身提供的scrollTo/ScrollBy方法,使用scrollTo/ScrollBy来实现View的滑动,只能将View的内容进行移动,并不能将View本身进行移动。
2、通过动画给View施加平移效果来实现滑动,可以实现复杂的动画效果。
3、通过改变View的LayoutParams使得View重新布局从而实现滑动。操作稍微复杂,适用于有交互的View。
比如我们想把一个Button向右平移100px,只需要将这个Button的LayoutParams的marginLeft参数增加100px即可。
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) mButton.getLayoutParams();
layoutParams.width += 100;
layoutParams.leftMargin += 100;
mButton.requestLayout();
//或者也可以使用 mButton.setLayoutParams(layoutParams);
应用场景:
如果我们实现一个View跟随手滑动的效果:
实现思路:重写onTouchEvent方法,并处理ACTION_MOVE事件。
可以采用改变布局方式的方式实现,也可以使用动画实现(但是因为动画的性质,3.0以下版本无法实现新位置的点击事件)。
1.外部拦截法
点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截。如果不需要就不拦截。这样就可以解决滑动冲突的问题。
ACTION_DOWN:父容器必须返回false,如果ACTION_DOWN返回true,那么后续的MOVE和UP事件都会直接交由父容器处理。就没办法传递给子View了。
ACTION_MOVE:根据需要决定是否拦截事件。
ACTION_UP:这里也必须返回false。如果返回true,就会导致子元素无法获取UP事件。
怎样判断拦截事件呢?可以根据水平和竖直的滑动距离来判断。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
int x = (int) ev.getX();//获取点击事件距离控件左边的距离
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要拦截事件) {
intercept = true;
} else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
default:
break;
}
mLastXIntercept=x;
mLastYIntercept=y;
return intercept;
}
有以下三种方法:
view.post、ViewTreeObserver、onWindowFocusChanged
4.1:view.post
Activity获取View的宽高,在onCreate、onResume等方法中获取到的都是0,因为View的测量过程并不是和Activity的生命周期同步执行的。
view.post投递一个Runnable,等looper调用此Runnable的时候,view已经初始化好了。
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
4.2:ViewTreeObserver使addOnGlobalLayoutListene
ViewTreeObserver使addOnGlobalLayoutListene
接口, 当view树的状态发生改变或者View树内部的view的可见性发生改变时,onGlobalLayout都会被调用, 需要注意的是,onGlobalLayout方法可能被调用多次, 代码如下:
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
4.3:onWindowFocusChanged
onWindowFocusChanged这个方法的含义是View已经初始化完毕了, 宽高已经准备好了, 需要注意的就是这个方法可能会调用多次, 在Activity onResume和onPause的时候都会调用, 也会有多次调用的情况。
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if(hasWindowFocus){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}