1.view的基础知识
1.1.view的解释:
View是android中所有控件的基类,也可以说是界面层所有控件的一个抽象,ViewGroup也是继承自View的。
1.2.view的参数位置
View的位置主要是由四个顶点来决定的:left,top,right,bottom,left是左上角的横坐标,top是左上角的纵坐标,right是右下角的横坐标,bottom是右下角的纵坐标。注意:这些坐标都是相对坐标来着,都是相对父容器的坐标。准确来讲,上面的描述是不正确的,因为如果view发生了平移或竖移的话left/top/right/bottom这四个值是不会发生变化的,发生变化的是我们下面将要提到的X/translationX/Y/translationY。因此left,top准确的描述应该是view的原始位置的左/上边界距离其父容器的左/上边界的距离。坐标系和父容器的关系图:
关于这四个坐标的值的获取在view的源码中有详细的定义:
mLeft = getLeft()
mTop = getTop()
mRight = getRight()
mBottom = getBottom()
Android3.0开始对View又增加了几个额外的参数,x、y、translationX、translationY,xy是View左上角的坐标,translationX和translationY是View左上角相对于父容器的偏移量,这些也都是相对于父容器的坐标和偏移量。left、x、translationX/top、y、translationY之间的关系如下,其实在view的源码中就可以得到。
public float getX() {
return mLeft + getTranslationX();
}
public float getY() {
return mTop + getTranslationY();
}
1.3.motionevent和touchslop
1.3.1motionEvent
是手指接触屏幕之后所产生的一系列事件:ACTION_DOWN--ACTION_MOVE--ACTION_UP。这三个事件一般会在自定义view或者处理滑动事件的时候用到,主要是处理点击事件的x,y坐标来实现需求。系统提供了两组获取xy坐标的方法:getX()/getY()和getRawX()/getRawY(),它们有什么区别呢:getX()/getY()放回的是相对于当前View的左上角的xy坐标,而getRawX()/getRawY()返回的是相对于手机屏幕左上角的xy坐标。直接上图最好理解:
1.3.2touchSlop:
是系统所能识的手指滑动的最小距离,和设备有关,touchSlop一般是用来判断某个手势是否有滑动,滑动距离太小的的话就判断它没有滑动。
1.4.VelocityTracker,手势GestureDetector和滑动Scroller
1.4.1VelocityTracker:
速度追踪,用于追踪手指在滑动中的速度,使用方法如下首先在onTouchEvent中初始化,接着在需要使用的地方先计算,再获取速度即可,最后使用完毕需要重置和回收。
1.4.2GestureDetector
手势检测,用于检测用户的单击,滑动,长按,双击等行为,但是如果仅仅是监听滑动相关的话完全可以在onTouchEvent回调中自己实现,GestureDetector反而显得略繁琐。使用过程如下:
public class MainActivity extends Activity implements OnTouchListener{
private GestureDetector mGestureDetector;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mGestureDetector = new GestureDetector(new gestureListener()); //使用派生自OnGestureListener
TextView tv = (TextView)findViewById(R.id.tv);
tv.setOnTouchListener(this);
tv.setFocusable(true);
tv.setClickable(true);
tv.setLongClickable(true);
}
/*
* 在onTouch()方法中,我们调用GestureDetector的onTouchEvent()方法,将捕捉到的MotionEvent交给GestureDetector
* 来分析是否有合适的callback函数来处理用户的手势
*/
public boolean onTouch(View v, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
private class gestureListener implements GestureDetector.OnGestureListener{
// 用户轻触触摸屏,由1个MotionEvent ACTION_DOWN触发
public boolean onDown(MotionEvent e) {
Log.i("MyGesture", "onDown");
Toast.makeText(MainActivity.this, "onDown", Toast.LENGTH_SHORT).show();
return false;
}
/*
* 用户轻触触摸屏,尚未松开或拖动,由一个1个MotionEvent ACTION_DOWN触发
* 注意和onDown()的区别,强调的是没有松开或者拖动的状态
*
* 而onDown也是由一个MotionEventACTION_DOWN触发的,但是他没有任何限制,
* 也就是说当用户点击的时候,首先MotionEventACTION_DOWN,onDown就会执行,
* 如果在按下的瞬间没有松开或者是拖动的时候onShowPress就会执行,如果是按下的时间超过瞬间
* (这块我也不太清楚瞬间的时间差是多少,一般情况下都会执行onShowPress),拖动了,就不执行onShowPress。
*/
public void onShowPress(MotionEvent e) {
Log.i("MyGesture", "onShowPress");
Toast.makeText(MainActivity.this, "onShowPress", Toast.LENGTH_SHORT).show();
}
// 用户(轻触触摸屏后)松开,由一个1个MotionEvent ACTION_UP触发
///轻击一下屏幕,立刻抬起来,才会有这个触发
//从名子也可以看出,一次单独的轻击抬起操作,当然,如果除了Down以外还有其它操作,那就不再算是Single操作了,所以这个事件 就不再响应
public boolean onSingleTapUp(MotionEvent e) {
Log.i("MyGesture", "onSingleTapUp");
Toast.makeText(MainActivity.this, "onSingleTapUp", Toast.LENGTH_SHORT).show();
return true;
}
// 用户按下触摸屏,并拖动,由1个MotionEvent ACTION_DOWN, 多个ACTION_MOVE触发
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
Log.i("MyGesture22", "onScroll:"+(e2.getX()-e1.getX()) +" "+distanceX);
Toast.makeText(MainActivity.this, "onScroll", Toast.LENGTH_LONG).show();
return true;
}
// 用户长按触摸屏,由多个MotionEvent ACTION_DOWN触发
public void onLongPress(MotionEvent e) {
Log.i("MyGesture", "onLongPress");
Toast.makeText(MainActivity.this, "onLongPress", Toast.LENGTH_LONG).show();
}
// 用户按下触摸屏、快速移动后松开,由1个MotionEvent ACTION_DOWN, 多个ACTION_MOVE, 1个ACTION_UP触发
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
Log.i("MyGesture", "onFling");
Toast.makeText(MainActivity.this, "onFling", Toast.LENGTH_LONG).show();
return true;
}
};
}
1.4.3Scroller
scroller一般是和view的computeScroll方法配合使用,来实现弹性滑动,解决scrollTo和scrollBy滑动瞬间完成这个问题。它的使用套路比较固定:
Scroller mScroller = new Scroller(getContext());
/**
* 一秒内缓慢滚动到指定位置
**/
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
/**
* 一秒内缓慢滚动指定的距离
**/
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();
}
}
2.view的滑动
2.1使用scrollTo和scrollBy滑动
这是view本身自带的滑动方法,主要是理解滑动过程中mScrollX和mScrollY的改变规则,这两个属性可以通过getScrollX和getScrollY来获取。滑动过程中,mScrollX的值等于view的左边缘与view的内容的左边缘在水平方向上的距离,mScrollY的值等于view的上边缘和view的内容的上边缘的距离。一定要注意:scrollTo和scrollBy只能够改变view的内容的位置,并不能改变view在它的父容器中的位置。还有就是scrollTo的两个参数的正负值和我们正常的理解是相反的,向右滑动时mScrollX为负值,向左滑动时mScrollX为正值。具体如下图:
2.2使用动画滑动
使用动画来移动view,从源码角度来说主要是改变view的translationX和translationY的值。例如view100ms之内向右平移100个像素,但是view动画做平移会带来一个问题,它并不会真正改变view的位置,也就是说如果view有点击事件的话平移后触发onClick是没有反应的,反而是点击它原始的位置会有反应(3.0以上属性动画没有这个问题)。
2.3改变布使用参数来滑动
改变布局参数也就是百变LayoutParams,比如将button向右平移100个像素:
2.4各种滑动方式的对比
scrollTo和scrollBy | 使用动画滑动 | 改变布局参数 |
操作简单,但是只能滑动view的内容,并不能滑动view本身 | 有版本限制,3.0以下版本view动画并不能改变view本身的属性,如果view本身和用户没有交互的话使用动画来实现滑动时比较简单和适用的。 | 适用于和用户有交互的view |
例子,使用动画滑动来实现view在屏幕中跟随手势滑动的效果:
public class TestButton extends android.support.v7.widget.AppCompatTextView {
private static final String TAG = "TestButton";
private int mScaledTouchSlop;
// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
public TestButton(Context context) {
this(context, null);
}
public TestButton(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public TestButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mScaledTouchSlop = ViewConfiguration.get(getContext())
.getScaledTouchSlop();
Log.d(TAG, "sts:" + mScaledTouchSlop);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
//相对于手机屏幕左上角的x值,也就是手指位置x坐标
int x = (int) event.getRawX();
//相对于手机屏幕左上角的y值,也就是手指位置的y坐标
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
//按下
break;
}
case MotionEvent.ACTION_MOVE: {
//开始滑动
//x方向的滑动距离
int deltaX = x - mLastX;
//y方向上的滑动距离
int deltaY = y - mLastY;
Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);
int translationX = (int)ViewHelper.getTranslationX(this) + deltaX;
int translationY = (int)ViewHelper.getTranslationY(this) + deltaY;
//分别设置x,y方向上的滑动距离。
ViewHelper.setTranslationX(this, translationX);
ViewHelper.setTranslationY(this, translationY);
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
}
3.弹性滑动
3.1使用scroller
前面已经有了scroller的常见用法,现在来分析一下scroller的源码:
调用startScroll并没有做滑动相关的操作view是无法滑动的,其内部做的仅仅是一些赋值动作,真正滑动的是下面的invalidate方法。invalidate方法会导致view的重绘,在view的draw方法中又去会调用computeScroll方法也就是我们自己自定义实现的(在View中它是空实现)。在computeScroll中获取scroller的当前scrollX和scrollY并调用scrollTo(scrollX,scrollY)这才真正的实现了滑动,紧接着又调用了postInvalidate进行第二次重绘如此反复。mScroller.computeScrollOffset其实就是根据duration的时间流动的百分比来计算当前的scrollX和scrollY,至于返回值如果当前时间-开始时间小于duration那么标识滑动未结束返回true,否则表示滑动结束返回false。
3.2通过动画
动画天然的具有弹性滑动的功能。
3.3使用延时策略
利用handler或者view的postDelay方法,接连不断的发送延时消息就可以实现弹性滑动的效果,同时也可以通过while循环不断的滑动和sleep实现弹性滑动。
4.view的事件分发机制
4.1点击触摸事件的传递规则
MotionEvent即点击事件,对点击事件的事件分发即是对MotionEvent的事件分发。主要有三个方法:
boolean dispathTouchEvent:用于事件的分发,表示是否分发或者传递时间,返回值默认是super()也就是不断的向下分发直至被消费掉为止。它的返回值受当前view的onTouchEvent和下级view的dispathTouchEvent来共同决定,表示是否消耗当前事件。
返回true:如果直接返回true表示事件被直接消费了,该事件就停止分发并且也不会逆向向上传递就直接结束了。这个是针对同一事件序列而言的,比如ACTION_DOWN到这里结束了,后面的ACTION_UP也是会到这里结束的。
返回false:返回false的时候,该view是不会再执行自己的onTouchEvent方法了,会从该view的上一级onTouchEvent事件逆向向上传递。
返回super():事件会继续向下分发直至事件被消费为止。
boolean onInterceptTouchEvent:在dispathTouchEvent方法内部被调用是viewgroup特有的方法,用来判断是否拦截某个事件,返回结果表示是否拦截某个事件默认情况是不拦截的。如果当前view拦截了某个事件,那么在同一事件序列中此方法不会被再次调用。
返回true:表示viewgroup会拦截后续事件,在这一级会执行onTouchEvent方法并逆向向上传递
返回false:不做拦截正常向下分发至下级的dispathTouchEvent
返回super:和返回false是一样的
boolean onTouchEvent:在dispathTouchEvent方法内部被调用,处理点击事件,返回结果表示是否消耗了当前事件。如果不消耗则在同一事件序列中,当前view无法再次接收到该事件。
返回true:立即消费掉此事件,事件不会继续向上传递到此终止
返回false:不消费此次事件,事件将会层层向上传递直至被消费,但是在同一事件序列中当前view是无法再次接收到该事件的。
返回super:和返回false一样
view的onTouchListener 中的onTouch方法>当前view的onTouchEvent方法>当前view设置的onClickListener方法。
通常情况下点击事件的传递顺序:Activity-->Window-->View,事件总是先传递给Activity,Activity再传递给Window,Window传递给顶级View,然后顶级View会按照 事件分发机制往下传递。如果所有view的onTouchEvent都返回false,那么这个点击事件最终会返回给Activity,需要Activity的onTouchEvent来处理。事件传递机制总结:
- 4.1.1上面提到的同一个事件序列,指的是从手指接触屏幕的那一刻开始到手指离开屏幕的那一刻结束,也就是down--move.....--up。
- 4.1.2正常情况下一个事件序列只能被一个view拦截且消耗,因为一旦某个view拦截并且处理了某个事件,那么同一事件序列内的其他所有事件都会交给它来处理。
- 4.1.3某个view一旦决定拦截事件,那么这个事件序列就只能由它来处理,并且onInterceptTouchEvent不会再次被调用了,也就是后续直接就交给它了,不会再调用onInterceptTouchEvent来询问是否拦截了。
- 4.1.4和上一条类似,如果某个view的onTouchEvent返回了false,那么同一时间序列的其他事件都不会再交给它来处理,这次没干好下次就没机会了。
- 4.1.5如果view只消耗了action_down事件,那么这个点击事件会消失,最终都会传递给activity来处理。它的父view不会处理,并且它也会持续收到后续事件(这个没怎么搞明白)。
- 4.1.6默认情况下ViewGroup的onInterceptTouchEvent是返回false的,也就是不会进行事件拦截。
- 4.1.7View是没有onInterceptTouchEvent方法的,一旦有事件传递给他onTouchEvent立马会被调用。
- 4.1.8View的onTouchEvent默认都会返回true即消耗事件,除非它是不可点击的(clickable和longClickable都为false)。View的longClickable默认都是false的,clickable则要根据具体的控件来定,Button的clickable默认是true,textview的clickable默认是false。
- 4.1.9View的enable状态不影响onTouchEvent的返回值。
4.2事件分发的源码解析
4.2.1activity对点击事件的分发过程
4.2.2viewgroup对点击事件的分发过程
ViewGroup的dispathTouchEvent方法:最开始的条件时间类型为action_down或者mFirstTouchTarget != null的时候才会判断是否需要拦截当前事件,主要看看mFirstTouchTarget != null是什么意思。当事件由子元素处理时,mFirstTouchTarget会被赋值并且指向子元素,也就是说事件没有被viewgroup拦截被子元素处理时mFirstTouchTarget != null,具体赋值代码如下:
上述源码说明:1.当viewgroup决定拦截事件后,则后序的点击事件都会默认交给它来处理不会再调用onInterceptTouchEvent了。2.onInterceptTouchEvent并不会每次都会调用,如果我们想提前处理所有的点击事件需要在dispatchTouchEvent中来处理。3.当viewgroup不拦截事件的时候,事件会向下分发交由它的子view来处理:这个过程首先会遍历viewgroup的所有子元素,然后判断子元素是否可以接收到点击事件。能否接收到点击事件的标准就是点击事件的坐标是否落在子元素的区域内,满足的话点击事件就可以交由它来处理。
4.2.3view对点击事件的处理分发过程
首先下面这段代码表示的是onTouchListener.onTouch和onTouchEvent的优先级。
view的onTouchEvent方法的实现:
1.只要是CLICKABLE和LONG_CLIAKABLE中有一个为true就会消耗这个事件,跟view是否enable是没有关系的。一般情况下LONG_CLIAKABLE默认是false的,CLICKABLE则和具体额view有关,可点击的view的CLICKABLE是true的,比如button按钮,我们可以通过setClickable和setLongClickable来改变他们的值。
2.当action_up事件发生时,会触发performClick方法,如果view设置了onclickListener的话则会触发onClick方法。
5.view的滑动冲突
5.1常见的滑动冲突的场景
5.1.1外部滑动和内部滑动方向不一致
这种一般常见的就是viewpager和fragment组成的页面效果,但是在viewpager的内部已经对滑动冲突做了处理我们感觉不到而已。但是如果将viewpager换成scrollview我们就必须要处理滑动冲突了。
5.1.2外部滑动和内部滑动方向一致
这种稍微难处理一点,要么内外都是上下滑动,要么内外都是左右滑动。
5.1.3二者的嵌套
这种基本就是前面两种的叠加而已。
5.2滑动冲突的处理规则
- 5.2.1对于内外部滑动方向不一致的场景来说:
规则就是根据滑动方向来决定让谁来拦截点击事件。判别滑动方向:1.根据滑动路径和水平方向的夹角来判断,大于45度上下滑动小于45度左右滑动 2.根据水平和竖直方向的坐标距离差来判别 3.其实也可以根据水平和竖直方向上的速度差来做判断(具体没试过)
- 5.2.2方向一致这种一般是根据业务场景来判断到底是滑动外层view还是内层view,嵌套模式也一样需要根据业务来判断的。
5.3滑动冲突的解决方式
解决滑动冲突最关键一点就是要找到滑动规则,有了规则就好办了。
- 5.3.1外部拦截法:
简而言之就是点击事件都会先经过父容器的拦截处理,如果父容器需要此事件就拦截,父容器不需要此事件就不拦截,主要就是重写父容器的onInterceptTouchEvent方法,在其内部做相应的拦截即可。典型代码如下: