一、View的位置
1、Left、Right、Top、Bottom
left = view.getLeft(); // 表示view的左边距离父控件左边的距离
right= view.getRight(); // 表示view的右边距离父控件左边的距离
top= view.getTop(); // 表示view的上边距离父控件顶部的距离
bottom= view.getBottom(); // 表示view的下边距离父控件顶部的距离
2、X、Y、TranslationX、TranslationY
translationX = view.set/getTranslationX(); // 表示属性动画平移的X方向距离
translationY = view.set/getTranslationY(); // 表示属性动画平移的Y方向距离
x = view.getX();
y = view.getY();
其中X和Y表示view左上角的坐标,这个坐标会随着translation而发生变化。
3、结论如下
(1)上面的8个参数值都是相对于父控件的值
(2)Left、Right、Top、Bottom都是基于原始位置的值,哪怕View发生过平移,这个值依然不会发生改变。
(3)X = getLeft() + getTranslationX();
Y = getTop() + getTranslationY();
Width = getRight() - getLeft();
Height = getBottom() - getTop();
二、滑动参数
1、MotionEvent
MotionEvent.ACTION_DOWN ---- 手指按下屏幕
MotionEvent.ACTION_MOVE ---- 手指在屏幕移动
MotionEvent.ACTION_UP ---- 手指从屏幕松开
MotionEvent.ACTION_CANCEL ---- 非人为意外的中断行为
MotionEvent.getX/getY,获取到触摸点的坐标,这个坐标是相对于当前View左上角的坐标值。
MotionEvent.getRawX/getRawY,获取到触摸点的坐标,这个坐标是相对于手机屏幕左上角的坐标值。
2、TouchSlop
TouchSlop是系统所能识别出的最小滑动距离,跟设备有关系,不同设备不一样。获取方式是,ViewConfiguration.get(context).getScaledTouchSlop();
3、VeloctiyTracker
用于追踪手指的滑动速度,包括水平和垂直方向的滑动速度。
VelocityTracker vt = VelocityTracker.obtain();
vt.addMovement(event);
当我们想计算当前的滑动速度时,可采用下面的方式获取:
vt.computeCurrentVelocity(1000); // 必须先调用计算,1000表示时间,单位毫秒
int xVelocity = (int) vt.getXVeloctiy(); // 获取水平滑动速度
int yVelocity = (int) vt.getYVeloctiy(); // 获取垂直滑动速度
不需要的时候,需要释放和回收内存:
if (vt != null) {
vt.clear();
vt.recycler();
}
关于时间参数,这里再说明一下:
假设1000ms内X方向滑动了100个像素,那么xVelocity = 100像素/1S = 100
假设我们设置的是100ms,同样也是滑动了100个像素,那么xVelocity =100像素/0.1S = 1000
此外,速度是可以负数的,因为滑动距离从左往右是正数,从右往左滑就是负数了。
4、GestureDetector
手势检测,用于辅助检测用户的单击、双击、长按、滑动等行为。使用方法如下:
GestureDetector gd = new GestureDetector(this, new SimpleOnGestureListener() {
public boolean onFling() {
}
});
public boolean onTouchEvent(MotionEvent event) {
return gd.onTouchEvent(event);
}
实际开发中,很多时候都是在View的OnTouchEvent里实现相应的监听或者事件处理,如果单纯只是监听类似于双击这种行为的话,可以选择GestureDetoctor
5、ScrollTo、ScrollBy
(1)ScrollBy最终调用的是ScrollTo方法,只不是By里传的参数是偏移量,而To里传的参数里坐标位置
(2)这两个方法都可以让View实现滑动,但是要注意的是,并不是View本身滑动,而是让其里面的内容进行滑动
(3)下图中,正方形框表示View,黄色表示View的内容。第2个图,scrollX=100,相当于该View的内容向左滑动100像素,即黄色区域向左了100像素。第3个图,scrollY=100,表示View的内容向上滑动了100像素。
三、弹性滑动
1、Scroller方式
/**
* 开启滚动
*/
public void startScroll(Context context, int startX, int dx) {
if (mScroller == null) {
mScroller = new Scroller(context);
}
// scroller源码,startScroll并没有做什么事情,仅仅只是保存了滚动的起始位置
// 滚动的距离以及时间等变量
mScroller.startScroll(startX, 0, dx, 0, 1000);
// 这里调用invalidate去刷新的目的是,让其重新执行View onDraw方法
// 而onDraw方法刷新会触发其父类的computeScroll方法
// 父类的computeScroll则是一个空实现,所以最终会调用我们重写的computeScroll方法
invalidate();
}
@Override
public void computeScroll() {
// 核心就是mScroller.computeScrollOffset()方法了
if (mScroller != null && mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
/**
* mScroller的这个方法根据时间的流逝的比例来判断应该滚动多少像素
* 比如我们startScroll的时候记录了当前的时间A
* 经过onDraw,执行computeScroll后,computeScrollOffset被调用的时候记录当前时间B
* (B - A) / total,就能知道整体流逝的比例,从而计算出应该滚动的距离
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
.....
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
2、属性动画
// 属性动画,方式1
ObjectAnimator.ofFloat(view, "translationX", 0, 100)
.setDuration(500)
.start();
// 属性动画,方式2
ValueAnimator va = ValueAnimator.ofFloat(0, 100);
va.setDuration(500);
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = animation.getAnimatedFraction();
view.scrollTo((int) value, 0);
// 又或者直接setLayoutParams的形式
// FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) view.getLayoutParams();
// params.leftMargin = (int) value;
// view.setLayoutParams(params);
}
});
va.start();
3、Thread+Handler的形式也可以
四、触摸事件分发机制
1、事件分发顺序
Activity -> ViewGroup -> View,即1个点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到View。即Activity(dispatchTouchEvent) -> Window -> ViewGroup(dispatchTouchEvent)
2、事件分发顺序的源码
/**
* Activity的dispatchTouchEvent方法
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
// 这个调用可以忽略,我们看主干
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 调用window的superDispatchTouchEven方法
// 我们知道在Android里,PhoneWindow是Window类的唯一实现类,
// 所以实际上调用的就是PhoneWindow的superDispatchTouchEvent
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
/**
* PhoneWindow的superDispatchTouchEvent方法
*/
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
// 直接调用DecorView的superDispatchTouchEvent方法,
// DecorView也就是我们屏幕显示的最根布局的FrameLayout(ViewGroup)
return mDecor.superDispatchTouchEvent(event);
}
基于上述的源码,我们可以知道,事件经由Activity传递Window再到ViewGroup的dispatchTouchEvent进行处理。
当整个dispatchTouchEvent的返回值是true时,Activity的事件分发也就完成了。当整个dispatchTouchEvent的返回值是false时(相当于其里的ViewGroup或者View的dispatchTouchEvent或者onTouchEvent返回false,没有消费这个事件),会调用Activity的onTouchEvent方法。这个方法的源码如下:
/**
* Activity的onTouchEvent方法
*/
public boolean onTouchEvent(MotionEvent event) {
// 判断这个触摸事件是否超出边界。如果超出,Activity直接finish掉。
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
3、ViewGroup的dispatchTouchEvent源码
/**
* ViewGroup的dispatchTouchEvent方法
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 按下时,重置一些标记位及状态
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 这里是重点,如果是按下事件或者mFirstTouchTarget != null就可以进去。
// 如果事件能够传递到子View去处理(消费),那么mFirstTouchTarget就指向该子View
// 换句话,如果上一个事件被拦截了,又或者子View没有消费这个事件,最终当前这个ViewGroup消费掉时
// 那么mFirstTouchTarget就等于null,那么intercepted=true
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 禁止拦截功能是否开启,默认是false,即关闭的。可通过requestDisallowIntercept方法来控制这个开关。
if (!disallowIntercept) {
// 调用onInterceptTouchEvent方法
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 如果intercepted=true,表示拦截,那么就不会执行其内部的分发给子View的代码
// 如果intercepted=false,表示不拦截,就会执行分发给子View的代码
if (!canceled && !intercepted) {
// 分发给子View的代码,这里的主要逻辑是遍历找出其子View
// 然后child.dispatchTouchEvent()把事件传递给子View
}
if (mFirstTouchTarget == null) {
// 看下面的方法
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
}
}
private boolean dispatchTransformedTouchEvent(MotionEvent event) {
// 核心代码
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
}
通过上述源码得到以下结论:
1、down事件时,必然会走onInterceptTouchEvent方法的。但是,move和up事件就不一定了。
2、如果onInterceptTouchEvent返回true,表示事件被拦截。那么就不会进入分发给子View的代码。而是调用dispatchTransformedTouchEvent方法,最终由于child==null,而调用其父类View的dispatchTouchEvent,View的dispatchTouchEvent方法下面会讲到,最核心的代码就几行,其中一行是会调用onTouchEvent方法。而子类ViewGroup里重写了onTouchEvent。所以这就解释了为什么我们拦截了事件之后,会执行其自己的onTouchEvent方法了。
3、如果onInterceptTouchEvent返回false,表示事件往下分发。从上面的分析可以知道,就会进入分发给子View的代码,调用child.dispatchTouchEvent方法进行事件传递。
4、可通过requestDisallowIntercept方法来控制onInterceptTouchEvent的调用情况
4、View的dispatchTouchEvent源码
/**
* View的dispatchTouchEvent方法,我们只看重点代码
*/
public boolean dispatchTouchEvent(MotionEvent event) {
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
// 这里会调用当前View设置的OnTouchListener的onTouch方法,其返回值相当重要
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 如果没有设置OnTouchListener或者OnTouchListener的onTouch方法返回是false,那么自己的onTouchEvent就会被调用
// 否则,onTouchEvent将不执行。
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
通过上述源码得到以下结论:
1、OnTouchListener的onTouch方法会比View自己的onTouchEvent方法优先执行。
2、OnTouchListener的onTouch方法返回值如果是true,表示事件在此消费,那么onTouchEvent将不被调用。
3、OnTouchListener的onTouch方法返回值如果是false,或者没有设置OnTouchListener,表示事件在此没有被消费,那么就会传递到onTouchEvent里。
5、View的onTouchEvent源码
/**
* View的onTouchEvent方法
*/
public boolean onTouchEvent(MotionEvent event) {
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
// 核心代码就是这里,up的时候,会执行点击事件的回调
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
}
break;
}
return true;
}
return false;
}
通过上述源码得到以下结论:
1、只有调View.onTouchEvent方法被调用了,在UP的时候才有可能会触发OnClickListener的onClick事件
2、onTouchListener > onTouchEvent > onClickListener
6、Demo验证结论
准备工作:
(1)自定义了一个MyLinearLayout,重写了dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
(2)同时也自定义了一个MyTextView,重写了dispatchTouchEvent和onTouchEvent(返回true)
// 默认情况下,点击MyTextView的日志如下:
I: ===MyLinearLayout===dispatchTouchEvent===0
I: ===MyLinearLayout===onInterceptTouchEvent===0
I: ===MyTextView===dispatchTouchEvent===0
I: ===MyTextView===onTouchEvent===0
I: ===MyLinearLayout===dispatchTouchEvent===2
I: ===MyLinearLayout===onInterceptTouchEvent===2
I: ===MyTextView===dispatchTouchEvent===2
I: ===MyTextView===onTouchEvent===2
I: ===MyLinearLayout===dispatchTouchEvent===2
I: ===MyLinearLayout===onInterceptTouchEvent===2
I: ===MyTextView===dispatchTouchEvent===2
I: ===MyTextView===onTouchEvent===2
I: ===MyLinearLayout===dispatchTouchEvent===1
I: ===MyLinearLayout===onInterceptTouchEvent===1
I: ===MyTextView===dispatchTouchEvent===1
I: ===MyTextView===onTouchEvent===1
// 把MyLinearLayout的onInterceptTouchEvent在down的时候返回true,
// onTouchEvent返回true的日志如下:
I: ===MyLinearLayout===dispatchTouchEvent===0
I: ===MyLinearLayout===onInterceptTouchEvent===0
I: ===MyLinearLayout===onTouchEvent===0
I: ===MyLinearLayout===dispatchTouchEvent===2
I: ===MyLinearLayout===onTouchEvent===2
I: ===MyLinearLayout===dispatchTouchEvent===2
I: ===MyLinearLayout===onTouchEvent===2
I: ===MyLinearLayout===dispatchTouchEvent===1
I: ===MyLinearLayout===onTouchEvent===1
// MyLinearLayout的onInterceptTouchEvent的返回false,onTouchEvent返回true,
// MyTextView的onTouchEvent返回false的日志如下:
I: ===MyLinearLayout===dispatchTouchEvent===0
I: ===MyLinearLayout===onInterceptTouchEvent===0
I: ===MyTextView===dispatchTouchEvent===0
I: ===MyTextView===onTouchEvent===0
I: ===MyLinearLayout===onTouchEvent===0
I: ===MyLinearLayout===dispatchTouchEvent===2
I: ===MyLinearLayout===onTouchEvent===2
I: ===MyLinearLayout===dispatchTouchEvent===2
I: ===MyLinearLayout===onTouchEvent===2
I: ===MyLinearLayout===dispatchTouchEvent===1
I: ===MyLinearLayout===onTouchEvent===1
// MyLinearLayout的onInterceptTouchEvent的返回false,onTouchEvent返回true,
// MyTextView的onTouchEvent返回true,并且给MyTextView设置
// onTouchListener(返回false)和onClickListener,日志如下:
I: ===MyLinearLayout===dispatchTouchEvent===0
I: ===MyLinearLayout===onInterceptTouchEvent===0
I: ===MyTextView===dispatchTouchEvent===0
I: ===MyTextView===OnTouchListener===0
I: ===MyTextView===onTouchEvent===0
I: ===MyLinearLayout===dispatchTouchEvent===2
I: ===MyLinearLayout===onInterceptTouchEvent===2
I: ===MyTextView===dispatchTouchEvent===2
I: ===MyTextView===OnTouchListener===2
I: ===MyTextView===onTouchEvent===2
I: ===MyLinearLayout===dispatchTouchEvent===2
I: ===MyLinearLayout===onInterceptTouchEvent===2
I: ===MyTextView===dispatchTouchEvent===2
I: ===MyTextView===OnTouchListener===2
I: ===MyTextView===onTouchEvent===2
I: ===MyLinearLayout===dispatchTouchEvent===1
I: ===MyLinearLayout===onInterceptTouchEvent===1
I: ===MyTextView===dispatchTouchEvent===1
I: ===MyTextView===OnTouchListener===1
I: ===MyTextView===onTouchEvent===1
// 发现并没有执行onClickListener,是因为虽然onTouchListener返回false
// onTouchEvent有被调用,但是被调用的是重写的MyTextView里的onTouchEvent,
// 要想onClickListener被执行,父类的super.onTouchEvent必须被调用才行。
// MyLinearLayout的onInterceptTouchEvent的返回false,onTouchEvent返回true,
// MyTextView的onTouchEvent返回super.onTouchEvent,并且给MyTextView设置
// onTouchListener(返回false)和onClickListener,日志如下:
I: ===MyLinearLayout===dispatchTouchEvent===0
I: ===MyLinearLayout===onInterceptTouchEvent===0
I: ===MyTextView===dispatchTouchEvent===0
I: ===MyTextView===OnTouchListener===0
I: ===MyTextView===onTouchEvent===0
I: ===MyLinearLayout===dispatchTouchEvent===2
I: ===MyLinearLayout===onInterceptTouchEvent===2
I: ===MyTextView===dispatchTouchEvent===2
I: ===MyTextView===OnTouchListener===2
I: ===MyTextView===onTouchEvent===2
I: ===MyLinearLayout===dispatchTouchEvent===2
I: ===MyLinearLayout===onInterceptTouchEvent===2
I: ===MyTextView===dispatchTouchEvent===2
I: ===MyTextView===OnTouchListener===2
I: ===MyTextView===onTouchEvent===2
I: ===MyLinearLayout===dispatchTouchEvent===1
I: ===MyLinearLayout===onInterceptTouchEvent===1
I: ===MyTextView===dispatchTouchEvent===1
I: ===MyTextView===OnTouchListener===1
I: ===MyTextView===onTouchEvent===1
I: ===MyTextView===OnClickListener=== // OnClickListener已经被执行
// MyLinearLayout的onInterceptTouchEvent的返回false,onTouchEvent返回true,
// MyTextView的onTouchEvent返回true,并且给MyTextView设置
// onTouchListener(返回true)和onClickListener,日志如下:
I: ===MyLinearLayout===dispatchTouchEvent===0
I: ===MyLinearLayout===onInterceptTouchEvent===0
I: ===MyTextView===dispatchTouchEvent===0
I: ===MyTextView===OnTouchListener===0
I: ===MyLinearLayout===dispatchTouchEvent===2
I: ===MyLinearLayout===onInterceptTouchEvent===2
I: ===MyTextView===dispatchTouchEvent===2
I: ===MyTextView===OnTouchListener===2
I: ===MyLinearLayout===dispatchTouchEvent===2
I: ===MyLinearLayout===onInterceptTouchEvent===2
I: ===MyTextView===dispatchTouchEvent===2
I: ===MyTextView===OnTouchListener===2
I: ===MyLinearLayout===dispatchTouchEvent===1
I: ===MyLinearLayout===onInterceptTouchEvent===1
I: ===MyTextView===dispatchTouchEvent===1
I: ===MyTextView===OnTouchListener===1 // 直接被onTouchListener消费掉,事件到达不了onTouchEvent
五、滑动冲突解决
1、常见的滑动冲突类型
同方向滑动冲突、不同方向滑动冲突、混合滑动冲突
2、外部拦截法解决滑动冲突
/**
* 重写外层控件的onInterceptTouchEvent
* 假设外层是垂直滚动的,内层有一个横向滚动的scrollview
* 那么当垂直方向滑动距离大于横向滑动距离时,我们就认为
* 用户的滑动意向是垂直方向,这个时候就拦截掉这个事件让自己(即外层的控件)去处理
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event){
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if(父布局需要滑动) // 根据冲突的不同情况自己判断
intercepted = true;
else
intercepted = false;
break;
default:
break;
}
mLastX = x; // 用于判断是否拦截的条件
mLastY = y; // 用于判断是否拦截的条件
return intercepted;
}
3、内部拦截法解决滑动冲突
/**
* 重写内层控件的dispatchTouchEvent
* 假设外层是垂直滚动的,内层有一个横向滚动的scrollview
*
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
int y = (int) ev.getY();
int x = (int) ev.getX();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 请求父容器下一个事件不要拦截
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要这个事件) {
// 请求父容器拦截掉此事件,不让要其传递到子View这
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
/**
* 重写外层即父容器的onInterceptTouchEvent
* 对比于ViewGroup分发的源码理解一下
* down的时候会调用ViewGroup的onInterceptTouchEvent,这个时候不拦截
* 事件就传递到内层View的dispatchTouchEvent里,然后requestDisallowInterceptTouchEvent(true);
* 设置状态位为true。当Move事件来到ViewGroup的dispatchTouchEvent时,不再调用onInterceptTouchEvent
* 方法,intercepted=false表示不拦截,那么事件继续传递到内层View的dispatchTouchEvent里,符合外层
* 的条件,这个时候,就requestDisallowInterceptTouchEvent(false)来要求父控件来拦截掉这个事件。接着当
* 下一个move事件到来时,会执行ViewGroup的onInterceptTouchEvent方法,此方法返回true,所以事件就此没
* 法往下传递了,相当于自己处理了。
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event){
if (event.getAction() == MotionEvent.ACTION_DOWN) {
return false;
}
return true;
}