View基础知识

 

什么是View

Android中的控件主要分为容器控件和普通控件,它们都继承View父类,容器控件中可以容纳多个控件(容器控件与普通控件)。这种关系最终形成View树的结构

 

View的位置参数

View的位置主要由4个顶点来决定,分别是:topleftrightbottom。其中top是控件左端横坐标,right是控件右端横坐标,top是控件顶部纵坐标,bottom是控件底部纵坐标。可以通过getter方法获得

 


那么,控件的宽高就为

Width = right – leftHeight = bottom – top

Android3.0开始,View增加了额外的参数:xytranslationXtranlationY,这四个参数都是相对于父容器的坐标;xy表示View的左上角坐标,translationXtranslationY是表示View相对于父容器的偏移量(属性动画平移);两者的换算关系:

X = left + translationXY = top + translationY

注意:在View平移过程中,topleft是原始左上角的位置信息,其值并不会改变,改变的是xytranslationXtranlationY

 

MotionEventTouchSlop

在手机触摸屏幕后产生的一系列事件,典型的事件类型:

ACTION_DOWN, ACTION_MOVE, ACTION_UP

例如:点击事件, DOWN-> UP

滑动事件:DOWN->MOVE->UP

通过MotionEvent对象提供的方法我们可以获取到事件发生的坐标位置:getX/getYgetRowX/getRowY;前者获取的坐标是相对于View自身,后者坐标的是相对于手机屏幕左上角

 

TouchSlop是系统所能识别出的被认为是滑动的最小距离,如果用户在屏幕滑动的两点之间距离小于这个常量,那么系统就认为是在进行滑动操作。这是一个常量,与设备有关,不同设备上可能不同

ViewConfiguration.get(this).getScaledTouchSlop()

意义:在处理滑动时可以把该值作为临界值

VelocityTrackerGestureDetectorScroller

VelocityTracker用于追踪手指在滑动过程中的速度(包括水平与垂直)

// 获取对象

VelocityTracker tracker = VelocityTrakcher.obtain();

// 添加事件

tracker.addMovement(event);

//计算速度

tracker.computeCurrentVelocity(times);

int xVelocity = (int) tracker.getXVelocity();

int yVelocity = (int) tracker.getYVelocity();

// 不用时重置并回收

tracker.clear();

tracker.recycle();

GestureDetector作用主要是进行手势的设定与检测,可辅助检测用户的单击,滑动,双击等行为

Scroller主要用于实现View的弹性滑动

 

View的滑动

实现View滑动的三种方式:

1. 使用ScrollTo/ScrollBy

ScrollBy是基于当前位置的相对滑动

ScrollTo是基于参数的绝对滑动

ScrollBy内部其实也是调用的scrollTo方法

scrollTo(mScrollX + x,mScrollY + y);

mScrollX表示View左边缘到View内容左边缘的距离

mScrollY表示View上边缘到View内容上边缘的距离

注意:两方法改变的是View内容位置无法改变View在布局中的位置

(也就是说无法scroll到其他控件的区域中)

 

SrcollTo/By是瞬间完成,可以配合ScrollerHandler实现弹性滑动

利用Handler即每次延时发送一个消息调用SrcollTo/By方法

利用Scroller实现弹性滑动原理:

当我们构造一个Scroller对象并调用startScroll方法时,其实Scroller内部什么事也没做(只是保存了传递的参数)

那么View是如何实现滑动呢

调用startScroll后接着调用invalidate方法,该方法会导致View重绘,在重绘时会调用computeScroll方法,该方法由我们覆写调用computeScrollOffsetcomputeScrollOffset方法会根据插值器类型以及时间的流逝计算当前的scrollXscrollY,接着我们就可以通过Scroller对象getter方法获取这两个值并调用scrollTo方法;调用scrollTo方法后再次调用invalidate方法触发下一次滑动直至滑动结束

computeScrollOffset的返回值是boolean值,true表示滑动未结束

总结Scroller本身不能实现View的滑动,需要配合computeScroll方法才能完成弹性滑动效果,通过不断的让View重绘,而每次重绘距离起始时间会有一个时间间隔,通过这个时间间隔获得View当前需要滑动的位置,然后通过scrollTo进行滑动

 

2.使用动画

动画可以分为:View动画(帧动画,补间动画)与属性动画

两种动画的区别本质在于:后者能够改变控件的位置

