10.View实用类和属性
clipToPadding & clipChildren
clipChildren
是否限制子视图在其范围内,简单理解,就是其子 view 是否可以在超出自身原本的范围绘制 “ 突出 “ 的部分,默认值为 true。
- clipChildren 作用于爷 ViewGroup,用于限制 “ 爷爷 ViewGroup 的孙子 View” 是否可以超出 “ 孙子 View 的父 ViewGroup” 的范围,默认为 true 即不可以。
- 孙子 View 虽然能显示超出 其父 ViewGroup , 但不会超出其爷爷 ViewGroup。
错误理解
- 错误的理解:
如下图的例子所示, 对父级 LinearLayout 设置 clipChildren = false, 父 Layout 宽度为 100dp, 而内部子 View 的宽度为 200dp, 但在预览中可以看到,它内部的 子 View 并没超出父 LinearLayout 的 宽度范围? - 正确的理解:
clipChildren 作用于爷 ViewGroup,用于限制子 View 是否可以超出父 ViewGroup 的范围,默认为 true 即不可以,也可以在代码中设置:setClipChildren (boolean clipChildren)
,也可以从代码中判断某个 ViewGroup 的 clipChildren 值:boolean getClipChildren()。
1
2
3
4
5
6
<LinearLayout爷
android:clipChildren="false">
<LinearLayout父>
<View子(可超出父的范围) />
</LinearLayout>
</LinearLayout>
如果想让某个子 view 显示 其超过父 View 的 范围的部分, 就必须先找到 子 view 的 父 ViewGroup 的父 ViewGroup(即子 View 的爷爷 ViewGroup), 对其 爷爷 ViewGroup 设置 clipChildren = false,才能达到我们想要的效果
案例:底部 Tab 集合中间一个突出
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:orientation="vertical">
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:background="@color/transparent_40"
android:layout_height="0dip"
android:layout_weight="1.0"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dip"
android:layout_marginBottom="20dp"
android:background="#B0C4DE"
android:orientation="horizontal">
<ImageView
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1.0"
android:scaleType="fitCenter"
android:src="@mipmap/ic_launcher"/>
<ImageView
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1.0"
android:scaleType="fitCenter"
android:src="@mipmap/ic_launcher"/>
<ImageView
android:layout_width="0dip"
android:layout_height="200dip"
android:layout_gravity="center"
android:layout_weight="1.0"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher"/>
<ImageView
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1.0"
android:scaleType="fitCenter"
android:src="@mipmap/ic_launcher"/>
<ImageView
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1.0"
android:scaleType="fitCenter"
android:src="@mipmap/ic_launcher"/>
</LinearLayout>
</LinearLayout>
clipToPadding
clipToPadding 用来定义 ViewGroup 是否允许在 padding 中绘制。默认情况下,cliptopadding 被设置为 true, 也就是把 padding 中的值都进行裁切了。
以实现如下图所示的,ListView、RecycleView、ScrollView 的 “ 初始 padding” 的效果
clipToPadding 默认为 true,表示父容器会裁剪 padding 的,不允许子 View 在 padding 绘制;false 可以
案例 1:clipToPadding 和 ViewPager 配合
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
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff"
android:id="@+id/fl"
android:clickable="true"
android:clipChildren="false"
android:orientation="vertical">
<androidx.viewpager.widget.ViewPager
android:id="@+id/vp_clip"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_gravity="center"
android:layout_marginLeft="60dp"
android:layout_marginRight="60dp"
android:background="@android:color/darker_gray"
android:clipToPadding="false"
android:orientation="vertical"
android:overScrollMode="never"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:paddingBottom="20dp"
android:paddingTop="20dp"/>
</FrameLayout>
当我们把 clipToPadding 的属性设置为 false 的时候,在滑动的时候,就会忽略 padding 边界的存在,view 在滑动的时候,自由穿过 padding 的位置。
- ViewPager 设置
android:clipToPadding="false"
,ViewPager 的 item 就可以绘制到 padding 中 - FrameLayout 设置
android:clipChildren="false"
,ViewPager 的 item 超出 ViewPager 控件大小的也可以展示,不设置的话会被截取
案例 2:clipToPadding 和 clipChildren 结合 ScrollView、ListView 及 RecycleView 滑动
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f6f6f6"
android:clipChildren="false"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="100dp">
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="很长很长的文本\n很长很长的文本\n很长很长的文本\n很长很长的文本\n很长很长的文本\n很长很长的文本\n很长很长的文本\n很长很长的文本\n很长很长的文本\n很长很长的文本\n很长很长的文本\n很长很长的文本\n很长很长的文本\n"
android:textSize="50sp"/>
</ScrollView>
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#a00058f1"
android:padding="0dp"
android:includeFontPadding="false"
android:gravity="center"
android:text="TITLE"
android:textColor="#ffffffff"
android:textSize="20sp"/>
</FrameLayout>
<TextView
android:id="@+id/tv_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#a00058f1"
android:padding="0dp"
android:includeFontPadding="false"
android:gravity="center"
android:text="TITLE"
android:textColor="#ffffffff"
android:textSize="20sp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_gravity="bottom"
android:background="@android:color/darker_gray"
android:elevation="12dp"
android:orientation="horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@mipmap/ic_launcher"/>
<ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@mipmap/ic_launcher"/>
<ImageView
android:id="@+id/iv"
android:layout_width="0dp"
android:layout_height="180dp"
android:layout_gravity="bottom"
android:layout_weight="1"
android:scaleType="fitXY"
android:src="@mipmap/ic_launcher"/>
<ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@mipmap/ic_launcher"/>
<ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:src="@mipmap/ic_launcher"/>
</LinearLayout>
</LinearLayout>
初始化时,TextView 是距离顶部有一个 PaddingTop 的,默认 ScrollView clipToPadding 是 true,会裁剪子 View,不让其在自己的 padding 绘制,滚动时不会出现在 TITLE 下面;
设置为 false 后滚动,运行子 View 在自己的 padding 绘制,滚动时,文字会在 TITLE 下面
RecyclerView 带 padding 滚动穿过 padding
该属性很适合的应用场景: 设置 RecyclerView 的第一个 (最后一个)Item 距离屏幕 TOP(BOTTOM) 有一段距离的情况
有的需求场景是我们需要给列表上下留空,但是滑动的时候又要滑动到留空的区域,如果我们只是给 RecyclerView 设置了 paddingTop 和 paddingBottom ,那么我们可以发现在滑动 RecyclerView 的过程
中这个 padding 当然是存在的.在 padding 部分是看不到 RecyclerView 的 item 的,本质上是因为在这两部分没有绘制我们的 RecyclerView。
假若我们此时为 RecyclerView 设置属性 android:clipToPadding=”false”,同样再滑动 RecyclerView 此时可以
发现在 RecyclerView 的头部以上和尾部以下都占有 padding 部分,但是滑动依然可以显示我们的 RecyclerView 的 item。
应用
- 底部 tab 栏凸出一个 Tab
- 头像框框架,就可以不用适配 margin 了
Fading Edge
可以用于 ScrollView,ListView 等
- android:fadingEdge
fadingEdge 属性用来设置拉滚动条时 ,边框渐变的方向
none(边框颜色不变),horizontal(水平方向颜色变淡),vertical(垂直方向颜色变淡)。 - android:fadingEdgeLength
fadingEdgeLength 用来设置边框渐变的长度。 - android:requiresFadingEdge=”vertical”
案例 1:ScrollView+TextView
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<ScrollView
android:layout_width="match_parent"
android:layout_height="128dp"
android:layout_marginTop="20dp"
android:scrollbars="vertical"
android:scrollbarSize="50dp"
android:scrollbarFadeDuration="5000"
android:fadingEdge="vertical"
android:fadingEdgeLength="50dp"
android:requiresFadingEdge="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="@string/content"/>
</ScrollView>
案例 2:
TextView 还是无法滚动起来,还需要在 Activity 中添加如下一行代码,TextView 才能滚动起来:
1
tv.setMovementMethod(new ScrollingMovementMethod());
1
2
3
4
5
6
7
8
9
10
11
<TextView
android:layout_marginTop="100dp"
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="128dp"
android:background="@color/colorPrimary"
android:fadingEdge="vertical"
android:fadingEdgeLength="50dp"
android:requiresFadingEdge="vertical"
android:text="@string/content"
/>
ViewConfiguration
ViewConfiguration 介绍
ViewConfiguration 是 Android 自带 View 的常量配置类,用于保存了各类 View 的超时时间、点击、长按、拖动、滑动等等一些 View 的配置数据。
1
ViewConfiguration viewConfiguration = ViewConfiguration.get(Context);
不同的手机获取的值不太一样,和 density 有关
常用对象方法
touch 相关
getScaledTouchSlop 获取 touchSlop(系统滑动距离的最小值,大于该值可以认为滑动)
1
2
3
4
private static final int TOUCH_SLOP = 8; // dp
public int getScaledTouchSlop() {
return mTouchSlop;
}
getScaledPagingTouchSlop 一页的滑动距离
1
2
3
4
private static final int PAGING_TOUCH_SLOP = TOUCH_SLOP * 2; // dp
public int getScaledPagingTouchSlop() {
return mPagingTouchSlop;
}
fling
getScaledMinimumFlingVelocity 获得允许执行 fling (抛)的最小速度值
1
2
3
4
private static final int MINIMUM_FLING_VELOCITY = 50; // dp per second
public int getScaledMinimumFlingVelocity() {
return mMinimumFlingVelocity;
}
getScaledMaximumFlingVelocity 获得允许执行 fling (抛)的最大速度值
1
2
3
4
private static final int MAXIMUM_FLING_VELOCITY = 8000; // dp per second
public int getScaledMaximumFlingVelocity() {
return mMaximumFlingVelocity;
}
hasPermanentMenuKey
// Report if the device has a permanent menu key available to the user
// (报告设备是否有用户可找到的永久的菜单按键)
// 即判断设备是否有返回、主页、菜单键等实体按键(非虚拟按键)
boolean hasPermanentMenuKey = viewConfiguration.hasPermanentMenuKey();
常用静态方法
1
2
3
4
5
6
7
8
9
10
11
// 获得敲击超时时间,如果在此时间内没有移动,则认为是一次点击
int tapTimeout = ViewConfiguration.getTapTimeout();
// 双击间隔时间,在该时间内被认为是双击
int doubleTapTimeout = ViewConfiguration.getDoubleTapTimeout();
// 长按时间,超过此时间就认为是长按
int longPressTimeout = ViewConfiguration.getLongPressTimeout();
// 重复按键间隔时间
int repeatTimeout = ViewConfiguration.getKeyRepeatTimeout();
其他
- static int getDoubleTapTimeout()
返回一个 双击的毫秒超时时间。
它在第一次点击的 up 事件开始记录,在第二次点击的 down 事件停止。
两次点击之间的时间值小于等于 getDoubleTapTimeout(),就表示是一个双击操作 - static int getJumpTapTimeout()
This method was deprecated in API level 20.
如果第二次点击在这时间毫秒值内,则是双击事件;反之,在时间值内,未有第二次点击,即是单击事件
感觉跟 getDoubleTapTimeout() 功能重复了。可能这就是后来 deprecated 的原因吧
- static int getKeyRepeatDelay()
连续重复按键的延迟毫秒时间 - static int getKeyRepeatTimeout()
重复按键的超时毫秒时间
内部调用了 getLongPressTimeout()。
- static int getLongPressTimeout()
长按超时毫秒时间。超出它,表示长按 - static int getPressedStateDuration()
在 (子) 组件上按住状态的持续毫秒时间 - int getScaledDoubleTapSlop()
两次 touch 间的像素距离值。若满足它,在符合超时规则的同时,可被视为一个双击操作 - int getScaledEdgeSlop()
当用户 touch 在屏幕边缘时,插入一定像素值,以寻找出可触摸内容 - int getScaledFadingEdgeLength()
边缘渐变的像素长度 - int getScaledMaximumDrawingCacheSize()
View 的最大绘图缓存,以字节表示 - int getScaledMaximumFlingVelocity()
返回一个表示飞速滑动的最大初始速率值。单位是 像素/秒 - int getScaledMinimumFlingVelocity()
返回一个表示飞速滑动的最小初始速率值。单位是 像素/秒 - int getScaledOverflingDistance()
飞速滑动,当要显示 view 的边缘效果时,view 可以超出的最大像素距离值 - int getScaledOverscrollDistance()
滚动后,当要显示 view 的边缘效果时,view 可以超出的最大像素距离值 - int getScaledPagingTouchSlop()
一个 touch 动作,满足该像素距离时,可以认为用户滚动了一整个页面 - int getScaledScrollBarSize()
获取水平滚动条的宽 或 垂直滚动条的高,以像素为单位 - int getScaledScrollFactor()
8.0API。 - int getScaledTouchSlop()
满足这个像素距离,可以认为用户在滚动中 - int getScaledWindowTouchSlop()
定义一个 window 范围外的像素距离值,当 touch 动作,满足在该值以外,则认为可以 dismiss 该 window - static int getScrollBarFadeDuration()
scrollBar 逐渐消失的毫秒值 - static int getScrollDefaultDelay()
在滚动条消失前的延迟时间 - static float getScrollFriction()
一个代表了摩擦系数的标量。它应用在 flings 或 scrolls 状态。 - static int getTapTimeout()
点击超时毫秒值。当用户在该间隔时间内,没有 “move” 操作,就认为是单击操作;反之认为是 scroll 操作 - static long getZoomControlsTimeout()
为了响应用户对焦动作,焦点框显示的超时毫秒值。应该是用在摄像头一类的操作里。
ViewTreeObserver
什么是 ViewTreeObserver
ViewTreeObserver 是用来监听一些全局变化的。ViewTreeObserver 注册一个观察者来监听视图树,当视图树的布局、视图树的焦点、视图树将要绘制、视图树滚动等发生改变时,ViewTreeObserver 都会收到通知,ViewTreeObserver 不能被实例化,可以调用 View.getViewTreeObserver() 来获得
ViewTreeObserver 获取
1
2
3
4
5
6
7
8
9
public ViewTreeObserver getViewTreeObserver() {
if (mAttachInfo != null) {
return mAttachInfo.mTreeObserver;
}
if (mFloatingTreeObserver == null) {
mFloatingTreeObserver = new ViewTreeObserver(mContext);
}
return mFloatingTreeObserver;
}
- 如果如果 mAttachInfo!=null,直接返回 mAttachInfo.mTreeObserver
- 如果 mAttachInfo==null,new 一个 ViewTreeObserver,在 dispatchAttachedToWindow,会将
mFloatingTreeObserver
merge 到ViewRootImpl.AttachInfo.mTreeObserver
,将 mFloatingTreeObserver 中的 Listener 也合并到ViewRootImpl.AttachInfo.mTreeObserver
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// View
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
if (mFloatingTreeObserver != null) {
info.mTreeObserver.merge(mFloatingTreeObserver);
mFloatingTreeObserver = null;
}
}
// ViewTreeObserver
void merge(ViewTreeObserver observer) {
if (observer.mOnWindowAttachListeners != null) {
if (mOnWindowAttachListeners != null) {
mOnWindowAttachListeners.addAll(observer.mOnWindowAttachListeners);
} else {
mOnWindowAttachListeners = observer.mOnWindowAttachListeners;
}
}
if (observer.mOnWindowFocusListeners != null) {
if (mOnWindowFocusListeners != null) {
mOnWindowFocusListeners.addAll(observer.mOnWindowFocusListeners);
} else {
mOnWindowFocusListeners = observer.mOnWindowFocusListeners;
}
}
if (observer.mOnGlobalFocusListeners != null) {
if (mOnGlobalFocusListeners != null) {
mOnGlobalFocusListeners.addAll(observer.mOnGlobalFocusListeners);
} else {
mOnGlobalFocusListeners = observer.mOnGlobalFocusListeners;
}
}
if (observer.mOnGlobalLayoutListeners != null) {
if (mOnGlobalLayoutListeners != null) {
mOnGlobalLayoutListeners.addAll(observer.mOnGlobalLayoutListeners);
} else {
mOnGlobalLayoutListeners = observer.mOnGlobalLayoutListeners;
}
}
if (observer.mOnPreDrawListeners != null) {
if (mOnPreDrawListeners != null) {
mOnPreDrawListeners.addAll(observer.mOnPreDrawListeners);
} else {
mOnPreDrawListeners = observer.mOnPreDrawListeners;
}
}
if (observer.mOnDrawListeners != null) {
if (mOnDrawListeners != null) {
mOnDrawListeners.addAll(observer.mOnDrawListeners);
} else {
mOnDrawListeners = observer.mOnDrawListeners;
}
}
if (observer.mOnFrameCommitListeners != null) {
if (mOnFrameCommitListeners != null) {
mOnFrameCommitListeners.addAll(observer.captureFrameCommitCallbacks());
} else {
mOnFrameCommitListeners = observer.captureFrameCommitCallbacks();
}
}
if (observer.mOnTouchModeChangeListeners != null) {
if (mOnTouchModeChangeListeners != null) {
mOnTouchModeChangeListeners.addAll(observer.mOnTouchModeChangeListeners);
} else {
mOnTouchModeChangeListeners = observer.mOnTouchModeChangeListeners;
}
}
if (observer.mOnComputeInternalInsetsListeners != null) {
if (mOnComputeInternalInsetsListeners != null) {
mOnComputeInternalInsetsListeners.addAll(observer.mOnComputeInternalInsetsListeners);
} else {
mOnComputeInternalInsetsListeners = observer.mOnComputeInternalInsetsListeners;
}
}
if (observer.mOnScrollChangedListeners != null) {
if (mOnScrollChangedListeners != null) {
mOnScrollChangedListeners.addAll(observer.mOnScrollChangedListeners);
} else {
mOnScrollChangedListeners = observer.mOnScrollChangedListeners;
}
}
if (observer.mOnWindowShownListeners != null) {
if (mOnWindowShownListeners != null) {
mOnWindowShownListeners.addAll(observer.mOnWindowShownListeners);
} else {
mOnWindowShownListeners = observer.mOnWindowShownListeners;
}
}
if (observer.mGestureExclusionListeners != null) {
if (mGestureExclusionListeners != null) {
mGestureExclusionListeners.addAll(observer.mGestureExclusionListeners);
} else {
mGestureExclusionListeners = observer.mGestureExclusionListeners;
}
}
observer.kill();
}
以 OnGlobalLayoutListener 来源码分析
onGlobalLayout 调用的时机
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
37
38
39
40
41
42
43
44
45
// ViewTreeObserver
public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
checkIsAlive();
if (mOnGlobalLayoutListeners == null) {
mOnGlobalLayoutListeners = new CopyOnWriteArray<OnGlobalLayoutListener>();
}
mOnGlobalLayoutListeners.add(listener);
}
public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim) {
checkIsAlive();
if (mOnGlobalLayoutListeners == null) {
return;
}
mOnGlobalLayoutListeners.remove(victim);
}
private void checkIsAlive() {
if (!mAlive) {
throw new IllegalStateException("This ViewTreeObserver is not alive, call "
+ "getViewTreeObserver() again");
}
}
public final void dispatchOnGlobalLayout() {
// NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
// perform the dispatching. The iterator is a safe guard against listeners that
// could mutate the list by calling the various add/remove methods. This prevents
// the array from being modified while we iterate it.
final CopyOnWriteArray<OnGlobalLayoutListener> listeners = mOnGlobalLayoutListeners;
if (listeners != null && listeners.size() > 0) {
CopyOnWriteArray.Access<OnGlobalLayoutListener> access = listeners.start();
try {
int count = access.size();
for (int i = 0; i < count; i++) {
//调用方法在这里
access.get(i).onGlobalLayout();
}
} finally {
listeners.end();
}
}
}
调用 dispatchOnGlobalLayout 方法的地方,是在 ViewRootImpl#performTraversals 方法中调用:
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
// ViewRootImpl
private void performTraversals() {
if (mFirst) {
...
host.dispatchAttachedToWindow(mAttachInfo, 0); // 保证在调用dispatchOnGlobalLayout时,已经merge好了游离的ViewTreeObserver
...
}
...
if (...){
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
...
if (didLayout) {
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
}
...
if (triggerGlobalLayoutListener) {
mAttachInfo.mRecomputeGlobalAttributes = false;
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
}
...
}
顺序依次是 dispatchAttachedToWindow、performMeasure、performLayout、dispatchOnGlobalLayout。发现 dispatchOnGlobalLayout 是在这几个方法的最后面执行,由此可以知道:
- 执行 dispatchOnGlobalLayout 时,已经合并到了 AttachInfo 的 mTreeObserver 中
- 执行在 onGlobalLayout 中可以拿到 view 的宽高,因为 performMeasure(测量)、performLayout(布局)已经先执行
提供的功能
OnGlobalLayoutListener 视图树的布局发生改变或者 View 在视图树的可见状态发生改变时
OnGlobalLayoutListener 是 ViewTreeObserver 的内部类,当一个视图树的布局发生改变时,可以被 ViewTreeObserver 监听到,这是一个注册监听视图树的观察者 (observer),在视图树的全局事件改变时得到通知。ViewTreeObserver 不能直接实例化,而是通过 getViewTreeObserver() 获得。
当在一个视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变时,所要调用的回调函数的接口类
- OnGlobalLayoutListener 可能会被多次触发
OnGlobalLayoutListener 何时调用?
1
2
3
4
5
6
7
8
9
10
11
// ViewRootImpl#performTraversals() Android29
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (triggerGlobalLayoutListener) {
mAttachInfo.mRecomputeGlobalAttributes = false;
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
}
private void kill() {
mAlive = false;
}
一次获取 removeListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 监听一次View的ViewTreeObserver.OnGlobalLayoutListener
*/
public static void addOnGlobalLayoutListener(@NonNull final View view, final ViewTreeObserver.OnGlobalLayoutListener listener) {
if (view == null || listener == null) {
return;
}
view.getViewTreeObserver()
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (listener != null) {
listener.onGlobalLayout();
}
if (Build.VERSION.SDK_INT >= 16) {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
}
});
}
应用场景
监听键盘的弹出和隐藏,虚拟导航栏高度
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
37
38
39
40
41
42
43
44
public void setOnKeyboardHiddenChangedListener(final Activity activity, OnKeyboardHiddenChangedListener listener) {
mListener = listener;
if (activity != null && listener != null && DeviceUtils.getSystemSDKInt() > 18) {
mListener.attach(this);
mNavigationHeight = WindowUtils.getNavigationBarHeight();
// API 18 Android 4.3 adjustResize的和adjustPan一样导致计算键盘高度和是否收起或展开不正确
final View decorView = activity.getWindow().getDecorView();
ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
// 在其他页面弹出键盘会影响布局
if ((activity instanceof BaseActivity && !((BaseActivity) activity).isOnResume) || activity.isFinishing())
return;
Rect rect = new Rect();
decorView.getWindowVisibleDisplayFrame(rect);
int displayHeight = rect.bottom;
int height = decorView.getHeight();
int keyboardHeight = Math.abs(height - displayHeight);
if (mPreviousKeyboardHeight != keyboardHeight) {
final boolean hide = (double) (displayHeight - rect.top) / height > 0.8; //表示是否弹出了软键盘
// 部分手机上虚拟按键的高度计算
if (hide) { //如果没有弹出软键盘,多出来的高度就是虚拟按键的高度
mNavigationHideHeight = keyboardHeight;
} else { //如果弹出了软键盘,需要判断是否弹出或隐藏了虚拟按键
if (isVirtualNavigationChanged(mPreviousKeyboardHeight - keyboardHeight)) {
mNavigationHideHeight = keyboardHeight < mPreviousKeyboardHeight ? 0 : mNavigationHeight;
}
}
mKeyboardHide = hide;
mListener.onKeyboardHiddenChanged(keyboardHeight, hide);
mPreviousKeyboardHeight = keyboardHeight;
}
}
};
decorView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
mOnGlobalLayoutListenerRef = new SoftReference<>(onGlobalLayoutListener);
}
}
在 onCreate 中获取 View 的 width 和 height
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获得ViewTreeObserver
ViewTreeObserver observer=view.getViewTreeObserver();
//注册观察者,监听变化
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//判断ViewTreeObserver 是否alive,如果存活的话移除这个观察者
if(observer.isAlive()){
observer.removeGlobalOnLayoutListener(this);
//获得宽高
int viewWidth=view.getMeasuredWidth();
int viewHeight=view.getMeasuredHeight();
}
}
});
OnPreDrawListener 视图树将要被绘制前调用
OnPreDrawListener 是什么?
即将绘制视图树时执行的回调函数。这时所有的视图都测量完成并确定了框架。 可以使用该方法来调整滚动边框,甚至可以在绘制之前请求新的布局。
onPreDraw 返回值:
true,即将绘制,会执行 onDraw
- false,绘制将取消,并重新安排绘制,不会执行 onDraw
1
2
3
4
5
6
7
@Override
public boolean onPreDraw() {
// ...
// Return true to proceed with the current drawing pass, or false to cancel.
// 返回 true 继续绘制,返回false取消。
return true;
}
OnPreDrawListener 调用时机源码分析
ViewRootImpl.performTraversals()
在 ViewRootImpl 的 performTraversals(),在执行了 performMeasure() 和 preformLayout() 后(即测量和布局后,这时可以获取得到 view 的宽高了),会分发 preDraw 的逻辑
- view 不可见,取消当前绘制
- dispatchOnPreDraw 返回 true 且 View 可见,会重新安排下一次绘制;否则取消绘制
- dispatchOnPreDraw 返回 false,执行 performDraw 进行绘制操作
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
37
38
39
40
41
42
43
44
45
// ViewRootImpl
private void performTraversals() {
// ...
performMeasure();
// ...
performLayout();
// ...
final int viewVisibility = getHostVisibility();
final boolean isViewVisible = viewVisibility == View.VISIBLE;
// ...
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
} else {
if (isViewVisible) {
// Try again
scheduleTraversals();
} else {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
// We may never draw since it's not visible. Report back that we're finished
// drawing.
if (!wasReportNextDraw && mReportNextDraw) {
mReportNextDraw = false;
pendingDrawFinished();
}
}
}
}
int getHostVisibility() {
return (mAppVisible || mForceDecorViewVisibility) ? mView.getVisibility() : View.GONE;
}
dispatchOnPreDraw()
dispatchOnPreDraw(),返回 true,取消当前绘制,安排到下一次绘制;返回 false 执行绘制
- 遍历所有的 listeners,所有的 OnPreDrawListener 都返回了 true,那么 dispatchOnPreDraw 就会返回 false,会执行绘制
- 如果有一个 OnPreDrawListener 返回了 false,那么 dispatchOnPreDraw 就会返回 true,取消当前绘制,安排到下一次绘制
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
// ViewTreeObserver
public final boolean dispatchOnPreDraw() {
boolean cancelDraw = false;
final CopyOnWriteArray<OnPreDrawListener> listeners = mOnPreDrawListeners;
if (listeners != null && listeners.size() > 0) {
CopyOnWriteArray.Access<OnPreDrawListener> access = listeners.start();
try {
int count = access.size();
for (int i = 0; i < count; i++) {
cancelDraw |= !(access.get(i).onPreDraw());
}
} finally {
listeners.end();
}
}
return cancelDraw;
}
public void addOnPreDrawListener(OnPreDrawListener listener) {
checkIsAlive();
if (mOnPreDrawListeners == null) {
mOnPreDrawListeners = new CopyOnWriteArray<OnPreDrawListener>();
}
mOnPreDrawListeners.add(listener);
}
public void removeOnPreDrawListener(OnPreDrawListener victim) {
checkIsAlive();
if (mOnPreDrawListeners == null) {
return;
}
mOnPreDrawListeners.remove(victim);
}
View.getViewTreeObserver()
View 通过 getViewTreeObserver() 获取到当前 View attach 到 windows 的 mAttachInfo,从 mAttachInfo 中获取到 mTreeObserver
1
2
3
4
5
6
7
8
9
public ViewTreeObserver getViewTreeObserver() {
if (mAttachInfo != null) {
return mAttachInfo.mTreeObserver;
}
if (mFloatingTreeObserver == null) {
mFloatingTreeObserver = new ViewTreeObserver(mContext);
}
return mFloatingTreeObserver;
}
OnPreDrawListener 应用场景
ViewTreeObserver.addOnPreDrawListener 来获得宽高
在执行 onDraw 之前已经执行了 onLayout() 和 onMeasure(),可以得到宽高了,当获得正确的宽高后,请移除这个观察者,否则回调会多次执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获得ViewTreeObserver
ViewTreeObserver observer=view.getViewTreeObserver();
//注册观察者,监听变化
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if(observer.isAlive()){
observer.removeOnDrawListener(this);
}
//获得宽高
int viewWidth=view.getMeasuredWidth();
int viewHeight=view.getMeasuredHeight();
return true;
}
});
CoordinatorLayout 用到获取宽高
有一个例子就是 CoordinatorLayout 调用 Behavior 的 onDependentViewChanged 就是通过注册 OnPreDrawListener 接口,在绘制的时候检查界面是否发生变化,如果变化就调用 Behavior 的 onDependentViewChanged。
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors();
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
//注册OnPreDrawListener
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
// We're set to fitSystemWindows but we haven't had any insets yet...
// We should request a new dispatch of window insets
ViewCompat.requestApplyInsets(this);
}
mIsAttachedToWindow = true;
}
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
//分发OnDependentViewChanged
dispatchOnDependentViewChanged(false);
return true;
}
}
void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Check child views before for anchor
for (int j = 0; j < i; j++) {
final View checkChild = mDependencySortedChildren.get(j);
if (lp.mAnchorDirectChild == checkChild) {
offsetChildToAnchor(child, layoutDirection);
}
}
//判断是否发生变化
// Did it change? if not continue
final Rect oldRect = mTempRect1;
final Rect newRect = mTempRect2;
getLastChildRect(child, oldRect);
getChildRect(child, true, newRect);
if (oldRect.equals(newRect)) {
continue;
}
recordLastChildRect(child, newRect);
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) {
// If this is not from a nested scroll and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
//调用onDependentViewChanged
final boolean handled = b.onDependentViewChanged(this, checkChild, child);
...
}
}
}
}
OnPreDrawListener 使用总结
- 遇到异常场景, 要在 onPreDraw()return 时, 一定要 return true;onPreDraw 如果一直返回 false, 导致整个 view 树的重构流程不能完成
- 及时在 onPreDraw() 中 getViewTreeObserver().removeOnPreDrawListener(this);
- 没有特殊原因, 最终 return true 就好了
OnGlobalFocusChangeListener 当一个视图树的焦点状态改变时,会调用的接口
OnScrollChangedListener 当视图树的一些组件发生滚动时会调用的接口
OnTouchModeChangeListener 当视图树的触摸模式发生改变时,会调用的接口
坑
View.getViewTreeObserver 每次得到的 ViewTreeObserver 可能不是同一个实例
- View 未 attach 到 Window,获取的 ViewTreeObserver 是各自 View 的 mFloatingTreeObserver
- View 已经 attach 到 Window,所有 View 获取到的 ViewTreeObserver 都是
ViewRootImpl#AttachInfo#mTreeObserver
同一个实例
OnPreDrawListener 黑屏问题?
问题 1:黑屏
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class addOnPreDrawListenerDemo : AppCompatActivity() {
private fun preDrawBlackScreenIssue() {
// 问题1:返回false的情况下,又忘记移除removeOnPreDrawListener代码,onPreDraw会一直调用,画面黑屏
fl_root.viewTreeObserver.addOnPreDrawListener {
LogUtils.w("hacket", "onPreDraw")
// fl_root.viewTreeObserver.removeOnPreDrawListener(this) //1
false //2
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_on_pre_draw_listener)
preDrawBlackScreenIssue()
}
}
onPreDraw onPreDraw onPreDraw onPreDraw // … 一直不停的 onPreDraw,大约每隔 16ms 刷新一次
解决:
- onPreDraw 返回 true
- 及时 removeOnPreDrawListener�
问题 2:APP 线上的问题黑屏
现象:
从设置页点击返回按钮,会员页面点返回按钮黑屏
问题代码:
分析
使用 addOnPreDrawListener 的时候,要注意 onPreDraw 返回 false 会导致页面 onDraw 不调用,就不绘制内容(体现出来就是黑屏,但不会卡死 ANR),并且要注意回调完之后需要 remove 掉这个 listener。
现在有个典型的黑屏案例,就是因为 onPreDraw 返回 false,并且因为 OnPreDrawListener 没有 remove 掉,导致 onPreDraw 一直返回 false,最后体现出来就是页面一直不会绘制,但是 activity 的 onResume 和 onStart 方法都能正常运行,打断点也正常,就很难排查到问题。
解决:
onPreDraw 返回 true 避免黑屏,并且及时 remove 掉添加 listener
问题 3:一次 Android 卡屏的分析
remove OnPreDrawListener 不了的问题
代码:
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 问题:非root的ViewaddOnPreDrawListener,返回false,然后被removeView了,onPreDraw会一直被调用,导致一直得不到绘制
btn_click.setOnClickListener {
progress.visibility = View.VISIBLE
tv_info.viewTreeObserver.addOnPreDrawListener(object :
ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
LogUtils.w("hacket", "onPreDraw")
tv_info.viewTreeObserver.removeOnPreDrawListener(this) //1
return false //2
}
})
LogUtils.w("hacket", "removeView tv_info")
fl_root.removeView(tv_info) //3 container是当前Activity的根容器,就只是一个FrameLayout
}
// xml
<?xml version="1.0" encoding="utf-8"?>
<me.hacket.assistant.samples.ui.杂项.ViewTreeObserver.addOnPreDrawListener.FrameLayoutTest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fl_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是测试的信息" />
<Button
android:id="@+id/btn_click"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="50dp"
android:layout_marginTop="40dp"
android:text="go"
android:textAllCaps="false" />
<Button
android:id="@+id/btn_click2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="100dp"
android:layout_marginTop="100dp"
android:text="这是其他Button" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="50dp"
android:layout_marginTop="130dp"
android:visibility="gone" />
</me.hacket.assistant.samples.ui.杂项.ViewTreeObserver.addOnPreDrawListener.FrameLayoutTest>
问题:
- 点击 go 按钮后,就卡住了,但是其他的按钮可以点击
分析:
- tv_info�调用 addOnPreDrawListener�添加了一个 OnPreDrawListener 监听,并在 onPreDraw 中调用
removeOnPreDrawListener�
移除 OnPreDrawListener - 然后通过 root 将 tv_info 移除了
- 然后 OnPreDrawListener 没有移除掉,这是因为 tv_info 被移除后,tv_info 就已经 detach window 了,那么其 AttachInfo 就是为空,通过 getViewTreeObserver() 获取到的就是游离的 mFloatingTreeObserver,和之前 addOnPreDrawListener 的 ViewTreeObserver 不是同一个对象了,自然就移除不掉了,那么就会一直在调用 preDraw,导致整棵树一直得不到绘制
解决 1: 用 root View 来 addOnPreDrawListener
1
2
3
4
5
6
7
8
9
10
11
12
13
btn_click.setOnClickListener {
progress.visibility = View.VISIBLE
fl_root.viewTreeObserver.addOnPreDrawListener(object :
ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
LogUtils.w("hacket", "onPreDraw")
fl_root.viewTreeObserver.removeOnPreDrawListener(this) //1
return false //2
}
})
LogUtils.w("hacket", "removeView tv_info")
fl_root.removeView(tv_info) //3 container是当前Activity的根容器,就只是一个FrameLayout
}
解决 2: onPreDraw 返回 true
1
2
3
4
5
6
7
8
9
10
11
12
13
btn_click.setOnClickListener {
progress.visibility = View.VISIBLE
tv_info.viewTreeObserver.addOnPreDrawListener(object :
ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
LogUtils.w("hacket", "onPreDraw")
tv_info.viewTreeObserver.removeOnPreDrawListener(this) //1
return true //2
}
})
LogUtils.w("hacket", "removeView tv_info")
fl_root.removeView(tv_info) //3 container是当前Activity的根容器,就只是一个FrameLayout
}
removeOnGlobalLayoutListener 出错
1
2
3
4
5
6
7
8
9
override fun onCreate(savedInstanceState: Bundle?) {
val viewTreeObserver = window.decorView.viewTreeObserver
val onGlobalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
}
- 报错:
1
java.lang.IllegalStateException: This ViewTreeObserver is not alive, call getViewTreeObserver() again
- 分析
在 onCreate 方法获取的 ViewTreeObserver,此时 View 还没 attachToWindow,获取到的是 View 自己的 ViewTreeObservermFloatingTreeObserver
,等 View attachToWindow 时,会将自身 View 的mFloatingTreeObserver
merge 到 ViewRootImpl#AttachInfo#ViewTreeObserver 去,并将mFloatingTreeObserver
kill 并将引用置为 null,此时 onGlobalLayout 回调后,viewTreeObserver.removeOnGlobalLayoutListener(this)
会调用 checkIsAlive(),而此时 mAlive 已经为 false,抛出异常了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void kill() {
mAlive = false;
}
public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim) {
checkIsAlive();
if (mOnGlobalLayoutListeners == null) {
return;
}
mOnGlobalLayoutListeners.remove(victim);
}
private void checkIsAlive() {
if (!mAlive) {
throw new IllegalStateException("This ViewTreeObserver is not alive, call "
+ "getViewTreeObserver() again");
}
}
- 解决
1
2
3
4
5
6
7
8
9
override fun onCreate(savedInstanceState: Bundle?) {
val viewTreeObserver = window.decorView.viewTreeObserver
val onGlobalLayoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
// viewTreeObserver.removeOnGlobalLayoutListener(this) // java.lang.IllegalStateException: This ViewTreeObserver is not alive, call getViewTreeObserver() again
window.decorView.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
}
Ref
- 直面底层:经常用的 ViewTreeObserver 背后的原理
https://mp.weixin.qq.com/s/ixgiyEEDuLVfZHcjUF9DsA