Android总结1-View

Posted by Jfson on 2018-12-25

附上大量自定义View出现的时候,如何解耦

1.View

  • 什么是View

a. 所有View控件:TextView,Button等等

b. 所有ViewGroup(容器):RelativeLayout,LinearLayout。是容器也是View

  • View 事件

MotionEvent:ACTION_DOWN、ACTION_UP、ACTION_MOVE ;手势的落下,抬起,移动

但其中有两个比较特殊的事件: ACTION_CANCELACTION_OUTSIDE

1
2
3
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){} }

TouchSlop 可以识别出的滑动最小距离;判断此时是否属于滑动事件

VelocityTracker 滑动速度

GestureDetector 手势检测,常用于负责手势场景:比如双击;滑动调节进度;音量。OnGestureListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Override
public boolean onTouchEvent(MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
GestureDetector gestureDetector = new GestureDetector(this.getBaseContext(), new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent motionEvent) {
return false;
}
@Override
public void onShowPress(MotionEvent motionEvent) {
}
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
return false;
}
@Override
public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
return false;
}
@Override
public void onLongPress(MotionEvent motionEvent) {
}
@Override
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
return false;
}
});

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 ,顺序下发
1
2
3
4
5
6
7
8
9
10
MainActivity [老板]: dispatchTouchEvent 经理,我准备发展一下电商业务,下周之前做一个淘宝出来.
RootView [经理]: dispatchTouchEvent 呼叫技术部,老板要做淘宝,下周上线.
RootView [经理]: onInterceptTouchEvent (老板可能疯了,但又不是我做.)
ViewGroupA [组长]: dispatchTouchEvent 老板要做淘宝,下周上线?
ViewGroupA [组长]: onInterceptTouchEvent (看着不太靠谱,先问问小王怎么看)
View1 [码农]: dispatchTouchEvent 做淘宝???
View1 [码农]: onTouchEvent 这个真心做不了啊.
ViewGroupA [组长]: onTouchEvent 小王说做不了.
RootView [经理]: onTouchEvent 报告老板, 技术部说做不了.
MainActivity [老板]: onTouchEvent 这么简单都做不了,你们都是干啥的(愤怒).
  • onTouchEvent 最下层View如果消费了,所有回调到此为止
    • 如果返回值是true,表示消费(consume)了这个事件。以ACTION_DOWN为例,如果某个控件的onTouchEvent返回值为true,则后续的n个ACTION_MOVE与1个ACTION_UP都会逐层传递到这个控件的onTouchEvent进行处理。
    • 如果返回值是false,则会将ACTION_DOWN传递给其父ViewGroup的onTouchEvent进行处理,直到由哪一层ViewGroup消费了ACTION_DOWN事件为止。
1
2
3
4
5
6
7
MainActivity [老板]: dispatchTouchEvent 把按钮做的好看一点,要有光泽,给人一种点击的欲望.
RootView [经理]: dispatchTouchEvent 技术部,老板说按钮不好看,要加一道光.
RootView [经理]: onInterceptTouchEvent
ViewGroupA [组长]: dispatchTouchEvent 给按钮加上一道光.
ViewGroupA [组长]: onInterceptTouchEvent
View1 [码农]: dispatchTouchEvent 加一道光.
View1 [码农]: onTouchEvent 做好了.
  • onInterceptTouchEvent 如果ViewGroup拦截,则View1无事件;
    • 如果返回值是true,代表事件在当前的viewGroup中会被处理,向下传递之路被截断(所有子控件将没有机会参与Touch事件),同时把事件传递给当前的控件的onTouchEvent()继续进行传递或处理。
    • 如果返回值是false,即不拦截当前传递来的事件,会继续向下传递,把事件交给子控件的onInterceptTouchEvent()。
1
2
3
4
5
6
MainActivity [老板]: dispatchTouchEvent 现在项目做到什么程度了?
RootView [经理]: dispatchTouchEvent 技术部,你们的app快做完了么?
RootView [经理]: onInterceptTouchEvent
ViewGroupA [组长]: dispatchTouchEvent 项目进度?
ViewGroupA [组长]: onInterceptTouchEvent
ViewGroupA [组长]: onTouchEvent 正在测试,明天就测试完了
  • 补充:
    • 水平滑动置为true.则父布局不响应(比如竖直RecyclerView嵌套横向RecyclerView,横向滑动时屏蔽父布局)
1
2
3
getParent().requestDisallowInterceptTouchEvent(true);
- true 不要拦截,即不处理
- false 正常处理
  • 滑动冲突解决
    • 外部拦截: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()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class TestThread extends Thread {
    @Override
    public void run() {
    super.run();
    // Activity 内部类Thread
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    }
    });
    }
    }
    // 或者
    this.runOnUiThread(new Runnable() { //this = Activity
    @Override
    public void run() {
    }
    });
    1
    2
    3
    4
    5
    6
    7
    // 源码还是Handler
    public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
    mHandler.post(action);
    } else {
    action.run();
    }

  • View.post()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Runnable testR = new Runnable() {
    @Override
    public void run() {
    view.post(new Runnable() {
    @Override
    public void run() {
    view.setText("post");
    }
    });
    }
    };
    Thread t = new Thread(testR);
    t.start();
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public 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。

    总结

    1. 当View已经attach到window,不管什么线程, 调用View#post 和 调用Handler#post效果一致
    2. 当View尚未attach到window,主线程调用View#post发送的runnable将在下一次performTraversals到来时执行,而非主线程调用View#post发送的runnable将无法被执行。sRunQueues 是一个ThreadLocal 类型,也就是说你上面加入的mActions跟主线程中的mActions 不是同一个.
    3. 可以通过在主线程调用View#post发送runnable来获取下一次performTraversals时视图树中View的布局信息,如宽高。
    4. 如果调用View#post方法的线程对象被GC-Root引用,则发送的runnable将会造成内存泄漏。

pv UV: