一、View基础
1、view
- view的位置主要由四个顶点来决定,top、left、right、bottom,这些坐标都是相对于view的父容器,是一种相对坐标。
- view的另外几个参数:x、y、translationX、translationY,其中x和y是View左上角的坐标,而translationX、translationY是view左上角相对于父容器的偏移量。
x = left + translationX
y = top + translationY - view在平移的过程中,top和left表示的是原始左上角的位置信息,值不会发生变化,发生变化的是x、y、translationX、translationY这四个参数。
2、MotionEvent
- 三个事件:ACTION_DOWN、ACTION_MOVE、ACTION_UP
- 点击事件坐标:getX/getY()返回当前view左上角的坐标,getRawX/getRawY返回相对于屏幕左上角坐标
- TouchSlop:系统识别被认为的最小滑动距离。获取这个常量方法:ViewConfiguration.get(getContext()).getScaledTouchSlop()
3、VelocityTracker
- 含义:速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。
- 用法:
①在view的onTouchEvent方法中追踪当前单击事件的速度
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
②获取当前速度
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
在获取速度之前需要调用computeCurrentVelocity来计算速度,当前的速度是这1000ms手指划过的像素数,此时计算速度:速度=(终点位置 - 起点位置)/时间段,其中速度可以为负,比如是从右向左滑动
③最后不需要使用时需要回收
velocityTracker.clear();
velocityTracker.recycle();
4、GestureDetector
- 含义:手势检测,用于辅助检测用户的单机、滑动、长按、双击等行为
- 用法:
①新建一个类继承OnGestureListener,OnDoubleTapListener是双击,选择性实现
public static class MyGestureListener implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener
②创建一个GestureDetector对象,GestureDetector有三个构造函数
- public GestureDetector(Context context, OnGestureListener listener)
- public GestureDetector(Context context, OnGestureListener listener, Handler handler)
- public GestureDetector(Context context, OnGestureListener listener, Handler handler, boolean unused)
GestureDetector mDetector=new GestureDetector(getContext(),new MyGestureListener());
//解决长按屏幕后无法拖动的现象
mDetector.setIsLongpressEnabled(false);
③接管目标view的onTouchEvent方法,在待监听的View的onTouchEvent方法中添加实现
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
5、Scroller
- 含义:弹性滑动对象,用于实现view的弹性滑动
- 用法:代码比较固定
Scroller scroller = new Scroller(mContext);
//缓慢滚动到指定位置
private void smoothScrollTo(int destX, int destY){
int scrollX = getScroller();
int deltaX = destX - scrollX;
//1000ms内滑向destX,效果就是慢慢滑动
scroller.startScroll(scrollX, 0, deltaX, 0, 1000);
invalidate();
}
@override
public void computeScroll(){
if (scroller.computeScrollOffset()){
scrollTo(mScrollrt.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
原理:
在startScroll中记录开始时间,mDuration 是整个滑动的周期,然后调用invalidate方法,这个方法会导致view重绘,继而调用view的draw方法,而draw方法会调用computeScroll方法(这个方法在view中是一个空实现),上面已经实现了computeScroll,然后继续看computeScrollOffset方法,这个方法里面记录了相应时间里面滑动了的距离,用scrollTo滑动到相应位置然后调用postInvalidate方法继续调用draw方法循环重绘直到滑动周期结束。
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
//记录开始时间
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
public boolean computeScrollOffset() {
...
//当前时间减去滑动开始时间,得到已经过去的时间
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;
...
}
}
...
return true;
}
二、View的滑动
1、使用scrollTo/scrollBy
scrollTo(int x, int y) —> x,y是view即将滑动的目标位置
scrollBy(int x, int y) —> x,y是view基于横向、竖向的滑动距离
滑动的是view内容的位置,而不能改变view在布局中的位置
2、使用动画
①view动画
②属性动画
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
3、改变布局参数
比如view1要向右移动100px,在这个view左边放一个0px的view2,此时设置view2宽度100px,让view2把view1挤到右边完成滑动
总结:
scrollTo/scrollBy:操作简单,适合对view内容的滑动
使用动画:操作简单,适合没有交互的view和实现复杂的动画效果
改变布局参数:操作稍微复杂,适用于又交互的view
4、弹性滑动
- 使用Scroller
- 使用动画
- 使用延时策略,可以运用延时策略百分比分割滑动过程分次平滑的滑动
三、事件分发
1、三个方法
boolan dispatchTouchEvent:来进行事件的分发,如果事件能够传递给这个view则一定会被调用,返回结果受当前view的onTouchEvent方法和下级的dispatchTouchEvent方法的影响
boolean onInterceptTouchEvent:在dispatchTouchEvent方法中调用,判断是否拦截某个事件
boolean onTouchEvent:在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,当前view不会再收到这个事件的后续move、up等事件
总结:他们的关系view(ViewGroup):
//事件传递到这个view,调用dispatchTouchEvent方法判断是否拦截消费
public boolean dispatchTouchEvent(MotionEvent ev) {
//如果这个view不消费
boolean consume = false;
//判断这个view是否拦截
if(onInterceptTouchEvent(ev)) {
//这个view拦截,调用这个view的onTouchEvent方法
consume = onTouchEvent(ev);
} else {
//这个view拦截,当前事件传递给子view
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
2、总体分发逻辑
事件的源头在activity的dispatchTouchEvent方法,会接着传递到phoneWindow、decorView,这个decorview是顶层ViewGroup,此时层层向下分发,判断Viewgroup是否拦截事件消费事件,如果不拦截不消费则发给他的子view。
- 整个分发过程中没有拦截和消费,down事件层层下发,并层层向上返回false,move和up事件会交给actiivity的onTouchEvent处理
- 整个分发过程中没有拦截但是有消费,down事件层层下发,并层层向上返回false,知道有消费返回true,move和up事件就交给这个消费事件的View处理。
- 整个分发过程中有拦截有消费,down事件层层下发,直到有拦截后,交给消费的view处理
- 分发过程中不拦截down事件,但拦截move事件并消费,之前的收到down事件的子view会收到cancel事件,后续的move和up会层层下发给拦截move事件的view
- 分发过程中不拦截down事件,但拦截move事件且不消费,这个down事件被cancel,后续的move等事件交给actvity的OnTouchEvent处理
四、滑动冲突
1、出现滑动冲突的场景
- 外部滑动和内部滑动方向不一致
- 外部滑动和内部滑动方向一致
- 上面两种情况的嵌套
2、外部拦截法
含义:点击事件经过父容器的拦截处理,判断父容器是否拦截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//ACTION_DOWN事件父容器不可以拦截
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
//根据需求拦截
if(父容器需要相应的事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
3、内部拦截法
含义:父容器的不拦截任何事件,所有的事件都传给子元素,如果子元素需要这个事件就自己处理,否则交给父容器处理
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if("父容器需要相应的事件") {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}