原因分析:之前有提到translationX/Y,这两个属性是在android3.0新增的,而属性动画只支持android3.0+,低版本需要使用nineoldandroids库进行兼容。translationX/Y是表示控件相对于父容器的偏移位置(类似margin,两者相互独立)。属性动画即通过这两个值来改变控件位置的,也就是说设置translationX/Y是能够改变控件位置的,但是不会改变控件LayoutParams中的margin属性,改变的是控件本身android:translationX/Y属性。

margin属性会改变控件的顶点坐标(lrtb),而translationX/Y属性是不会改变LayoutParams的,改变的是xymagin属性是属于父容器的属性,而translationX/Y是属于控件本身的(理解)。

那么对于属性动画的复位,我们可以直接用view. setTranslationX(0);

可以通过以下几种方式获取控件位置:

view.getLocationInWindow(pos);// 控件在其父窗口中的坐标位置

    view.getLocationOnScreen(pos);// 控件在其整个屏幕上的坐标位置

    view.getLocalVisibleRect();

view.getGlobalVisibleRect();

注:通过View动画 + updateLayoutParams的方式也可实现改变位置

Android3.0以下通过兼容库实现的属性动画本质还是View动画

 

3. 改变布局参数:适合于有交互的View

layoutParams=(RelativeLayout.LayoutParams) v.getLayoutParams();

 

View的事件分发机制

三个核心方法:

dispatchTouchEvent(MotionEvent event)

onInterceptTouchEvent(MotionEvent event)

onTouchEvent(MotionEvent event)

三者关系(伪代码):

public void diapatchTouchEvent(Motion event){

boolean consume = false;

if(onInterceptTouchEvent(event)){

consume = true;

}else{

if(haveChild)

consume = child. dispatchTouchEvent(event);

}

return consume;

}

 

事件的传递:事件的捕获过程与事件的冒泡过程

捕获(传递)过程:Activity -> Window -> View(过程中被拦截将终止)

冒泡(处理)过程:View -> Activity(过程中被消费将终止)

当一个点击事件产生后,对于一个ViewGroup来说首先会调用dispatchTouchEvent方法,如果这个ViewGrouponInteceptTouchEvent方法返回true表示要拦截当前事件,那么该事件会交给这个ViewGroup处理,它的onTouchEvent方法会被调用,如果onInterceptTouchEvent方法返回false表示不拦截事件,这时事件会继续传递给子元素,子元素的dispatchTouchEvent被调用,如此反复直至事件最终被处理

当一个View需要处理事件时,并且它设置了onTouchListener,那么onTouch方法会先被调用,如果onTouch方法返回false会继续调用onTouchEvent方法,否则onTouchEvent方法将不会被调用,也就是说onTouch优先级比onTouchEvent高。我们常见的onClick优先级是最低的,onClick会在onTouchEvent中处理;

在事件传递与处理的过程中,如果某个ViewonTouchEvent方法返回false,那么事件会传递给父容器的onTouchEvent;如果最后所有ViewonTouchEvent方法均返回false(所有元素都不处理该事件),那么该事件最终会传递给Activity,即ActivityonTouchEvent方法被调用(一层一层向上抛的过程,类似冒泡)

 

关于事件传递机制的一些总结:

1. 同一个事件序列是指从手指触摸屏幕开始到手指离开屏幕,即从down开始,中间包含nmove,最后以up结束

2. 正常情况下一个事件序列只能够被一个View拦截且消耗。因为一旦某个元素拦截了某事件,那么同一个事件序列的后续事件都会直接交由它处理,因此同一个事件序列的事件不能分别由两个View处理。(特殊手段:通过调用其他ViewonTouchEvent方法)

3. View决定拦截事件后,那么同一事件序列的后续事件都会由它处理,不会再调用onInterceptTouchEvent询问是否拦截

4. View一旦开始处理事件(即执行onTouchEvent方法),若它不消耗ACTION_DOWN事件,那么同一事件序列的后续事件都不会再交由它处理,并且将该DOWN事件重新交由父容器处理

5. 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent方法不会被调用,并且该View能够持续收到后续事件,最终消失的点击事件由Activity处理

6. ViewGroup默认不拦截任何事件

7. View没有onInterceptTouchEvent方法,不具有拦截功能,一旦事件传递给它,它的onTouchEvent方法会被调用

