/
ScreenFragment.kt
334 lines (296 loc) · 13.6 KB
/
ScreenFragment.kt
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
package com.swmansion.rnscreens
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewParent
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.EventDispatcher
import com.swmansion.rnscreens.events.HeaderBackButtonClickedEvent
import com.swmansion.rnscreens.events.ScreenAppearEvent
import com.swmansion.rnscreens.events.ScreenDisappearEvent
import com.swmansion.rnscreens.events.ScreenDismissedEvent
import com.swmansion.rnscreens.events.ScreenTransitionProgressEvent
import com.swmansion.rnscreens.events.ScreenWillAppearEvent
import com.swmansion.rnscreens.events.ScreenWillDisappearEvent
import kotlin.math.max
import kotlin.math.min
open class ScreenFragment : Fragment {
enum class ScreenLifecycleEvent {
Appear, WillAppear, Disappear, WillDisappear
}
// if we call empty constructor, there is no screen to be assigned so not sure why it is suggested
@Suppress("JoinDeclarationAndAssignment")
lateinit var screen: Screen
private val mChildScreenContainers: MutableList<ScreenContainer<*>> = ArrayList()
private var shouldUpdateOnResume = false
// if we don't set it, it will be 0.0f at the beginning so the progress will not be sent
// due to progress value being already 0.0f
private var mProgress = -1f
// those 2 vars are needed since sometimes the events would be dispatched twice in child containers
// (should only happen if parent has `NONE` animation) and we don't need too complicated logic.
// We just check if, after the event was dispatched, its "counter-event" has been also dispatched before sending the same event again.
// We do it for 'willAppear' -> 'willDisappear' and 'appear' -> 'disappear'
private var canDispatchWillAppear = true
private var canDispatchAppear = true
// we want to know if we are currently transitioning in order not to fire lifecycle events
// in nested fragments. See more explanation in dispatchViewAnimationEvent
private var isTransitioning = false
constructor() {
throw IllegalStateException(
"Screen fragments should never be restored. Follow instructions from https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704067 to properly configure your main activity."
)
}
@SuppressLint("ValidFragment")
constructor(screenView: Screen) : super() {
screen = screenView
}
override fun onResume() {
super.onResume()
if (shouldUpdateOnResume) {
shouldUpdateOnResume = false
ScreenWindowTraits.trySetWindowTraits(screen, tryGetActivity(), tryGetContext())
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val wrapper = context?.let { ScreensFrameLayout(it) }
val params = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
)
screen.layoutParams = params
wrapper?.addView(recycleView(screen))
return wrapper
}
private class ScreensFrameLayout(
context: Context,
) : FrameLayout(context) {
/**
* This method implements a workaround for RN's autoFocus functionality. Because of the way
* autoFocus is implemented it dismisses soft keyboard in fragment transition
* due to change of visibility of the view at the start of the transition. Here we override the
* call to `clearFocus` when the visibility of view is `INVISIBLE` since `clearFocus` triggers the
* hiding of the keyboard in `ReactEditText.java`.
*/
override fun clearFocus() {
if (visibility != INVISIBLE) {
super.clearFocus()
}
}
}
open fun onContainerUpdate() {
updateWindowTraits()
}
private fun updateWindowTraits() {
val activity: Activity? = activity
if (activity == null) {
shouldUpdateOnResume = true
return
}
ScreenWindowTraits.trySetWindowTraits(screen, activity, tryGetContext())
}
fun tryGetActivity(): Activity? {
activity?.let { return it }
val context = screen.context
if (context is ReactContext && context.currentActivity != null) {
return context.currentActivity
}
var parent: ViewParent? = screen.container
while (parent != null) {
if (parent is Screen) {
val fragment = parent.fragment
fragment?.activity?.let { return it }
}
parent = parent.parent
}
return null
}
fun tryGetContext(): ReactContext? {
if (context is ReactContext) {
return context as ReactContext
}
if (screen.context is ReactContext) {
return screen.context as ReactContext
}
var parent: ViewParent? = screen.container
while (parent != null) {
if (parent is Screen) {
if (parent.context is ReactContext) {
return parent.context as ReactContext
}
}
parent = parent.parent
}
return null
}
val childScreenContainers: List<ScreenContainer<*>>
get() = mChildScreenContainers
private fun canDispatchEvent(event: ScreenLifecycleEvent): Boolean {
return when (event) {
ScreenLifecycleEvent.WillAppear -> canDispatchWillAppear
ScreenLifecycleEvent.Appear -> canDispatchAppear
ScreenLifecycleEvent.WillDisappear -> !canDispatchWillAppear
ScreenLifecycleEvent.Disappear -> !canDispatchAppear
}
}
private fun setLastEventDispatched(event: ScreenLifecycleEvent) {
when (event) {
ScreenLifecycleEvent.WillAppear -> canDispatchWillAppear = false
ScreenLifecycleEvent.Appear -> canDispatchAppear = false
ScreenLifecycleEvent.WillDisappear -> canDispatchWillAppear = true
ScreenLifecycleEvent.Disappear -> canDispatchAppear = true
}
}
private fun dispatchOnWillAppear() {
dispatchEvent(ScreenLifecycleEvent.WillAppear, this)
dispatchTransitionProgress(0.0f, false)
}
private fun dispatchOnAppear() {
dispatchEvent(ScreenLifecycleEvent.Appear, this)
dispatchTransitionProgress(1.0f, false)
}
private fun dispatchOnWillDisappear() {
dispatchEvent(ScreenLifecycleEvent.WillDisappear, this)
dispatchTransitionProgress(0.0f, true)
}
private fun dispatchOnDisappear() {
dispatchEvent(ScreenLifecycleEvent.Disappear, this)
dispatchTransitionProgress(1.0f, true)
}
private fun dispatchEvent(event: ScreenLifecycleEvent, fragment: ScreenFragment) {
if (fragment is ScreenStackFragment && fragment.canDispatchEvent(event)) {
fragment.screen.let {
fragment.setLastEventDispatched(event)
val lifecycleEvent: Event<*> = when (event) {
ScreenLifecycleEvent.WillAppear -> ScreenWillAppearEvent(it.id)
ScreenLifecycleEvent.Appear -> ScreenAppearEvent(it.id)
ScreenLifecycleEvent.WillDisappear -> ScreenWillDisappearEvent(it.id)
ScreenLifecycleEvent.Disappear -> ScreenDisappearEvent(it.id)
}
val screenContext = screen.context as ReactContext
val eventDispatcher: EventDispatcher? =
UIManagerHelper.getEventDispatcherForReactTag(screenContext, screen.id)
eventDispatcher?.dispatchEvent(lifecycleEvent)
fragment.dispatchEventInChildContainers(event)
}
}
}
private fun dispatchEventInChildContainers(event: ScreenLifecycleEvent) {
for (sc in mChildScreenContainers) {
if (sc.screenCount > 0) {
sc.topScreen?.let {
sc.topScreen?.fragment?.let { fragment -> dispatchEvent(event, fragment) }
}
}
}
}
fun dispatchHeaderBackButtonClickedEvent() {
val screenContext = screen.context as ReactContext
val eventDispatcher: EventDispatcher? =
UIManagerHelper.getEventDispatcherForReactTag(screenContext, screen.id)
eventDispatcher?.dispatchEvent(HeaderBackButtonClickedEvent(screen.id))
}
fun dispatchTransitionProgress(alpha: Float, closing: Boolean) {
if (this is ScreenStackFragment) {
if (mProgress != alpha) {
mProgress = max(0.0f, min(1.0f, alpha))
/* We want value of 0 and 1 to be always dispatched so we base coalescing key on the progress:
- progress is 0 -> key 1
- progress is 1 -> key 2
- progress is between 0 and 1 -> key 3
*/
val coalescingKey = (if (mProgress == 0.0f) 1 else if (mProgress == 1.0f) 2 else 3).toShort()
val container: ScreenContainer<*>? = screen.container
val goingForward = if (container is ScreenStack) container.goingForward else false
val screenContext = screen.context as ReactContext
val eventDispatcher: EventDispatcher? =
UIManagerHelper.getEventDispatcherForReactTag(screenContext, screen.id)
eventDispatcher?.dispatchEvent(
ScreenTransitionProgressEvent(
screen.id, mProgress, closing, goingForward, coalescingKey
)
)
}
}
}
fun registerChildScreenContainer(screenContainer: ScreenContainer<*>) {
mChildScreenContainers.add(screenContainer)
}
fun unregisterChildScreenContainer(screenContainer: ScreenContainer<*>) {
mChildScreenContainers.remove(screenContainer)
}
fun onViewAnimationStart() {
dispatchViewAnimationEvent(false)
}
open fun onViewAnimationEnd() {
dispatchViewAnimationEvent(true)
}
private fun dispatchViewAnimationEvent(animationEnd: Boolean) {
isTransitioning = !animationEnd
// if parent fragment is transitioning, we do not want the events dispatched from the child,
// since we subscribe to parent's animation start/end and dispatch events in child from there
// check for `isTransitioning` should be enough since the child's animation should take only
// 20ms due to always being `StackAnimation.NONE` when nested stack being pushed
val parent = parentFragment
if (parent == null || (parent is ScreenFragment && !parent.isTransitioning)) {
// onViewAnimationStart/End is triggered from View#onAnimationStart/End method of the fragment's root
// view. We override an appropriate method of the StackFragment's
// root view in order to achieve this.
if (isResumed) {
// Android dispatches the animation start event for the fragment that is being added first
// however we want the one being dismissed first to match iOS. It also makes more sense from
// a navigation point of view to have the disappear event first.
// Since there are no explicit relationships between the fragment being added / removed the
// practical way to fix this is delaying dispatching the appear events at the end of the
// frame.
UiThreadUtil.runOnUiThread {
if (animationEnd) dispatchOnAppear() else dispatchOnWillAppear()
}
} else {
if (animationEnd) dispatchOnDisappear() else dispatchOnWillDisappear()
}
}
}
override fun onDestroy() {
super.onDestroy()
val container = screen.container
if (container == null || !container.hasScreen(this)) {
// we only send dismissed even when the screen has been removed from its container
val screenContext = screen.context
if (screenContext is ReactContext) {
val eventDispatcher: EventDispatcher? =
UIManagerHelper.getEventDispatcherForReactTag(screenContext, screen.id)
eventDispatcher?.dispatchEvent(ScreenDismissedEvent(screen.id))
}
}
mChildScreenContainers.clear()
}
companion object {
@JvmStatic
protected fun recycleView(view: View): View {
// screen fragments reuse view instances instead of creating new ones. In order to reuse a given
// view it needs to be detached from the view hierarchy to allow the fragment to attach it back.
val parent = view.parent
if (parent != null) {
(parent as ViewGroup).endViewTransition(view)
parent.removeView(view)
}
// view detached from fragment manager get their visibility changed to GONE after their state is
// dumped. Since we don't restore the state but want to reuse the view we need to change
// visibility back to VISIBLE in order for the fragment manager to animate in the view.
view.visibility = View.VISIBLE
return view
}
}
}