1.View
- 什么是View
a. 所有View控件:TextView,Button等等
b. 所有ViewGroup(容器):RelativeLayout,LinearLayout。是容器也是View
- View 事件
MotionEvent:ACTION_DOWN、ACTION_UP、ACTION_MOVE ;手势的落下,抬起,移动
但其中有两个比较特殊的事件: ACTION_CANCEL
和 ACTION_OUTSIDE
。
|
|
TouchSlop 可以识别出的滑动最小距离;判断此时是否属于滑动事件
VelocityTracker 滑动速度
GestureDetector 手势检测,常用于负责手势场景:比如双击;滑动调节进度;音量。OnGestureListener
|
|
Scroller 带过度的弹性滑动
2.View滑动
实现View滑动
a.scrollTo/scrollBy 只能改变View 中内容的位置,而不能改变View的位置
b.改变布局参数LayoutParam
c.使用动画
d.使用延时策略,Handler 计时,慢慢改变属性,达到动画效果
3.View 事件分发
类型 | 相关方法 | Activity | ViewGroup | View |
---|---|---|---|---|
事件分发 | dispatchTouchEvent | √ | √ | √ |
事件拦截 | onInterceptTouchEvent | X | √ | X |
事件消费 | onTouchEvent | √ | √ | √ |
- dispatchTouchEvent 最下层View 如果没有消费,则onTouchEvent逐个返回给上层逐步去消费
- 当返回为true时,顺序下发会中断
- false ,顺序下发
|
|
- onTouchEvent 最下层View如果消费了,所有回调到此为止
- 如果返回值是true,表示消费(consume)了这个事件。以ACTION_DOWN为例,如果某个控件的onTouchEvent返回值为true,则后续的n个ACTION_MOVE与1个ACTION_UP都会逐层传递到这个控件的onTouchEvent进行处理。
- 如果返回值是false,则会将ACTION_DOWN传递给其父ViewGroup的onTouchEvent进行处理,直到由哪一层ViewGroup消费了ACTION_DOWN事件为止。
|
|
- onInterceptTouchEvent 如果ViewGroup拦截,则View1无事件;
- 如果返回值是true,代表事件在当前的viewGroup中会被处理,向下传递之路被截断(所有子控件将没有机会参与Touch事件),同时把事件传递给当前的控件的onTouchEvent()继续进行传递或处理。
- 如果返回值是false,即不拦截当前传递来的事件,会继续向下传递,把事件交给子控件的onInterceptTouchEvent()。
|
|
- 补充:
- 水平滑动置为true.则父布局不响应(比如竖直RecyclerView嵌套横向RecyclerView,横向滑动时屏蔽父布局)
|
|
- 滑动冲突解决
- 外部拦截:onInterceptTouchEvent()
- 内部拦截:dispathTouchEvent() 配合requestDisallowInterceptTouchEvent()
4.View 原理
ViewRoot/DecorView
ViewRoot对应ViewRootImpl类,它是连接WindowManagerService和DecorView的纽带,View的三大流程(测量(measure),布局(layout),绘制(draw))均通过ViewRoot来完成。
DecorView是FrameLayout的子类,它可以被认为是Android视图树的根节点视图。DecorView作为顶级View,一般情况下它内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下三个部分,上面是个ViewStub,延迟加载的视图(应该是设置ActionBar,根据Theme设置),中间的是标题栏(根据Theme设置,有的布局没有),下面的是内容栏。
MeasureSpec: 32进制,高2位代表SpecMode(测量模式),低30位代表SpecSize(规格大小)
SpecModel:UNSPECIFIED 父容器不限制;EXACTLY 精确大小对应match_parent;AT_MOST 父容器指定大小,对应wrap_content
LayoutParams
DecorView,其MeasureSpec由窗口尺寸和其自身的LayoutParams来共同决定
普通View,其MeasureSpec由父容器的MeasureSpec 和 自身的LayoutParams决定
5.View Tree绘制流程
- 整个 View 树的绘图流程在ViewRoot.java类的performTraversals()函数展开,该函数所做 的工作可简单概况为是否需要重新计算视图大小(measure)、是否需要重新安置视图的位置(layout)、以及是否需要重绘(draw),流程图如下:
- Measure
- View的measure 方法会去调用View的onMeasure(),这里面调用getDefaultSize(),里面根据MeasureSpec返回View大小。
- 如果是ViewGroup,遍历子元素的measure方法,子元素在递归执行;ViewGroup是一个抽象类,没有重写View的onMeasure()方法,而是提供一个measureChildren()的方法。为什么会这样呢?因为不同的ViewGroup测量方式不一致
- 总结:自上而下的进行比measure,根据子View的MeasureSpec测量其尺寸,返回上一级
- 一般可以在measure结束后获取宽高,measure就是和layout 一直的结果。如果不确定View有没有measure,可以强行在获取之前调用 view.measure(0,0);
LinearLayout
上述Measure过程显示ViewGroup是一个抽象类并无onMeasure()方法,而是不同的ViewGroup有不同的测量方式
根据orientation去measureVertical()或者measureHorizontal(),在单个方向上存储高度或者宽度和,没测量一个就会增加。
RelativeLayout会measure两遍;measureChildHorizontal()和measureChild(),其子View会measure 4遍
FrameLayout会measure两遍,其子View会measure 2遍
LinearLayout会measure 1遍,必要时(View MatchParent)会measure2遍,其子View会measure 2遍
ConstraintLayout会measure两遍,其子View会measure 2遍
- Layout
- 自上而下进行遍历。在onLayout()中遍历其子元素layout,在onLayout根据Measure结果进行布局,确定其位置
- 获取View的宽高最好是在onLayout中获取,这是View的最终宽高
- draw
- 1.绘制背景 background.draw(canvas)
- 2.绘制自己 (onDraw)
- 3.绘制children (dispatchDraw)
- 4.绘制装饰 (onDrawScrollBars)
- 当我们自定义View不具备draw功能时;可以使用View.setWillNotDraw()开启标记位。便于后续优化
- 自定义View
- 继承自View,自我实现onDraw()等,onLayout,onMeasure(),全靠canvas来draw出所有的View来
- 继承现有的View(TextView,ReleativeView等),infalte相应的layout,处理相应的业务
- 集成ViewGroup,
- View 常用接口
- Activity/View # onWindowFocusChanged 失去焦点和获取焦点会各调用一次
- view.post(runnable) 发个消息队列,View 初始化好了之后,runnable被调用。常用于init时机不对
- ViewTreeObserver() onGlobalLayout()是获取宽高的好时机
- view.measure() 手动measure;常用 view.measure(0,0);
6.RemoteView
在其他进程中更新View
- 自定义通知栏View
- 桌面小部件
Android 更新UI的两种方法
Handler
Activity.runOnUiThread()
123456789101112131415161718192021class TestThread extends Thread {@Overridepublic void run() {super.run();// Activity 内部类ThreadrunOnUiThread(new Runnable() {@Overridepublic void run() {}});}}// 或者this.runOnUiThread(new Runnable() { //this = Activity@Overridepublic void run() {}});1234567// 源码还是Handlerpublic final void runOnUiThread(Runnable action) {if (Thread.currentThread() != mUiThread) {mHandler.post(action);} else {action.run();}
View.post()
1234567891011121314Runnable testR = new Runnable() {@Overridepublic void run() {view.post(new Runnable() {@Overridepublic void run() {view.setText("post");}});}};Thread t = new Thread(testR);t.start();1234567891011public boolean post(Runnable action) {final AttachInfo attachInfo = mAttachInfo;if (attachInfo != null) {// 如果当前View加入到了window中,直接调用UI线程的Handler发送消息return attachInfo.mHandler.post(action);}// Assume that post will succeed later// View未加入到window,放入ViewRootImpl的RunQueue中ViewRootImpl.getRunQueue().post(action);return true;}当View已经attach到了window,两者是没有区别的,都是调用UI线程的Handler发送runnable到MessageQueue,最后都是由handler进行消息的分发处理。
但是如果View尚未attach到window的话,runnable被放到了ViewRootImpl#RunQueue中,最终也会被处理,但不是通过MessageQueue。
总结
- 当View已经attach到window,不管什么线程, 调用View#post 和 调用Handler#post效果一致
- 当View尚未attach到window,主线程调用View#post发送的runnable将在下一次performTraversals到来时执行,而非主线程调用View#post发送的runnable将无法被执行。sRunQueues 是一个ThreadLocal 类型,也就是说你上面加入的mActions跟主线程中的mActions 不是同一个.
- 可以通过在主线程调用View#post发送runnable来获取下一次performTraversals时视图树中View的布局信息,如宽高。
- 如果调用View#post方法的线程对象被GC-Root引用,则发送的runnable将会造成内存泄漏。