-
-
Notifications
You must be signed in to change notification settings - Fork 643
Expand file tree
/
Copy pathCustomToolbar.kt
More file actions
171 lines (152 loc) · 6.89 KB
/
CustomToolbar.kt
File metadata and controls
171 lines (152 loc) · 6.89 KB
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
package com.swmansion.rnscreens
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.view.Choreographer
import android.view.WindowInsets
import android.view.WindowManager
import androidx.appcompat.widget.Toolbar
import androidx.core.view.WindowInsetsCompat
import com.facebook.react.modules.core.ReactChoreographer
import com.facebook.react.uimanager.ThemedReactContext
import com.swmansion.rnscreens.utils.InsetsCompat
import com.swmansion.rnscreens.utils.resolveInsetsOrZero
import kotlin.math.max
/**
* Main toolbar class representing the native header.
*
* This class is used to store config closer to search bar.
* It also handles inset/padding related logic in coordination with header config.
*/
@SuppressLint("ViewConstructor") // Only we construct this view, it is never inflated.
open class CustomToolbar(
context: Context,
val config: ScreenStackHeaderConfig,
) : Toolbar(context) {
// Due to edge-to-edge enforcement starting from Android SDK 35, isTopInsetEnabled prop has been
// removed. Previously, shouldAvoidDisplayCutout, shouldApplyTopInset would directly return the
// value of isTopInsetEnabled. Now, the values of shouldAvoidDisplayCutout, shouldApplyTopInse
// are hard-coded to true (which was the value used previously for isTopInsetEnabled when
// edge-to-edge was enabled: https://github.com/software-mansion/react-native-screens/pull/2464/files#diff-bd1164595b04f44490738b8183f84a625c0e7552a4ae70bfefcdf3bca4d37fc7R34).
private val shouldAvoidDisplayCutout = true
private val shouldApplyTopInset = true
private var lastInsets = InsetsCompat.NONE
private var isForceShadowStateUpdateOnLayoutRequested = false
private var isLayoutEnqueued = false
private val layoutCallback: Choreographer.FrameCallback =
object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
isLayoutEnqueued = false
// The following measure specs are selected to work only with Android APIs <= 29.
// See https://github.com/software-mansion/react-native-screens/pull/2439
measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST),
)
layout(left, top, right, bottom)
}
}
override fun requestLayout() {
super.requestLayout()
val softInputMode =
(context as ThemedReactContext)
.currentActivity
?.window
?.attributes
?.softInputMode
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && softInputMode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) {
// Below Android API 29, layout is not being requested when subviews are being added to the layout,
// leading to having their subviews in position 0,0 of the toolbar (as Android don't calculate
// the position of each subview, even if Yoga has correctly set their width and height).
// This is mostly the issue, when windowSoftInputMode is set to adjustPan in AndroidManifest.
// Thus, we're manually calling the layout **after** the current layout.
@Suppress("SENSELESS_COMPARISON") // mLayoutCallback can be null here since this method can be called in init
if (!isLayoutEnqueued && layoutCallback != null) {
isLayoutEnqueued = true
// we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current
// looper loop instead of enqueueing the update in the next loop causing a one frame delay.
ReactChoreographer
.getInstance()
.postFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
layoutCallback,
)
}
}
}
override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets? {
val unhandledInsets = super.onApplyWindowInsets(insets)
// There are few UI modes we could be running in
//
// 1. legacy non edge-to-edge mode,
// 2. edge-to-edge with gesture navigation,
// 3. edge-to-edge with translucent navigation buttons bar.
//
// Additionally we need to gracefully handle possible display cutouts.
val cutoutInsets =
resolveInsetsOrZero(WindowInsetsCompat.Type.displayCutout(), unhandledInsets)
val systemBarInsets =
resolveInsetsOrZero(WindowInsetsCompat.Type.systemBars(), unhandledInsets)
// This seems to work fine in all tested configurations, because cutout & system bars overlap
// only in portrait mode & top inset is controlled separately, therefore we don't count
// any insets twice.
val horizontalInsets =
InsetsCompat.of(
cutoutInsets.left + systemBarInsets.left,
0,
cutoutInsets.right + systemBarInsets.right,
0,
)
// We want to handle display cutout always, no matter the HeaderConfig prop values.
// If there are no cutout displays, we want to apply the additional padding to
// respect the status bar.
val verticalInsets =
InsetsCompat.of(
0,
max(cutoutInsets.top, if (shouldApplyTopInset) systemBarInsets.top else 0),
0,
max(cutoutInsets.bottom, 0),
)
val newInsets = InsetsCompat.add(horizontalInsets, verticalInsets)
if (lastInsets != newInsets) {
lastInsets = newInsets
applyExactPadding(
lastInsets.left,
lastInsets.top,
lastInsets.right,
lastInsets.bottom,
)
}
return unhandledInsets
}
override fun onLayout(
hasSizeChanged: Boolean,
l: Int,
t: Int,
r: Int,
b: Int,
) {
super.onLayout(hasSizeChanged, l, t, r, b)
config.onNativeToolbarLayout(
this,
hasSizeChanged || isForceShadowStateUpdateOnLayoutRequested,
)
isForceShadowStateUpdateOnLayoutRequested = false
}
fun updateContentInsets() {
contentInsetStartWithNavigation = config.preferredContentInsetStartWithNavigation
setContentInsetsRelative(config.preferredContentInsetStart, config.preferredContentInsetEnd)
}
private fun applyExactPadding(
left: Int,
top: Int,
right: Int,
bottom: Int,
) {
requestForceShadowStateUpdateOnLayout()
setPadding(left, top, right, bottom)
}
private fun requestForceShadowStateUpdateOnLayout() {
isForceShadowStateUpdateOnLayoutRequested = shouldAvoidDisplayCutout
}
}