3.1View基础知识
3.1什么是View
所以说,View是一种界面层的控件的一种抽象,它代表了一个控件。除了View,还有ViewGroup,从名字来看,它可以被翻译为控件组,言外之意是ViewGroup内部包含了许多个控件,即一组View。在Android的设计中ViewGroup也继承了View,这就意味着 View本身就可以是单个控件也可以是由多个控件组成的一组控件,通过这种关系就形成了View树的结构,这和Web前端中的DOM树的概念是相似的。根据 这个概念,我们知道,Button显然是个View,而LinearLayout不但是一个View而且还是一个ViewGroup,而ViewGroup内部是可以有子View的,这个子View同样还可以是ViewGroup,
3.2View的位置参数
View的位置主要由它的四个顶点来决定,分别对应于View的四个属性:top、left、right、bottom,其中top是左上角纵坐标,left是左上角横坐标,right是右 下角横坐标,bottom是右下角纵坐标。需要注意的是,这些坐标都是相对于View的父容器来说的,因此它是一种相对坐标,View的坐标和父容器的关系如图 3-2所示。
得到四个参数
- Left=getLeft();
- Right=getRight();
- Top=getTop;
- Bottom=getBottom()。
从Android3.0开始,View增加了额外的几个参数:x、y、translationX和translationY,其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标,并且translationX和translationY的默认值是0,和View的四个基本的位置参数一样,View也为它们提供了get/set方法,这几个参数的换算关系如下所示。
x=left+translationX
y=top+translationY
需要注意的是,View在平移的过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是x、y、translationX和translationY这四个参数。
translationX和translationY(在动画时的运动量偏移量)
3.3.1MotionEvent和TouchSlop
1,MotionEvent(触摸状态)
在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:
- ACTION_DOWN——手指刚接触屏幕;
- ACTION_MOVE——手指在屏幕上移动;
- ACTION_UP——手机从屏幕上松开的一瞬间。
正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,考虑如下几种情况:
点击屏幕后离开松开,事件序列为DOWN -> UP;
点击屏幕滑动一会再松开,事件序列为DOWN -> MOVE-> …> MOVE-> UP。
上述三种情况是典型的事件序列,同时通过MotionEvent对象我们可以得到点击事件发生的x和y坐标
2,TouchSlop
TouchSlop是系统所能识别出的被认为是滑动的最小距离,换句话说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。原因很简单:滑动的距离太短,系统不认为它是滑动。这是一个常量,和设备有关,在不同设备上这个值可能是不同的,通过如下方式即可获取这个常量:ViewConfiguration. get(getContext()).getScaledTouchSlop()。
3.4VelocityTracker、GestureDetector和Scroller
1. VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。它的使用过程很简单,首先,在View的onTouchEvent方法中追踪当前单击事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
创建对象,用于跟踪手指在屏幕上的速度,然后绑定触摸事件对象,这个并不是UI视图,要得到event需要调用setOnTouchListener
方法在他的参数中即可得到event对象,然后计算当前速度,参数代表1000毫秒,下面的是参数是X或者Y方向的速度,他是手指在X或者Y的速度,单位是像素/秒
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// 在这里处理触摸事件
int action = event.getAction(); // 获取触摸动作类型
return true;
}
});
上面是以坐标系正方向滑动的,如果逆向,速度为负数,上面的1000毫秒如果变成100毫秒,那么速度单位就是像素/100毫秒
如果不需要了,那么就使用clear方法重置然后回收内存
velocityTracker.clear();
velocityTracker.recycle();
手势检查,用于辅助检测用户的单击、滑动、长按、双击等行为
首先,创建个GestureDetector对象并实现OnGestureListener接口
GestureDetector mGestureDetector = new GestureDetector(this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
然后接管目标View的onTouchEvent方法,在待监听View的onTouchEvent方法中添加如下实现:
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
然后就可以实现OnGestureListener和OnDoubleTapListener中的方法了
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化 GestureDetector 对象
mGestureDetector = new GestureDetector(this, this);
mGestureDetector.setOnDoubleTapListener(this);
// 获取 TextView 对象
textView = findViewById(R.id.textView);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 在待监听 View 的 onTouchEvent 方法中调用 GestureDetector 的 onTouchEvent 方法
boolean consume = mGestureDetector.onTouchEvent(event);
return consume || super.onTouchEvent(event);
}
// 实现 OnGestureListener 中的方法
@Override
public boolean onDown(MotionEvent e) {
textView.setText("onDown");
return true;
}
这里的 boolean consume = mGestureDetector.onTouchEvent(event);
该方法用于将触摸事件传递给 GestureDetector
对象进行处理,以便进行手势检测。
3.Scroller
当使用View的scrollTo/scrollBy方法来进行滑动时,其过程是瞬间完成的,这个没有过渡效果的滑动用户体验不好。这个时候就可以使用Scroller来实现有过渡效果的滑动,其过程不是瞬间完成的,而是在一定的时间间隔内完成的。Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能
//创建Srcoller对象
Scroller scroller=new Scroller(mContext);
// 缓慢滚动到指定位置,destX 和 destY 分别表示滚动到的目标位置的横纵坐标
private void smoothScrollTo(int destX,int destY){
//获取当前视图的水平滚动位置。
int scrollX=getScrollX();
//计算目标位置与当前位置之间的偏移量。
int delta=destX-scrollX;
// 1000ms内滑向destX,效果就是慢慢滑动,花费一秒,由初坐标滑动到
mScroller.startScroll(scrollX,0,delta,0,1000);
//方法请求重绘,以便在下一次绘制帧时更新视图
invalidate();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
3.2View的滑动
三种滑动方法,scrollTo/scrollBy,动画,改变布局参数
3.2.1 ,使用scrollTo/scrollBy
View提供了专门的方法来实现,就是scrollTo/scrollBy俩个方法
public void scrollTo(int x,int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX,mScrollY,oldX,oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
public void scrollBy(int x,int y) {
scrollTo(mScrollX + x,mScrollY + y);
}
scrollTo是将视图滚动到指定位置,X,Y分别表示要滚到的位置
scrollBy是将视图像某个方向滚动,X,Y分别代表水平和垂直方向上的偏移量,可以看到在scrollBy也是调用了scrollTo这个方法。
这里的scrollTo中首先判断当前位置是否与目标位置相同,如果不是,就保存当前的位置,然后设置滚动位置为目标位置,然后调用onScrollChanged
,来滚动发生变化时通知视图发生变化,然后判断用于检查滚动条是否需要唤醒,用于在下一帧动画之前使视图无效,以便进行重新绘制。
这里的滚动是将View的内容移动,并不移动View本身的位置,因此,如果视图的内容部分移出了当前可见区域,但视图本身仍然位于屏幕内,那么移出当前视图的区域表现通常是视图内容被裁剪,只有部分内容能够显示在屏幕上。
3.2.2, 使用动画
使用动画来移动View,主要是操作View的translationX和translationY属性 动画这一部分内容后面还会提到这里只是单纯的介绍了动画移动
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"//这个属性指定动画完成后,是否保留动画的最终状态。
android:zAdjustment="normal" >//这个属性用于指定动画的 Z 轴方向上的调整方式
<translate
android:duration="100"//持续时间
android:fromXDelta="0"
android:fromYDelta="0"//俩个值代表X,Y方向的偏移量
android:interpolator="@android:anim/linear_interpolator"//指定了动画的插值器,这里使用了 Android 提供的线性插值器,表示动画的变化速度是匀速的。
android:toXDelta="100"
android:toYDelta="100" />//指定了动画的结束位置,这里的值表示 X 和 Y 轴方向的结束偏移量,都设置为 100 表示在 X 和 Y 轴方向上各向右下平移 100 个像素。
</set>//这是一个动画集合的根元素,用于定义一组动画效果。
如果需要使用它,需要将其放到 res/anim
目录下,然后是调用
// 加载动画资源
Animation animation = AnimationUtils.loadAnimation(context, R.anim.your_animation_file);
// 应用动画到视图
view.startAnimation(animation);
View动画并不能真正改变View的位置,View动画将一个Button向右移动100px,并且这个 View设置的有单击事件,然后你会惊奇地发现,单击新位置无法触发onClick事件,而单击原始位置仍然可以触发onClick事件,尽管Button已经不在原始位置 了。因为不管Button怎么做变换,但是它的位置信息(四个顶点和宽/高)并不会随着动画而改变,因此在系统眼里,这个Button并没有发生任何改变,它的真身仍然在原始位置。
属性动画
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration
(100).start();
这里的targetView是要移动的view,然后是translationX,是指定他是X方向平移,然后0,100,是初始位置和最后位置,单位是像素,100是花费时间100毫秒
3.2.3改变布局参数
这个比较好理解了,比如我们想把一个Button向右平移100px,我们只需要将这个Button的LayoutParams里marginLeft参数的值增加100px即可,是不是很简单呢?还有一种情形,为了达到移动Button的目的,我们可以在 Button的左边放置一个空的View,这个空View的默认宽度为0,当我们需要向右移动Button时,只需要重新设置空View的宽度即可,当空View的宽度增大时假设Button的父容器是水平方向的LinearLayout),Button就自动被挤向右边,即实现了向右平移的效果。
MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mButton1.requestLayout();
//或者mButton1.setLayoutParams(params);
在这里首先获取了button的布局参数,转换了他的类型,然后将button的宽和左边距改变,最后在button布局参数改变时,调用requestLayout来重新布局
3.2.4各种滑动方式的对比
- scrollTo/scrollBy:操作简单,适合对View内容的滑动;
- 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;
- 改变布局参数:操作稍微复杂,适用于有交互的View。
3.3弹性滑动
弹性滑动是面元素会在到达边界或者用户停止滑动时,会出现一种弹性效果。这种效果通常用于模拟物理弹簧的行为
。他的实现核心思想是将一次大的滑动分成若干次小的滑动并在一个时间段完成
3.3.1使用Scroller
这个在上面有简单提到过,
//创建Srcoller对象
Scroller mscroller=new Scroller(mContext);
// 缓慢滚动到指定位置,destX 和 destY 分别表示滚动到的目标位置的横纵坐标
private void smoothScrollTo(int destX,int destY){
//获取当前视图的水平滚动位置。
int scrollX=getScrollX();
//计算目标位置与当前位置之间的偏移量。
int delta=destX-scrollX;
// 1000ms内滑向destX,效果就是慢慢滑动,花费一秒,由初坐标滑动到
mScroller.startScroll(scrollX,0,delta,0,1000);
//方法请求重绘,以便在下一次绘制帧时更新视图
invalidate();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
这个是之前的Scroller使用,在这里它调用了startScroll方法,在这个方法中它什么也没有做,只是保存了传递的几个参数,
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;
}
在这里,他保存了各个参数,参数中的duration是指滑动时间,所以可以看到在这个方法中是无法使其滑动的,在上面使用的代码中有一个invalidate();
这个方法会使View重绘,然后在重绘中,他会调用draw方法,在这个方法中,会调用 computeScroll()
,这个方法是需要我们去实现的,它会像Scroller获取当前的srollX和srollY,然后通过scrollTo滑动,然后再次重绘,再次重复上面的过程,那么它如何停止,就是if判断中的computeScrollOffset()
方法,
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;
}
首先计算了,开始滚动到现在的时间,然后判断她是否超过预定时间,然后调整滚动进度,返回true,表示滚动还在继续
所以Scroller的工作原理就是:首先他自己不能实现View的滑动,然后它将要记录的值记录,配合View的computeScroll方法,让View不停的重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成View的滑动。就这样,View的每一次重绘都会导致View进行小幅度的滑动,
而多次的小幅度滑动就组成了弹性滑动。
3.3.2 使用动画实现弹性滑动
这里的内容和3.2.2一样
3.3.3使用延时策略
前面讲过弹性滑动的核心就是将一个滑动分为多个小的滑动,这里的延时策略,是指通过一系列的延时消息从而达到一种渐进式的效果,可以使用Handler或者View的postDelayed方法,也可以使用线程的sleep方法,对与sleep,就是通过在while循环中不停的View和sleep来实现一个弹性滑动的效果
private static final int MESSAGE_SCROLL_TO = 1;//是否执行滚动动画
private static final int FRAME_COUNT = 30;//整个动画的帧数
private static final int DELAYED_TIME = 33;//动画的时间
private int mCount = 0;
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_SCROLL_TO: {
mCount++;
if (mCount <= FRAME_COUNT) {
float fraction = mCount / (float) FRAME_COUNT;//确定当前帧数
int scrollX = (int) (fraction * 100);//计算X方向滚动距离
mButton1.scrollTo(scrollX,0);//滚动
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,
DELAYED_TIME);
}
break;
}
default:
break;
}
;
};
3.4 View的事件分发机制
在前面基础知识中有一个MotionEvent,他代表运动事件,事件分发机制就是对运动事件的事件分发,当一个MotionEvent产生后,系统需要将其传递给一个具体的View,这个传递过程就是分发机制
3.4.1 点击事件的分发机制
点击事件的分发有三个方法,dispatchTouchEvent,onInterceptTouchEvent 和onTouchEvent ,
首先就是**public boolean
dispatchTouchEvent(
MotionEvent event
**)
如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent 方法的影响,表示是否消耗当前事件。
(在这个方法内进行判断这个事件是否在这个对象处理,如果不是传递给子元素)
然后是**public boolean onInterceptTouchEvent(MotionEvent event)
**
在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
(在这里进行上面说的判断的过程)
最后是**public boolean onTouchEvent(MotionEvent event)
**
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
(在这里判断在当前对象处理这个事件,开始处理)
他们三个的关系,如下面的伪代码
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
对于一个ViewGroup,点击事件产生会传递给他,它调用dispatchTouchEvent ,在这个方法中通过onInterceptTouchEvent方法来判断,她是否要拦截这个事件,如果返回true,那么这个事件就交给这个ViewGroup处理,调用他的onTouchEvent,如果他不拦截,那么将其传递给他的子元素,他的子元素在重复这个过程直到被处理
当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调。这时事件如何处理还要看onTouch的返回 值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法将不会被调用。由此可见,给View设置的 OnTouchListener,其优先级比onTouchEvent要高。在onTouchEvent方法中,如果当前设置的有OnClickListener,那么它的onClick方法会被调用。
当一个点击事件产生后,它的传递过程遵循如下顺序:Activity -> Window -> View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View。顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器 的onTouchEvent将会被调用,依此类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent方法会被调用。 如果底部的viewd的onTouchEvent返回false即无法处理,那么这个事件就会上传到上层事件,然后它就有可能会一层一层上传
事件传递机制的结论
- 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。
- 正常情况下,一个事件序列只能被一个View拦截且消耗。
- 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理
- 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
- ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouch-Event方法默认返回false。
- View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
- View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable 和longClickable同时为false)
- View的enable属性不影响onTouchEvent的默认返回值。
- onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。
- 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外
3.4.2事件分发的源码
1,Activity对点击事件的分发过程
点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件派发,具体的工作是由Activity内部的Window来完成的。
Window会将事件传递给decor view
,decor view
一般就是当前界面的底层容器(即setContentView所设置的View的父容器),通过Activity.getWindow.getDecorView()可以获得。
首先是Activity的dispatchTouchEvent源码分析
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
在这里首先将事件分发给Window进行处理,如果它处理了,那么直接返回true,事件分发结束,否则将其分发给Activity的onTouchEvent
再观察Window如何将事件分发给ViewGroup,Window是一个抽象类,他的superDispatchTouchEvent也是一个抽象方法,Window的唯一实现类是,PhoneWindow ,在PhoneWindow中点击事件的处理是
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
他将事件传递给了DecorView,DecorView是窗口装饰视图,DecorView 是整个界面的顶级视图,它位于视图层级的最上层。因此,所有的 UI 元素都是以 DecorView 为根视图进行嵌套和布局的。
private DecorView mDecor;
@Override
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}
这里传递到了DecorVeiw,也就是说,下一步传递就到了顶级View,
通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)这种方式就可以获取Activity所设置的View
在这里事件一步一步从Activty传递到了Window,然后又传递到了顶层View
2, 顶层View对点击事件的分发过程
点击事件在View的分发前面在3.4.1已经介绍过了这里主要进行部分源码的学习
首先观察ViewGroup对点击事件的分发过程
final boolean intercepted;//表示是否拦截了事件
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_ INTERCEPT) != 0;
if (!disallowIntercept) {
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;
}
俩种情况会拦截ACTION_DOWN或者mFirstTouchTarget != null。
ACTION_DOWN代表表示用户的手指刚刚按下,触发了一个 ACTION_DOWN 事件。这通常是触摸事件序列的开始
,因此在这种情况下可能需要拦截触摸事件,以便 ViewGroup 可以处理后续的事件序列。
mFirstTouchTarget != null,表示存在第一个触摸目标(TouchTarget),即在 ViewGroup 中已经有一个子视图正在处理触摸事件。
在这种情况下,可能需要拦截触摸事件,以便 ViewGroup 自己处理触摸事件,而不是交给子视图处理。
如果没有拦截就调用 onInterceptTouchEvent(ev) 方法来判断是否拦截当前的触摸事件 ev。如果 onInterceptTouchEvent(ev) 返回 true,则表示拦截了触摸事件,将 intercepted 设置为 true。
调用 onInterceptTouchEvent(ev) 方法来判断是否拦截当前的触摸事件 ev。如果 onInterceptTouchEvent(ev) 返回 true,则表示拦截了触摸事件,将 intercepted 设置为 true。
ev.setAction(action);在拦截触摸事件后,将事件的动作类型恢复为原始的动作类型
这里有一个FLAG_DISALLOW_INTERCEPT标记位这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用
于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件。为什么说是除了
ACTION_DOWN以外的其他事件呢?这是因为ViewGroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,将导致子View中设置的这个标记位无效。
我们可以得出结论:当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的
onInterceptTouchEvent方法,
FLAG_DISALLOW_INTERCEPT这个标志的作用是让ViewGroup不再拦截事件,当然前提是
ViewGroup不拦截ACTION_DOWN事件
子View的分发过程
final View[] children = mChildren;
for (int i = childrenCount -1; i => 0; i--) {
//根据是否启用自定义绘制顺序(customOrder),确定当前子视图的索引位置。
final int childIndex = customOrder? getChildDrawingOrder(childrenCount,i) : i;
final View child = (preorderedList == null)? children[childIndex] : preorderedList.get(childIndex);
//前面检查是否可以接收事件,后面判断触摸点是否在子视图可触摸范围内
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x,y,child,null))
{
continue;
}
newTouchTarget = getTouchTarget(child);//获取子视图当前事件
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;//如果有那更新指针id
break;
}
//
resetCancelNextUpFlag(child);
//调用 dispatchTransformedTouchEvent 方法将转换后的触摸事件分发给当前子视图,并检查是否成功分发。
if (dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign)) {
//记录触摸事件按下时间
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;//记录触摸事件按下时所处的子视图索引位置。
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child,idBitsToAssign);//为当前子视图创建触摸事件目标,并将其添加到触摸事件目标链表中。
alreadyDispatchedToNewTouchTarget = true;
//标记已经将触摸事件分发给新的触摸事件目标。
break;
}
}
首先遍历ViewGroup的所有子元素,然后判断子元素是否能够接收到点击事件。是否能够接收点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。
在调用dispatchTransformedTouchEvent
实际上是调用子元素的dispatchTouchEvent方法,他的内部判断是否为空,不是则交给子元素处理
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
如果子元素的dispatchTouchEvent返回true,这时我们暂时不用考虑事件在子元素内部是怎么分发的,那么mFirstTouchTarget就会被赋值同时跳出for循环,
其实mFirstTouchTarget真正的赋值过程是在addTouchTarget内部完成的,从下面的addTouchTarget方法的内部结构可以看出,mFirstTouchTarget其实是一种单 链表结构。mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一序列中所有的点击事件,这一点在前面已经做了分析
private TouchTarget addTouchTarget(View child,int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child,pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果遍历所有的子元素后事件都没有被合适地处理,这包含两种情况:第一种是ViewGroup没有子元素;第二种是子元素处理了点击事件,但是在 dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false。在这两种情况下,ViewGroup会自己处理点击事件
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS);
}
它会调用super.dispatchTouchEvent(event),很显然,这里就转到了View的dispatchTouchEvent方法,即点击事件开始交由View来处理,
3.View对点击事件的处理
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this,event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
因为View(这里不包含ViewGroup)是一个单独的元素,它没有子元素因此无法向下传递事件,所以它只能
自己处理事件。从上面的源码可以看出View对点击事件的处理过程,首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent,这样做的好处是方便在外界处理点击事件。
再分析onTouchEvent的实现,当View处于不可用状态下点击事件照样会消耗点击事件,尽管它看起来不可用。接着,如果View设置有代理,那么还会执行TouchDelegate的onTouchEvent方法,这个onTouchEvent的工作机制看起来和OnTouchListener类似,
3.5View的滑动冲突
3.5.1 常见的滑动冲突
- 场景1——外部滑动方向和内部滑动方向不一致;
- 场景2——外部滑动方向和内部滑动方向一致;
- 场景3——上面两种情况的嵌套。
3.5.2 滑动冲突的处理规则
1.第一种冲突的处理规则
当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部View拦截点击事 件。这个时候我们就可以根据它们的特征来解决滑动冲突。
具体来说是:根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件,
这里水平还是竖直的判断方式有多种,判断夹角,判断水平和竖直距离大小,
2.第二种冲突的处理规则
无法根据角度等判断,一般在业务上找到突破点。(无明确方法,根据需求不同来处理)
3.第三种冲突的处理规则
和第二个一样
3.5.3 滑动冲突的解决方式
场景一的滑动冲突处理
1,外部拦截法
外部拦截法是指点击事情都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,伪代码如下
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: {
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;
}
在这里核心就是修改父容器拦截这个事件的调节,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了; 其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false;最后是ACTION_UP事件,这里必须要返回false,因为ACTION_UP事件本身没有太多意义。
考虑一种情况,假设事件交由子元素处理,如果父容器在ACTION_UP时返回了true,就会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而ACTION_UP作为最后一个事件也必定可以传递给父容器,即便父容器的onInterceptTouchEvent方法在ACTION_UP时返回了false。
没看懂
2,内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和 Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法稍显复杂。它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x -mLastX;
int deltaY = y -mLastY;
if (父容器需要此类点击事件)) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
这里是在子元素的方法中进行了判断父布局是否拦截事件,可以这么理解,首先在将所有事件除了DOWN,其他都在父布局就进行拦截,然后在继续传递到子元素,如果子元素需要那么就调用parent.requestDisallowInterceptTouchEvent(true);将其确定到子元素,如果子元素判断这个事件应该在父布局进行处理就返回parent.requestDisallowInterceptTouchEvent(false);确定在父布局进行拦截
这里的rent.requestDisallowInterceptTouchEvent(false); 就是传递给父布局是否拦截这个事件,true 不拦截,false拦截
而父布局的onInterceptTouchEvent方法如下
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
可以看到它除了Down事件,将其他方法都默认拦截
事例:制造滑动冲突处理
为了实现ViewPager的效果,我们定义了一个类似于水平的LinearLayout的东西,只不过它可以水平滑动,初始化时我们在它的内部添加若干个ListView, 这样一来,由于它内部的Listview可以竖直滑动。而它本身又可以水平滑动,因此一个典型的滑动冲突场景就出现了,并且这种冲突属于类型1的冲突。根据滑动策略,我们可以选择水平和竖直的滑动距离差来解决滑动冲突。
首先是Activity的处理
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private HorizontalScrollViewEx mListContainer;
private DisplayMetrics displayMetrics;//用来获得屏幕尺寸的
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG,"onCreate");
initView();
}
private void initView() {
displayMetrics = new DisplayMetrics();
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
LayoutInflater inflater = getLayoutInflater();//获取xml加载器--加载到该Activity中
mListContainer = findViewById(R.id.mListContainer);
int screenWidth = 0;//屏幕宽度
int screenHeight = 0;//屏幕高度
if(windowManager != null){
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
screenWidth = displayMetrics.widthPixels;
screenHeight = displayMetrics.heightPixels;
}
for (int i = 0; i < 3; i++) {
ViewGroup layout = (ViewGroup) inflater.inflate(
R.layout.content_layout,mListContainer,false);
layout.getLayoutParams().width = screenWidth;
TextView textView = (TextView) layout.findViewById(R.id.mTitle);
textView.setText("page " + (i + 1));
layout.setBackgroundColor(Color.rgb(255/(i+1),255 / (i + 1),0));
createList(layout);
mListContainer.addView(layout);
}
}
private void createList(ViewGroup layout) {
ListView listView = (ListView) layout.findViewById(R.id.mList);
ArrayList<String> datas = new ArrayList<String>();
// 向datas列表中添加50个项目
for (int i = 0; i < 50; i++) {
datas.add("name " + i);
}
// 创建并设置适配器
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
R.layout.item,datas);
listView.setAdapter(adapter);
}
}
在这里我的xml文件里面是一个HorizontalScrollViewEx,将其作为父布局,然后通过WindowManager获取屏幕宽度和高度 ,
在加载三个布局到父布局中,这个过程中使用了ViewGroup,他的xml文件是另外一个文件,在那个文件中只有一个textview和一个ListVIew设置了他的宽度,在设置他的详细信息,创建适配器等,
可以看到在这里我们设置了多个listView做为里面竖向滑动的布局,然后在外面是一个我们设置的HorizontalScrollViewEx,下面是HorizontalScrollViewEx的具体实现
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:
intercepted = false;
if(!mScroller.isFinished()){
mScroller.abortAnimation();
intercepted = true; //如果还没有完成滚动就要进行拦截
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;//获取滑动差值
int deltaY = y - mLastYIntercept;
if(Math.abs(deltaX) > Math.abs(deltaY)){
//如果横向滑动距离大于纵向滑动距离-即判定为水平滑动的话-那么由父布局拦截处理了
intercepted = true;
}else{
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;//一次完整的事件序列结束后,重置状态
break;
default:
break;
}
//更新状态
mLastX = x;
mLastY = y;
mLastYIntercept = y;
mLastXIntercept = x;
return intercepted;
}
这个是父布局中的onInterceptTouchEvent方法,在这里我们可以很明显的看到他对横向滑动距离和纵向滑动距离进行了比较判断滑动方向,而mScroller.abortAnimation();是为了优化滑动体验
下面是他的代码
public class HorizontalScrollViewEx extends LinearLayout {
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
private int mLastX = 0;
private int mLastY = 0;
//子元素的相关信息
private int mChildrenSize = 3;
private int mChildWidth;
private int mChildIndex;
private Scroller mScroller;//弹性滚动对象-仅能滚动内容
private VelocityTracker mVelocityTracker;
public HorizontalScrollViewEx(Context context) {
super(context);
init();
}
public HorizontalScrollViewEx(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public HorizontalScrollViewEx(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public HorizontalScrollViewEx(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void setInfo(int mChildrenSize,int mChildWidth){
this.mChildWidth = mChildWidth;
this.mChildrenSize = mChildrenSize;
}
private void init(){//初始化速度追踪器和弹性滚动对象
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
//先是外部拦截法
@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:
intercepted = false;
if(!mScroller.isFinished()){
mScroller.abortAnimation();
intercepted = true; //如果还没有完成滚动就要进行拦截
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;//获取滑动差值
int deltaY = y - mLastYIntercept;
if(Math.abs(deltaX) > Math.abs(deltaY)){
//如果横向滑动距离大于纵向滑动距离-即判定为水平滑动的话-那么由父布局拦截处理了
intercepted = true;
}else{
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;//一次完整的事件序列结束后,重置状态
break;
default:
break;
}
//更新状态
mLastX = x;
mLastY = y;
mLastYIntercept = y;
mLastXIntercept = x;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:{
if(!mScroller.isFinished()){
mScroller.abortAnimation();//停止滚动动画
}
break;
}
case MotionEvent.ACTION_MOVE:{
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX,0);
break;
}
case MotionEvent.ACTION_UP:{
int scrollX = getScrollX();
int scrollToChildIndex = scrollX / mChildWidth;
mVelocityTracker.computeCurrentVelocity(1000);
float velocityX = mVelocityTracker.getXVelocity();//获取横向滑动速度
if(Math.abs(velocityX) >= 50){
mChildIndex = velocityX > 0 ? mChildIndex - 1:mChildIndex + 1;
}else{
mChildIndex = (scrollX + mChildWidth/2) / mChildWidth;
}
mChildIndex = Math.max(0,Math.min(mChildIndex,mChildrenSize-1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx,0);
mVelocityTracker.clear();
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
private void smoothScrollBy(int dx,int dy){
mScroller.startScroll(getScrollX(),0,dx,0,1000);
invalidate();
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
}
可以看到主要是完成了滑动方向的判断,其他大部分继承了父类
这里是外部拦截法,即在父类进行了拦截
下面如果想使用内部拦截法
public class ListViewEx extends ListView {
private static final String TAG = "ListViewEx";
private HorizontalScrollViewEx2 mHorizontalScrollViewEx2;
// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
…
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent
(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x -mLastX;
int deltaY = y -mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
mHorizontalScrollViewEx2.requestDisallowInterceptTouch
Event(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
}
然后是他的父类HorizontalScrollViewEx2的onInterceptTouchEvent方法
public boolean onInterceptTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
mLastX = x;
mLastY = y;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
} else {
return true;
}
}
至于第二第第三,根据不同的业务写不同的判断条件即可
标签:认识,int,了解,事件,ACTION,滑动,event,View From: https://blog.csdn.net/m0_73986294/article/details/142055254