-
-
Notifications
You must be signed in to change notification settings - Fork 638
Expand file tree
/
Copy pathScreenStackHeaderConfig.kt
More file actions
479 lines (412 loc) · 16.5 KB
/
ScreenStackHeaderConfig.kt
File metadata and controls
479 lines (412 loc) · 16.5 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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
package com.swmansion.rnscreens
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.text.TextUtils
import android.util.TypedValue
import android.view.Gravity
import android.view.View.OnClickListener
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import com.facebook.react.ReactApplication
import com.facebook.react.bridge.JSApplicationIllegalArgumentException
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.ReactPointerEventsView
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.views.text.ReactTypefaceUtils
import com.swmansion.rnscreens.events.HeaderAttachedEvent
import com.swmansion.rnscreens.events.HeaderDetachedEvent
import kotlin.math.max
class ScreenStackHeaderConfig(
context: Context,
private val pointerEventsImpl: ReactPointerEventsView,
) : FabricEnabledHeaderConfigViewGroup(context),
ReactPointerEventsView by pointerEventsImpl {
constructor(context: Context) : this(context, pointerEventsImpl = PointerEventsBoxNoneImpl())
private val configSubviews = ArrayList<ScreenStackHeaderSubview>(3)
val toolbar: CustomToolbar
var isHeaderHidden = false // named this way to avoid conflict with platform's isHidden
var isHeaderTranslucent =
false // named this way to avoid conflict with platform's isTranslucent
private var title: String? = null
private var titleColor = 0
private var titleFontFamily: String? = null
private var direction: String? = null
private var titleFontSize = 0f
private var titleFontWeight = 0
private var backgroundColor: Int? = null
private var isBackButtonHidden = false
private var isShadowHidden = false
private var isDestroyed = false
private var backButtonInCustomView = false
private var tintColor = 0
private var isAttachedToWindow = false
private val defaultStartInset: Int
private val defaultStartInsetWithNavigation: Int
private val backClickListener =
OnClickListener {
screenFragment?.let {
val stack = screenStack
if (stack != null && stack.rootScreen == it.screen) {
val parentFragment = it.parentFragment
if (parentFragment is ScreenStackFragment) {
if (parentFragment.screen.nativeBackButtonDismissalEnabled) {
parentFragment.dismissFromContainer()
} else {
parentFragment.dispatchHeaderBackButtonClickedEvent()
}
}
} else {
if (it.screen.nativeBackButtonDismissalEnabled) {
it.dismissFromContainer()
} else {
it.dispatchHeaderBackButtonClickedEvent()
}
}
}
}
var isTitleEmpty: Boolean = false
val preferredContentInsetStart
get() = defaultStartInset
val preferredContentInsetEnd
get() = defaultStartInset
val preferredContentInsetStartWithNavigation
get() =
// Reset toolbar insets. By default we set symmetric inset for start and end to match iOS
// implementation where both right and left icons are offset from the edge by default. We also
// reset startWithNavigation inset which corresponds to the distance between navigation icon and
// title. If title isn't set we clear that value few lines below to give more space to custom
// center-mounted views.
if (isTitleEmpty) {
0
} else {
defaultStartInsetWithNavigation
}
val headerHeightUpdateProxy = ScreenStackHeaderHeightUpdateProxy()
fun destroy() {
isDestroyed = true
}
/**
* Native toolbar should notify the header config component that it has completed its layout.
*/
fun onNativeToolbarLayout(
toolbar: Toolbar,
shouldUpdateShadowStateHint: Boolean,
) {
if (!shouldUpdateShadowStateHint) {
return
}
val isBackButtonDisplayed = toolbar.navigationIcon != null
val contentInsetStartEstimation =
if (isBackButtonDisplayed) {
toolbar.currentContentInsetStart + toolbar.paddingStart
} else {
max(toolbar.currentContentInsetStart, toolbar.paddingStart)
}
// Assuming that there is nothing to the left of back button here, the content
// offset we're interested in in ShadowTree is the `left` of the subview left.
// In case it is not available we fallback to approximation.
val contentInsetStart =
configSubviews.firstOrNull { it.type === ScreenStackHeaderSubview.Type.LEFT }?.left
?: contentInsetStartEstimation
val contentInsetEnd = toolbar.currentContentInsetEnd + toolbar.paddingEnd
headerHeightUpdateProxy.updateHeaderHeightIfNeeded(this, screen)
// Note that implementation of the callee differs between architectures.
updateHeaderConfigState(
toolbar.width,
toolbar.height,
contentInsetStart,
contentInsetEnd,
)
}
override fun onLayout(
changed: Boolean,
l: Int,
t: Int,
r: Int,
b: Int,
) = Unit
override fun onAttachedToWindow() {
super.onAttachedToWindow()
isAttachedToWindow = true
val surfaceId = UIManagerHelper.getSurfaceId(this)
UIManagerHelper
.getEventDispatcherForReactTag(context as ReactContext, id)
?.dispatchEvent(HeaderAttachedEvent(surfaceId, id))
onUpdate()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
isAttachedToWindow = false
val surfaceId = UIManagerHelper.getSurfaceId(this)
UIManagerHelper
.getEventDispatcherForReactTag(context as ReactContext, id)
?.dispatchEvent(HeaderDetachedEvent(surfaceId, id))
}
private val screen: Screen?
get() = parent as? Screen
private val screenStack: ScreenStack?
get() = screen?.container as? ScreenStack
val screenFragment: ScreenStackFragment?
get() {
val screen = parent
if (screen is Screen) {
val fragment: Fragment? = screen.fragment
if (fragment is ScreenStackFragment) {
return fragment
}
}
return null
}
fun onUpdate() {
val stack = screenStack
val isTop = stack == null || stack.topScreen == parent
if (!isAttachedToWindow || !isTop || isDestroyed) {
return
}
val activity = screenFragment?.activity as AppCompatActivity? ?: return
if (direction != null) {
if (direction == "rtl") {
toolbar.layoutDirection = LAYOUT_DIRECTION_RTL
} else if (direction == "ltr") {
toolbar.layoutDirection = LAYOUT_DIRECTION_LTR
}
}
// orientation and status bar management
screen?.let {
// we set the traits here too, not only when the prop for Screen is passed
// because sometimes we don't have the Fragment and Activity available then yet, e.g. on the
// first setting of props. Similar thing is done for Screens of ScreenContainers, but in
// `onContainerUpdate` of their Fragment
val reactContext =
if (context is ReactContext) {
context as ReactContext
} else {
it.fragmentWrapper?.tryGetContext()
}
ScreenWindowTraits.trySetWindowTraits(it, activity, reactContext)
}
if (isHeaderHidden) {
if (toolbar.parent != null) {
screenFragment?.removeToolbar()
}
headerHeightUpdateProxy.updateHeaderHeightIfNeeded(this, screen)
return
}
if (toolbar.parent == null) {
screenFragment?.setToolbar(toolbar)
}
activity.setSupportActionBar(toolbar)
// non-null toolbar is set in the line above and it is used here
val actionBar = requireNotNull(activity.supportActionBar)
// hide back button
actionBar.setDisplayHomeAsUpEnabled(
screenFragment?.canNavigateBack() == true && !isBackButtonHidden,
)
// title
actionBar.title = title
if (TextUtils.isEmpty(title)) {
isTitleEmpty = true
}
// Reset toolbar insets. By default we set symmetric inset for start and end to match iOS
// implementation where both right and left icons are offset from the edge by default. We also
// reset startWithNavigation inset which corresponds to the distance between navigation icon and
// title. If title isn't set we clear that value few lines below to give more space to custom
// center-mounted views.
toolbar.updateContentInsets()
// when setSupportActionBar is called a toolbar wrapper gets initialized that overwrites
// navigation click listener. The default behavior set in the wrapper is to call into
// menu options handlers, but we prefer the back handling logic to stay here instead.
toolbar.setNavigationOnClickListener(backClickListener)
// shadow
screenFragment?.setToolbarShadowHidden(isShadowHidden)
// translucent
screenFragment?.setToolbarTranslucent(isHeaderTranslucent)
val titleTextView = findTitleTextViewInToolbar(toolbar)
if (titleColor != 0) {
toolbar.setTitleTextColor(titleColor)
}
if (titleTextView != null) {
if (titleFontFamily != null || titleFontWeight > 0) {
val titleTypeface =
ReactTypefaceUtils.applyStyles(
null,
0,
titleFontWeight,
titleFontFamily,
context.assets,
)
titleTextView.typeface = titleTypeface
}
if (titleFontSize > 0) {
titleTextView.textSize = titleFontSize
}
}
// background
backgroundColor?.let { toolbar.setBackgroundColor(it) }
// color
if (tintColor != 0) {
toolbar.navigationIcon?.colorFilter =
PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_ATOP)
}
// subviews
for (i in toolbar.childCount - 1 downTo 0) {
if (toolbar.getChildAt(i) is ScreenStackHeaderSubview) {
toolbar.removeViewAt(i)
}
}
var i = 0
val size = configSubviews.size
while (i < size) {
val view = configSubviews[i]
val type = view.type
if (type === ScreenStackHeaderSubview.Type.BACK) {
// we special case BACK button header config type as we don't add it as a view into toolbar
// but instead just copy the drawable from imageview that's added as a first child to it.
val firstChild =
view.getChildAt(0) as? ImageView
?: throw JSApplicationIllegalArgumentException(
"Back button header config view should have Image as first child",
)
actionBar.setHomeAsUpIndicator(firstChild.drawable)
i++
continue
}
val params = Toolbar.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)
when (type) {
ScreenStackHeaderSubview.Type.LEFT -> {
// when there is a left item we need to disable navigation icon by default
// we also hide title as there is no other way to display left side items
if (!backButtonInCustomView) {
toolbar.navigationIcon = null
}
toolbar.title = null
params.gravity = Gravity.START
}
ScreenStackHeaderSubview.Type.RIGHT -> params.gravity = Gravity.END
ScreenStackHeaderSubview.Type.CENTER -> {
params.width = LayoutParams.MATCH_PARENT
params.gravity = Gravity.CENTER_HORIZONTAL
toolbar.title = null
}
else -> {}
}
view.layoutParams = params
(view.parent as? ViewGroup)?.removeView(view)
toolbar.addView(view)
i++
}
headerHeightUpdateProxy.updateHeaderHeightIfNeeded(this, screen)
}
private fun maybeUpdate() {
if (parent != null && !isDestroyed && screen?.isBeingRemoved == false) {
onUpdate()
}
}
fun getConfigSubview(index: Int): ScreenStackHeaderSubview = configSubviews[index]
val configSubviewsCount: Int
get() = configSubviews.size
fun removeConfigSubview(index: Int) {
configSubviews.removeAt(index)
maybeUpdate()
}
fun removeAllConfigSubviews() {
configSubviews.clear()
maybeUpdate()
}
fun addConfigSubview(
child: ScreenStackHeaderSubview,
index: Int,
) {
configSubviews.add(index, child)
maybeUpdate()
}
fun setTitle(title: String?) {
this.title = title
}
fun setTitleFontFamily(titleFontFamily: String?) {
this.titleFontFamily = titleFontFamily
}
fun setTitleFontWeight(fontWeightString: String?) {
titleFontWeight = ReactTypefaceUtils.parseFontWeight(fontWeightString)
}
fun setTitleFontSize(titleFontSize: Float) {
this.titleFontSize = titleFontSize
}
fun setTitleColor(color: Int) {
titleColor = color
}
fun setTintColor(color: Int) {
tintColor = color
}
fun setBackgroundColor(color: Int?) {
backgroundColor = color
}
fun setHideShadow(hideShadow: Boolean) {
isShadowHidden = hideShadow
}
fun setHideBackButton(hideBackButton: Boolean) {
isBackButtonHidden = hideBackButton
}
fun setHidden(hidden: Boolean) {
isHeaderHidden = hidden
}
fun setTranslucent(translucent: Boolean) {
isHeaderTranslucent = translucent
}
fun setBackButtonInCustomView(backButtonInCustomView: Boolean) {
this.backButtonInCustomView = backButtonInCustomView
}
fun setDirection(direction: String?) {
this.direction = direction
}
private class DebugMenuToolbar(
context: Context,
config: ScreenStackHeaderConfig,
) : CustomToolbar(context, config) {
override fun showOverflowMenu(): Boolean {
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
(context.applicationContext as ReactApplication)
.reactHost
?.devSupportManager
?.showDevOptionsDialog()
} else {
(context.applicationContext as ReactApplication)
.reactNativeHost
.reactInstanceManager
.showDevOptionsDialog()
}
return true
}
}
init {
visibility = GONE
toolbar =
if (BuildConfig.DEBUG) DebugMenuToolbar(context, this) else CustomToolbar(context, this)
defaultStartInset = toolbar.contentInsetStart
defaultStartInsetWithNavigation = toolbar.contentInsetStartWithNavigation
// set primary color as background by default
val tv = TypedValue()
if (context.theme.resolveAttribute(android.R.attr.colorPrimary, tv, true)) {
toolbar.setBackgroundColor(tv.data)
}
toolbar.clipChildren = false
}
companion object {
fun findTitleTextViewInToolbar(toolbar: Toolbar): TextView? {
for (i in 0 until toolbar.childCount) {
val view = toolbar.getChildAt(i)
if (view is TextView) {
if (TextUtils.equals(view.text, toolbar.title)) {
return view
}
}
}
return null
}
}
}