8. ViewonTouchEvent方法默认返回true(除非它是不可点击的,clickablelongClickable均为false)。ViewlongClickable默认都为falseclickable视情况而定,如Buttonclickable默认为true,而TextViewclickable默认为false

注: setOnClickListener时会自动设置clickabletrue

  setOnLongClickListener时会自动设置longClickabletrue

9. Viewenable属性不影响onTouchEvent方法的默认返回值

10. OnClick发生的前提的View可点击并且收到DOWNUP事件

11. 通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父容器的事件分发过程(除ACTION_DOWN事件外)

12. View消耗ACTION_DOWN事件后,父容器仍然可拦截后续事件

注意:上述提及的可拦截事件的View均指的是ViewGroup

源码解析:

Activity#dispatchTouchEvent 事件分发源头

Window# superDispatchTouchEvent 事件传递给DecorView(抽象)

- Window可控制顶级View的外观和行为策略

- Window的唯一实现类是PhoneWindow

PhoneWindow # superDispatchTouchEvent事件传递DecorView(实现)

- DecorViewPhoneWindow的内部类,代表顶级View

- DecorView继承自FrameLayout,会调用其dispatchTouchEvent

ViewGroup#dispatchTouchEvent  ViewGroup的事件分发

- ACTION_DOWN事件会导致状态重置

- (actionMasked == MotionEvent.ACTION_DOWN

|| mFirstTouchTarget != null) 避免多次执行拦截方法

遍历子View将事件向下分发

- dispatchTransformedTouchEvent调用子View的事件分发

若找到接收事件的子View会跳出循环并赋值mFirstTouchTarget

如果ViewGroup没有子元素或者没有任何子元素处理事件将调用View的事件分发super.dispatchTouchEvent(event);

 

View的滑动冲突

在界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突。

主要有三个场景:

场景1:内外两层滑动方向不一致

(如:ViewPager嵌套FragmentFragment存在ListViewViewPager 内部已进行了滑动冲突的处理)

场景2:内外两层滑动方向一致

(如:ViewPagerSlideMenu同时存在)

场景3:上述两种场景的嵌套

(如:网易云音乐首页界面)


 

滑动冲突的处理规则

原理:主要利用事件分发机制

分析:针对场景1情况,我们可以通过某些滑动信息来决定将事件交给谁处理,如:水平滑动距离与垂直滑动距离的比较、水平速度与竖直速度或者通过路径与水平/垂直方向的角度大小;针对场景2,只能根据业务需求或者自定义控件的效果去决定由谁处理事件;而场景3是场景1与场景2的嵌套,我们只需要分别处理好中间层与外层,以及中间层与内层两个滑动冲突即可。

方式:外部拦截法,内部拦截法

外部拦截法是指点击事件先经过父容器的拦截处理,如果父容器需要处理该事件则将其拦截(符合事件分发机制)

外部拦截法伪代码如下:

public void boolean onInterceptTouchEvent(MotionEvent event){

    boolean intercepted = false;

    int x = (int) event.getX();

    int y = (int) event.getY();

    switch(event.getAction()){

        case MotionEvent.ACTIO_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;

}

针对不同的滑动冲突,只需修改if条件即可。在DOWN事件时必须返回false,因为一旦返回true,后续事件直接交由父容器处理,那么事件根本无法传递给子元素;在MOVE事件中进行具体判断决定是否拦截事件;UP事件中必须返回false,主要2个原因:1.UP事件本身没有太大意义,其作为事件序列的最后一个事件必定会传递给父容器2.UP事件返回true那么子元素将处理不了click事件

内部拦截法是指将父容器是否拦截事件交由子元素决定,这种方式与事件分发机制不一致,需配合requestDisallowInterceptTouchEvent

内部拦截法伪代码如下:

// 子元素

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;

}

// 父容器

public boolean onInterceptTouch(MotionEvent event){

    int action = event.getAction();

    if( action == MotionEvent.ACTION_DOWN ){

        return false;

    }else{

        return true; // 默认拦截除ACTION_DOWN以外所有事件

    }

}

 

首先,父容器默认拦截除DOWN事件以外其他事件。父容器具体是否要拦截事件由子元素决定,子元素的dispatchTouchEvent方法中,DOWN事件中默认不允许父容器拦截,MOVE事件中根据具体条件决定父容器是否拦截事件,UP事件无须关注

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