@apexnative/navigation 1.0.0-alpha.3 → 1.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/android/CLAUDE.md +26 -9
  3. package/android/CORE.md +32 -17
  4. package/android/RECIPES.md +9 -3
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +25 -9
  6. package/android/src/main/java/dev/apexnative/navigation/NavigationApplication.kt +34 -13
  7. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutChecks.kt +5 -1
  8. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutNode.kt +6 -2
  9. package/android/src/main/java/dev/apexnative/navigation/nativescreen/NativeScreenContext.kt +2 -2
  10. package/android/src/main/java/dev/apexnative/navigation/options/AnimationsOptions.kt +2 -2
  11. package/android/src/main/java/dev/apexnative/navigation/options/ButtonMenuOptions.kt +2 -2
  12. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +2 -2
  13. package/android/src/main/java/dev/apexnative/navigation/options/HardwareBackButtonOptions.kt +9 -1
  14. package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
  15. package/android/src/main/java/dev/apexnative/navigation/options/StackAnimationOptions.kt +40 -2
  16. package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
  17. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +6 -6
  18. package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +29 -36
  19. package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
  20. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +33 -22
  21. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +5 -4
  22. package/android/src/main/java/dev/apexnative/navigation/react/PendingCalls.kt +48 -0
  23. package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +39 -20
  24. package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +7 -4
  25. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +6 -6
  26. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +10 -5
  27. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +47 -46
  28. package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +34 -7
  29. package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationState.kt +154 -0
  30. package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +6 -4
  31. package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
  32. package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +12 -12
  33. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +4 -3
  34. package/android/src/main/java/dev/apexnative/navigation/slots/SlotViews.kt +40 -0
  35. package/android/src/main/java/dev/apexnative/navigation/utils/ImageKey.kt +66 -0
  36. package/android/src/main/java/dev/apexnative/navigation/utils/ImageLoader.kt +7 -89
  37. package/android/src/main/java/dev/apexnative/navigation/utils/ImageRequests.kt +76 -0
  38. package/android/src/main/java/dev/apexnative/navigation/utils/OptionImages.kt +262 -0
  39. package/android/src/main/java/dev/apexnative/navigation/utils/ViewController.kt +3 -1
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/ArrangementController.kt +3 -2
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/AppliedTabCalls.kt +49 -0
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +25 -17
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentViewController.kt +6 -2
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +2 -2
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenReadiness.kt +103 -56
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +5 -5
  49. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackCheck.kt +50 -0
  50. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +45 -14
  51. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +25 -8
  52. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +8 -5
  53. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +8 -8
  54. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
  55. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -1
  56. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitPanes.kt +23 -0
  57. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +61 -15
  58. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +28 -18
  59. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +25 -12
  60. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +2 -1
  61. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +85 -29
  62. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +2 -2
  63. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +7 -5
  64. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/ScreenSearch.kt +3 -5
  65. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +5 -8
  66. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarComponents.kt +5 -51
  67. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/MenuChecks.kt +13 -56
  68. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +14 -11
  69. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +56 -21
  70. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
  71. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomBottomTabItemView.kt +0 -5
  72. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
  73. package/cli/doctor/checks/app.js +61 -13
  74. package/cli/doctor/checks/splash.js +4 -22
  75. package/cli/migrate/native.js +18 -0
  76. package/cli/setup/source.js +2 -1
  77. package/cli/setup/steps/mainApplication.js +134 -19
  78. package/cli/splash/index.js +3 -1
  79. package/docs-md/architecture/transitions.md +6 -7
  80. package/docs-md/containers/modals.md +2 -2
  81. package/docs-md/guides/cli.md +1 -2
  82. package/docs-md/help/glossary.md +1 -1
  83. package/docs-md/help/troubleshooting.md +22 -5
  84. package/docs-md/index.json +4 -0
  85. package/docs-md/migration/from-react-native-navigation.md +8 -2
  86. package/docs-md/screen-ui/back-and-gestures.md +24 -2
  87. package/docs-md/start/installation.md +4 -2
  88. package/ios/CLAUDE.md +17 -5
  89. package/ios/CORE.md +18 -13
  90. package/ios/Contract/ANNCommandAnswer.h +9 -2
  91. package/ios/Contract/ANNCommandAnswer.mm +28 -30
  92. package/ios/Contract/ANNCommandContract.h +15 -0
  93. package/ios/Contract/ANNCommandContract.mm +78 -0
  94. package/ios/Core/ANNAppearanceDispatcher.h +5 -4
  95. package/ios/Core/ANNAppearanceDispatcher.mm +12 -1
  96. package/ios/Core/ANNErrorHandler.h +1 -1
  97. package/ios/Core/ANNErrorHandler.mm +3 -1
  98. package/ios/Core/ANNEventEmitter.h +1 -1
  99. package/ios/Core/ANNEventEmitter.mm +2 -2
  100. package/ios/Core/ANNJavaScriptCalls.h +34 -0
  101. package/ios/Core/ANNJavaScriptCalls.mm +61 -0
  102. package/ios/Core/ANNNavigationState.h +7 -5
  103. package/ios/Core/ANNNavigationState.mm +8 -7
  104. package/ios/Core/ANNNodeHelper.h +32 -12
  105. package/ios/Core/ANNNodeHelper.mm +100 -3
  106. package/ios/Core/ANNRenderWait.h +15 -1
  107. package/ios/Core/ANNRenderWait.mm +119 -17
  108. package/ios/Core/UIViewController+LayoutProtocol.h +9 -2
  109. package/ios/Core/UIViewController+LayoutProtocol.mm +8 -0
  110. package/ios/Layouts/Modal/ANNModalCoordinator.h +1 -1
  111. package/ios/Layouts/Modal/ANNModalCoordinator.mm +55 -14
  112. package/ios/Layouts/Root/ANNSplashViewController.h +5 -3
  113. package/ios/Layouts/Root/ANNSplashViewController.mm +147 -0
  114. package/ios/Layouts/Screen/ANNComponentViewController.h +8 -0
  115. package/ios/Layouts/Screen/ANNComponentViewController.mm +32 -5
  116. package/ios/Layouts/Screen/ANNScreenPlace.h +41 -0
  117. package/ios/Layouts/Screen/ANNScreenPlace.mm +175 -0
  118. package/ios/Layouts/SplitView/ANNSplitViewCommands.h +3 -2
  119. package/ios/Layouts/SplitView/ANNSplitViewCommands.mm +105 -39
  120. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -2
  121. package/ios/Layouts/Stack/ANNPreviewPush.h +5 -5
  122. package/ios/Layouts/Stack/ANNPreviewPush.mm +85 -22
  123. package/ios/Layouts/Stack/ANNStackCommands.mm +1 -0
  124. package/ios/Layouts/Stack/ANNStackController.mm +26 -3
  125. package/ios/Layouts/Stack/ANNStackQueue.h +3 -1
  126. package/ios/Layouts/Stack/ANNStackQueue.mm +56 -43
  127. package/ios/Layouts/Stack/UINavigationController+ANNCommands.h +4 -2
  128. package/ios/Layouts/Stack/UINavigationController+ANNCommands.mm +2 -3
  129. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.h +4 -0
  130. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.mm +11 -0
  131. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +13 -5
  132. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +2 -2
  133. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +109 -7
  134. package/ios/Options/ANNOptionsCache.h +7 -6
  135. package/ios/Options/ANNSearchBarOptions.h +4 -3
  136. package/ios/Options/ANNUserChoice.h +57 -0
  137. package/ios/Options/ANNUserChoice.mm +44 -0
  138. package/ios/Options/Params/ANNImageSource.h +61 -0
  139. package/ios/Options/Params/ANNImageSource.mm +101 -0
  140. package/ios/Options/Params/ANNOptionImages.h +37 -0
  141. package/ios/Options/Params/ANNOptionImages.mm +284 -0
  142. package/ios/Options/Params/Image.h +11 -2
  143. package/ios/Options/Params/Image.mm +15 -2
  144. package/ios/Options/Params/ImageParser.h +5 -5
  145. package/ios/Options/Params/ImageParser.mm +28 -46
  146. package/ios/Public/ApexNavigation/ApexNavigation.h +1 -1
  147. package/ios/RECIPES.md +11 -5
  148. package/ios/React/Slots/ANNSlotComponentView.mm +24 -17
  149. package/ios/React/Slots/ANNSlotContainers.h +14 -6
  150. package/ios/React/Slots/ANNSlotContainers.mm +75 -2
  151. package/ios/TopBar/ANNBarButtons.h +9 -5
  152. package/ios/TopBar/ANNBarButtons.mm +211 -63
  153. package/ios/TopBar/ANNIconButtonImage.h +1 -0
  154. package/ios/TopBar/ANNIconButtonImage.mm +34 -1
  155. package/ios/TopBar/ANNLargeTitlePresenter.h +13 -0
  156. package/ios/TopBar/ANNLargeTitlePresenter.mm +26 -3
  157. package/ios/TopBar/ANNSearchPresenter.mm +170 -19
  158. package/ios/TopBar/ANNTopBarPresenter.h +5 -0
  159. package/ios/TopBar/ANNTopBarPresenter.mm +60 -11
  160. package/ios/TurboModules/ANNTurboModule.mm +39 -21
  161. package/lib/module/ARCHITECTURE.md +18 -18
  162. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  163. package/lib/module/api/commands/commands.js +54 -6
  164. package/lib/module/api/commands/commands.js.map +1 -1
  165. package/lib/module/api/navigation.js.map +1 -1
  166. package/lib/module/engine/commands.js +9 -7
  167. package/lib/module/engine/commands.js.map +1 -1
  168. package/lib/module/engine/layout.js +13 -9
  169. package/lib/module/engine/layout.js.map +1 -1
  170. package/lib/module/engine/options.js +44 -11
  171. package/lib/module/engine/options.js.map +1 -1
  172. package/lib/module/engine/places.js +52 -7
  173. package/lib/module/engine/places.js.map +1 -1
  174. package/lib/module/engine/props.js +30 -1
  175. package/lib/module/engine/props.js.map +1 -1
  176. package/lib/module/engine/rememberedOptions.js +1 -1
  177. package/lib/module/engine/rememberedOptions.js.map +1 -1
  178. package/lib/module/engine/screens.js +22 -12
  179. package/lib/module/engine/screens.js.map +1 -1
  180. package/lib/module/interfaces/contract/commands.json +101 -0
  181. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +5 -2
  182. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  183. package/lib/typescript/api/commands/commands.d.ts.map +1 -1
  184. package/lib/typescript/api/commands/presentation.d.ts +4 -3
  185. package/lib/typescript/api/commands/presentation.d.ts.map +1 -1
  186. package/lib/typescript/api/events/navigationEvents.d.ts +1 -1
  187. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  188. package/lib/typescript/api/hooks/useNavigation.d.ts +1 -1
  189. package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
  190. package/lib/typescript/api/navigation.d.ts +4 -1
  191. package/lib/typescript/api/navigation.d.ts.map +1 -1
  192. package/lib/typescript/engine/commands.d.ts.map +1 -1
  193. package/lib/typescript/engine/options.d.ts +5 -2
  194. package/lib/typescript/engine/options.d.ts.map +1 -1
  195. package/lib/typescript/engine/places.d.ts +7 -2
  196. package/lib/typescript/engine/places.d.ts.map +1 -1
  197. package/lib/typescript/engine/props.d.ts +8 -1
  198. package/lib/typescript/engine/props.d.ts.map +1 -1
  199. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  200. package/lib/typescript/engine/screens.d.ts.map +1 -1
  201. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  202. package/lib/typescript/interfaces/ComponentEvents.d.ts.map +1 -1
  203. package/lib/typescript/interfaces/Events.d.ts +19 -10
  204. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  205. package/lib/typescript/interfaces/Options.d.ts +1 -1
  206. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  207. package/lib/typescript/interfaces/options/animations.d.ts +23 -4
  208. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  209. package/lib/typescript/interfaces/options/buttons.d.ts +13 -5
  210. package/lib/typescript/interfaces/options/buttons.d.ts.map +1 -1
  211. package/lib/typescript/interfaces/options/stack.d.ts +9 -0
  212. package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
  213. package/lib/typescript/interfaces/options/topBar.d.ts +22 -9
  214. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  215. package/package.json +1 -1
  216. package/src/ARCHITECTURE.md +18 -18
  217. package/src/adapters/NativeANNTurboModule.ts +5 -2
  218. package/src/api/commands/commands.ts +48 -7
  219. package/src/api/commands/presentation.ts +4 -3
  220. package/src/api/events/navigationEvents.ts +1 -1
  221. package/src/api/hooks/useNavigation.ts +1 -1
  222. package/src/api/navigation.ts +4 -1
  223. package/src/engine/commands.ts +19 -7
  224. package/src/engine/layout.ts +16 -9
  225. package/src/engine/options.ts +39 -11
  226. package/src/engine/places.ts +65 -7
  227. package/src/engine/props.ts +30 -1
  228. package/src/engine/rememberedOptions.ts +4 -1
  229. package/src/engine/screens.tsx +23 -13
  230. package/src/interfaces/ComponentEvents.ts +2 -2
  231. package/src/interfaces/Events.ts +19 -10
  232. package/src/interfaces/Options.ts +1 -0
  233. package/src/interfaces/contract/commands.json +101 -0
  234. package/src/interfaces/options/animations.ts +24 -4
  235. package/src/interfaces/options/buttons.ts +13 -5
  236. package/src/interfaces/options/stack.ts +9 -0
  237. package/src/interfaces/options/topBar.ts +22 -9
  238. package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationStateReporter.kt +0 -62
  239. package/android/src/main/java/dev/apexnative/navigation/react/events/StateHold.kt +0 -62
@@ -0,0 +1,262 @@
1
+ package dev.apexnative.navigation.utils
2
+
3
+ import android.content.ComponentCallbacks2
4
+ import android.content.Context
5
+ import android.content.res.Configuration
6
+ import android.content.res.Resources
7
+ import android.graphics.BitmapFactory
8
+ import android.graphics.drawable.BitmapDrawable
9
+ import android.graphics.drawable.Drawable
10
+ import android.net.Uri
11
+ import android.os.StrictMode
12
+ import android.util.LruCache
13
+ import androidx.core.content.ContextCompat
14
+ import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
15
+ import java.io.FileInputStream
16
+ import java.io.FileNotFoundException
17
+ import java.io.IOException
18
+ import java.io.InputStream
19
+ import java.net.URL
20
+ import java.util.Collections
21
+ import java.util.concurrent.ExecutorService
22
+ import java.util.concurrent.Executors
23
+
24
+ /**
25
+ * The images and icons of the options, every one of them (bar buttons and their menus, the back button, search
26
+ * suggestions, tab icons, the FAB): one cache for the app, by [ImageKey], sized by the bytes of its images and let go
27
+ * when the system is low on memory with the app in the background ([clearsOnTrim]), not each time the app's UI hides;
28
+ * each caller gets its own mutable copy. The images don't depend on the JavaScript, so a reload keeps them.
29
+ *
30
+ * A `file:` address is a file; anything else first a drawable of the app by name (as React Native's images by resource
31
+ * name: release builds' assets, `{ android }` icons). Those two load on the caller's thread ([load]) or on a
32
+ * background thread of their own ([loadLater]). Not found while the app is in development: the address is read as a
33
+ * stream on the main thread, Metro's asset over the network (network on the main thread allowed for that read only)
34
+ * or through the content resolver. A remote address that is not Metro's asset is never loaded. A failure (a missing
35
+ * file or drawable, data that doesn't decode, an image too large for the memory) never ends the app: no image, and one
36
+ * warning per address in the log (and LogBox in development).
37
+ *
38
+ * A theme attribute inside a drawable of the app is resolved against the theme of the context that loaded it first.
39
+ * Main thread, unless a member says otherwise.
40
+ */
41
+ internal object OptionImages {
42
+ private val cache = object : LruCache<ImageKey, Entry>(maxBytes()) {
43
+ override fun sizeOf(key: ImageKey, value: Entry) = value.bytes
44
+ }
45
+
46
+ // The addresses already named in a warning: one each.
47
+ private val warned: MutableSet<String> = Collections.synchronizedSet(HashSet())
48
+
49
+ // The loads off the main thread under way, with who waits for each.
50
+ private val loading = HashMap<ImageKey, MutableList<Waiter>>()
51
+
52
+ private val background: ExecutorService by lazy {
53
+ Executors.newSingleThreadExecutor { task -> Thread(task, THREAD_NAME).apply { isDaemon = true } }
54
+ }
55
+
56
+ @Volatile
57
+ private var listening = false
58
+
59
+ /** The cache and the loads for [context]: the source of an [ImageRequests]. */
60
+ fun source(context: Context): ImageSource = object : ImageSource {
61
+ override fun key(uri: String) = ImageKey.of(context, uri)
62
+
63
+ override fun cached(key: ImageKey) = cached(context, key)
64
+
65
+ override fun now(key: ImageKey) = load(context, key)
66
+
67
+ override fun loadsLater(uri: String) = loadsLater(context, uri)
68
+
69
+ override fun later(key: ImageKey, done: (Drawable?) -> Unit) = loadLater(context, key, done)
70
+ }
71
+
72
+ /** A copy of the image of [key] when the cache has it. Any thread. */
73
+ fun cached(context: Context, key: ImageKey): Drawable? = cache.get(key)?.copy(context)
74
+
75
+ /** A copy of the image of [uri], from the cache or loaded on the caller's thread; none on a failure. */
76
+ fun load(context: Context, uri: String): Drawable? = load(context, ImageKey.of(context, uri))
77
+
78
+ /**
79
+ * A copy of the image of [key] (built in the configuration of [context]), from the cache or loaded on the caller's
80
+ * thread; none on a failure.
81
+ */
82
+ fun load(context: Context, key: ImageKey): Drawable? {
83
+ listen(context)
84
+ cache.get(key)?.let { return it.copy(context) }
85
+ val loaded = decodeOrWarn(context, key.uri) ?: return null
86
+ return keep(key, loaded)?.copy(context) ?: loaded
87
+ }
88
+
89
+ /** Whether [uri] loads off the main thread: a file, or a drawable the app has by that name. Any thread. */
90
+ fun loadsLater(context: Context, uri: String): Boolean =
91
+ uri.startsWith(FILE_SCHEME) || ResourceDrawableIdHelper.getResourceDrawableId(context, uri) > 0
92
+
93
+ /**
94
+ * Loads [key] (built in the configuration of [context]) on the background thread and gives [done] a copy of it, or
95
+ * none on a failure, on the main thread; one load for all who ask for the same image meanwhile. Only for what
96
+ * [loadsLater].
97
+ */
98
+ fun loadLater(context: Context, key: ImageKey, done: (Drawable?) -> Unit) {
99
+ listen(context)
100
+ val waiters = loading[key]
101
+ if (waiters != null) {
102
+ waiters.add(Waiter(context, done))
103
+ return
104
+ }
105
+ loading[key] = mutableListOf(Waiter(context, done))
106
+ background.execute {
107
+ val loaded = decodeOrWarn(context, key.uri)
108
+ val entry = loaded?.let { keep(key, it) }
109
+ UiThread.post { deliver(key, loaded, entry) }
110
+ }
111
+ }
112
+
113
+ /** Lets every image of the cache go. Any thread. */
114
+ fun clear() {
115
+ cache.evictAll()
116
+ }
117
+
118
+ /**
119
+ * Whether the cache lets its images go at a trim of [level]: when the system is low on memory with the app in the
120
+ * background (`TRIM_MEMORY_BACKGROUND` and above), not at `TRIM_MEMORY_UI_HIDDEN`, which comes each time the app
121
+ * goes to the background: its icons are there when it comes back.
122
+ */
123
+ fun clearsOnTrim(level: Int): Boolean = level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND
124
+
125
+ // Each waiter gets a copy of its own; an image the cache can't keep (no constant state) goes to the first only,
126
+ // the others load it again.
127
+ private fun deliver(key: ImageKey, loaded: Drawable?, entry: Entry?) {
128
+ val waiters = loading.remove(key) ?: return
129
+ waiters.forEachIndexed { index, waiter ->
130
+ val image = when {
131
+ loaded == null -> null
132
+ entry != null -> entry.copy(waiter.context)
133
+ index == 0 -> loaded
134
+ else -> load(waiter.context, key)
135
+ }
136
+ waiter.done(image)
137
+ }
138
+ }
139
+
140
+ // Into the cache, when the image can be copied; the entry it keeps. Any thread.
141
+ private fun keep(key: ImageKey, image: Drawable): Entry? {
142
+ val state = image.constantState ?: return null
143
+ return Entry(state, bytesOf(image)).also { cache.put(key, it) }
144
+ }
145
+
146
+ private fun bytesOf(image: Drawable): Int {
147
+ if (image is BitmapDrawable) image.bitmap?.let { return it.allocationByteCount }
148
+ // A drawable that draws itself (a vector) keeps a picture of its own size when it is drawn.
149
+ return image.intrinsicWidth.coerceAtLeast(1) * image.intrinsicHeight.coerceAtLeast(1) * BYTES_PER_PIXEL
150
+ }
151
+
152
+ // The image of [uri], or none and one warning per address. Any thread.
153
+ private fun decodeOrWarn(context: Context, uri: String): Drawable? = try {
154
+ decode(context, uri)
155
+ } catch (e: Exception) {
156
+ warnOnce(uri, (e as? LoadFailure)?.why ?: ": ${e.message ?: e.javaClass.simpleName}")
157
+ null
158
+ } catch (e: OutOfMemoryError) {
159
+ warnOnce(uri, ": it is too large to decode")
160
+ null
161
+ }
162
+
163
+ private fun warnOnce(uri: String, why: String) {
164
+ if (warned.add(uri)) DevelopmentLog.warn("Could not load image $uri$why")
165
+ }
166
+
167
+ private fun decode(context: Context, uri: String): Drawable {
168
+ if (uri.startsWith(FILE_SCHEME)) return decodeStream(context) { FileInputStream(Uri.parse(uri).path) }
169
+ resourceDrawable(context, uri)?.let { return it }
170
+ if (!DevelopmentLog.isDevelopment) throw LoadFailure("")
171
+ val address = Uri.parse(uri)
172
+ return when (address.scheme) {
173
+ "http", "https" -> {
174
+ if (!isMetroAsset(address)) throw LoadFailure(": remote images are not supported in options")
175
+ withNetworkOnMainThread { decodeStream(context) { URL(uri).openStream() } }
176
+ }
177
+ else -> decodeStream(context) {
178
+ context.contentResolver.openInputStream(address) ?: throw FileNotFoundException(uri)
179
+ }
180
+ }
181
+ }
182
+
183
+ // A drawable of the app by name; none when the app has none by that name.
184
+ private fun resourceDrawable(context: Context, name: String): Drawable? {
185
+ val id = ResourceDrawableIdHelper.getResourceDrawableId(context, name)
186
+ if (id <= 0) return null
187
+ return try {
188
+ ContextCompat.getDrawable(context, id)
189
+ } catch (e: Resources.NotFoundException) {
190
+ null
191
+ }
192
+ }
193
+
194
+ // The image of the stream [open] gives, closed after the read, also on an error; one it can't decode fails, and so
195
+ // does one too large for the memory.
196
+ private fun decodeStream(context: Context, open: () -> InputStream): Drawable {
197
+ val decoded = try {
198
+ open().use { BitmapFactory.decodeStream(it) }
199
+ } catch (e: OutOfMemoryError) {
200
+ throw LoadFailure(": it is too large to decode")
201
+ }
202
+ val bitmap = decoded ?: throw LoadFailure(": it can't be decoded")
203
+ return BitmapDrawable(context.resources, bitmap)
204
+ }
205
+
206
+ // Metro's asset: an address of the server the JavaScript bundle came from, under `/assets/`.
207
+ private fun isMetroAsset(address: Uri): Boolean {
208
+ val bundle = DevelopmentLog.bundleUrl?.let(Uri::parse) ?: return false
209
+ return address.host == bundle.host && address.port == bundle.port &&
210
+ address.path.orEmpty().startsWith(ASSETS_PATH)
211
+ }
212
+
213
+ // Reading from the network on the main thread for [read] only; the thread's policy comes back, also on an error.
214
+ private fun <T> withNetworkOnMainThread(read: () -> T): T {
215
+ val policy = StrictMode.getThreadPolicy()
216
+ StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder(policy).permitNetwork().build())
217
+ try {
218
+ return read()
219
+ } finally {
220
+ StrictMode.setThreadPolicy(policy)
221
+ }
222
+ }
223
+
224
+ // The cache lets its images go when the system is low on memory ([clearsOnTrim]): they load again when asked.
225
+ private fun listen(context: Context) {
226
+ if (listening) return
227
+ synchronized(this) {
228
+ if (listening) return
229
+ listening = true
230
+ }
231
+ context.applicationContext.registerComponentCallbacks(object : ComponentCallbacks2 {
232
+ override fun onTrimMemory(level: Int) {
233
+ if (clearsOnTrim(level)) clear()
234
+ }
235
+
236
+ override fun onConfigurationChanged(newConfig: Configuration) = Unit
237
+
238
+ @Deprecated("Android 14 deprecates it; onTrimMemory comes with it")
239
+ override fun onLowMemory() = clear()
240
+ })
241
+ }
242
+
243
+ // A sixteenth of the app's heap, at most 16 MB: the options' icons are small.
244
+ private fun maxBytes(): Int = (Runtime.getRuntime().maxMemory() / HEAP_PART).coerceAtMost(MAX_BYTES).toInt()
245
+
246
+ // An image the cache keeps: what makes its copies, and its size.
247
+ private class Entry(val state: Drawable.ConstantState, val bytes: Int) {
248
+ fun copy(context: Context): Drawable = state.newDrawable(context.resources).mutate()
249
+ }
250
+
251
+ private class Waiter(val context: Context, val done: (Drawable?) -> Unit)
252
+
253
+ // A failure the loader names itself: `Could not load image <source>`, then [why].
254
+ private class LoadFailure(val why: String) : IOException("Could not load image$why")
255
+
256
+ private const val FILE_SCHEME = "file:"
257
+ private const val ASSETS_PATH = "/assets/"
258
+ private const val THREAD_NAME = "ApexNavigation images"
259
+ private const val BYTES_PER_PIXEL = 4
260
+ private const val HEAP_PART = 16L
261
+ private const val MAX_BYTES = 16L * 1024 * 1024
262
+ }
@@ -59,7 +59,9 @@ private fun ViewController<*>.hostIsShown(): Boolean {
59
59
  * Runs [mounted] as the content of this screen's slot mounts into its container, and [start] on the pre-draw of that
60
60
  * frame: after React Native applied the whole mount, before the frame is drawn. At once if the content has mounted,
61
61
  * after [DRAW_WAIT_LIMIT] if it never does; while [waiting] holds. A screen prepared for its push was rendered at its
62
- * size and insets before: in that frame it is whole.
62
+ * size and insets before, so it is whole from that frame on; its push doesn't start in that frame: [start] hands it to
63
+ * the stack's queue, which starts the transition on a later turn, behind the calls JavaScript made before
64
+ * (`PendingCalls`), so the `mergeOptions` of the mount's commit has applied.
63
65
  */
64
66
  fun ViewController<*>.whenContentMounted(waiting: () -> Boolean, mounted: () -> Unit, start: () -> Unit) {
65
67
  var done = false
@@ -10,6 +10,7 @@ import dev.apexnative.navigation.NavigationActivity
10
10
  import dev.apexnative.navigation.options.DefaultOptions
11
11
  import dev.apexnative.navigation.options.Options
12
12
  import dev.apexnative.navigation.react.ArrangementStates
13
+ import dev.apexnative.navigation.react.events.EventEmitter
13
14
  import dev.apexnative.navigation.utils.SystemUiUtils
14
15
  import dev.apexnative.navigation.utils.WindowFolds
15
16
  import dev.apexnative.navigation.viewcontrollers.child.ChildController
@@ -146,11 +147,11 @@ class ArrangementController internal constructor(
146
147
  val single = plan.layout == ArrangementPlan.Layout.SINGLE
147
148
  if (primary is ComponentViewController) {
148
149
  val state = ArrangementStates.State(PRIMARY, layout, front = overlay, hidden = false)
149
- ArrangementStates.set(activity, primary.id, state, notify)
150
+ ArrangementStates.set(primary.id, state, notify, EventEmitter.of(primary.jsGeneration))
150
151
  }
151
152
  if (secondary is ComponentViewController) {
152
153
  val state = ArrangementStates.State(SECONDARY, layout, front = false, hidden = single)
153
- ArrangementStates.set(activity, secondary.id, state, notify)
154
+ ArrangementStates.set(secondary.id, state, notify, EventEmitter.of(secondary.jsGeneration))
154
155
  }
155
156
  }
156
157
 
@@ -0,0 +1,49 @@
1
+ package dev.apexnative.navigation.viewcontrollers.bottomtabs
2
+
3
+ import dev.apexnative.navigation.utils.ImageKey
4
+
5
+ /**
6
+ * The last call of each kind for each item of the bar, by which [BottomTabsPresenter.run] skips a call equal to it: an
7
+ * icon isn't loaded and set again as a screen appears. The badge and the font always go (their defaults are read at
8
+ * each set). An icon whose load gave no image goes again ([failed]); a `file:` icon compares by its [ImageKey] (the
9
+ * file's modification time and size, read by [fileKey]), so a file written again at the same path shows. Main thread.
10
+ */
11
+ internal class AppliedTabCalls(private val fileKey: (String) -> ImageKey) {
12
+ /** A call that runs: [isChange], the item had a call of its kind before; [image], the key of a `file:` icon. */
13
+ class Next(val isChange: Boolean, val image: ImageKey?)
14
+
15
+ private class Applied(val call: TabItems.Call, val image: ImageKey?, var failed: Boolean = false)
16
+
17
+ private val last = HashMap<Pair<Int, Class<*>>, Applied>()
18
+
19
+ /** How [call] runs, and it is the last of its kind for its item from now on; none when it is skipped. */
20
+ fun next(call: TabItems.Call): Next? {
21
+ val key = call.index to call.javaClass
22
+ val before = last[key]
23
+ val image = uriOf(call)?.takeIf { it.startsWith(FILE_SCHEME) }?.let(fileKey)
24
+ val always = call is TabItems.Call.Badge || call is TabItems.Call.Font
25
+ if (!always && before != null && before.call == call && !before.failed && before.image == image) return null
26
+ last[key] = Applied(call, image)
27
+ return Next(isChange = before != null, image = image)
28
+ }
29
+
30
+ /** The icon of [call] didn't load: the same call goes again. */
31
+ fun failed(call: TabItems.Call) {
32
+ last[call.index to call.javaClass]?.takeIf { it.call == call }?.failed = true
33
+ }
34
+
35
+ /** The bar goes: every call goes again. */
36
+ fun clear() {
37
+ last.clear()
38
+ }
39
+
40
+ private fun uriOf(call: TabItems.Call): String? = when (call) {
41
+ is TabItems.Call.Icon -> call.uri
42
+ is TabItems.Call.SelectedIcon -> call.uri
43
+ else -> null
44
+ }
45
+
46
+ private companion object {
47
+ const val FILE_SCHEME = "file:"
48
+ }
49
+ }
@@ -26,14 +26,12 @@ import dev.apexnative.navigation.utils.scrollToTop
26
26
  import dev.apexnative.navigation.viewcontrollers.child.ChildControllersRegistry
27
27
  import dev.apexnative.navigation.viewcontrollers.component.ComponentViewController
28
28
  import dev.apexnative.navigation.viewcontrollers.component.RestPlace
29
- import dev.apexnative.navigation.viewcontrollers.component.ScreenReadiness
30
29
  import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackState
31
30
  import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
32
31
  import dev.apexnative.navigation.viewcontrollers.parent.ContainerBackground
33
32
  import dev.apexnative.navigation.viewcontrollers.parent.ParentController
34
33
  import dev.apexnative.navigation.viewcontrollers.stack.StackController
35
34
  import dev.apexnative.navigation.viewcontrollers.stack.fab.FabPlacement
36
- import dev.apexnative.navigation.viewcontrollers.stack.topbar.BarComponents
37
35
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
38
36
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
39
37
  import dev.apexnative.navigation.views.animations.FadeThrough
@@ -74,6 +72,10 @@ class BottomTabsController(
74
72
  private var pendingSelected: (() -> Unit)? = null
75
73
  // Ends the fade through of a switch still running, with its events.
76
74
  private var endSwitch: (() -> Unit)? = null
75
+
76
+ // The made tabs that didn't show at the last repaint (new default options, a light/dark switch): each is repainted
77
+ // as it is selected, not while hidden.
78
+ private val staleTabs = mutableSetOf<ViewController<*>>()
77
79
  // The bar goes with a stack's transition: the options of the new top don't move it themselves.
78
80
  private var riding = false
79
81
  private var restAttached = false
@@ -247,7 +249,7 @@ class BottomTabsController(
247
249
  // `mergeOptions` with the tabs' id: their own options, then what they show, then the layouts around without
248
250
  // `bottomTabs`; the one-shot options act once, also while the view isn't shown.
249
251
  override fun mergeOptions(options: Options) {
250
- this.options = this.options.copy().mergeWith(options)
252
+ mergeOwnOptions(options)
251
253
  applyMerge(options, null)
252
254
  performOnParentController { it.mergeChildOptions(withoutBottomTabs(options), this) }
253
255
  }
@@ -311,8 +313,10 @@ class BottomTabsController(
311
313
  private enum class How { Press, Code, Back }
312
314
 
313
315
  /**
314
- * A tap on the tab at [index], on the bar or on its cell of the custom row: its `ANN.BottomTabPressed` first, then
315
- * the selection, the repeat, or nothing, as its resolved options say.
316
+ * A tap on the tab at [index], on the bar or on its cell of the custom row, the one path of both: a switch still
317
+ * running ends first, with its `ANN.BottomTabSelected`; then this press's `ANN.BottomTabPressed`, then the
318
+ * selection (`ANN.BottomTabSelected` after the new screen's DidAppear), the repeat (the pop's own events), or
319
+ * nothing, as its resolved options say. The same order as `-[ANNBottomTabsController pressTabAtIndex:source:]`.
316
320
  */
317
321
  internal fun onTabPressed(index: Int) {
318
322
  if (isDestroyed || index !in tabs.indices) return
@@ -363,6 +367,9 @@ class BottomTabsController(
363
367
  }
364
368
  selectedIndex = index
365
369
  attach(index)
370
+ // A tab hidden at a repaint takes it now, before its screens appear: its bar's parts, its search's and its
371
+ // FAB's colors follow the theme and the defaults only through a repaint.
372
+ if (staleTabs.remove(shown)) Navigator.repaintLayer(shown)
366
373
  peekView()?.bar?.let {
367
374
  it.setCurrentItem(index, false)
368
375
  it.onCustomItemViewSelectionChanged(index)
@@ -370,6 +377,9 @@ class BottomTabsController(
370
377
  // A layer that waits for the tabs waits for the tab they show now.
371
378
  navigator?.shownScreensChanged(this)
372
379
  if (onScreen) AppearanceDispatcher.beginTransition(listOf(shown))
380
+ // From code and by back the state goes at once, as on iOS: `selectTab` resolves on it, and the command after
381
+ // it must act in this tab, not in the one still fading out.
382
+ if (how != How.Press) AppearanceDispatcher.reportState()
373
383
  if (views == null) return switched(index, previous, how, onScreen, hasViews = false)
374
384
  endSwitch = views.switch(shown, left, animated = onScreen) {
375
385
  endSwitch = null
@@ -381,8 +391,8 @@ class BottomTabsController(
381
391
  * The switch to the tab at [index] from [previous] ended: what went disappears, what came appears — unless the tabs
382
392
  * stopped showing during the fade (a modal over them): it appears when they show again; then for a press
383
393
  * `ANN.BottomTabSelected` right after the new screen's DidAppear and the left tab's `popToRootOnBlur`, with the state
384
- * as one update after them; from code and by back, `popToRootOnBlur` and the state at once. [onScreen]: the switch
385
- * started on screen.
394
+ * as one update after them; from code and by back, `popToRootOnBlur` and the state, which went at the start too
395
+ * ([select]). [onScreen]: the switch started on screen.
386
396
  */
387
397
  private fun switched(index: Int, previous: Int, how: How, onScreen: Boolean, hasViews: Boolean) {
388
398
  val left = tabs[previous]
@@ -592,8 +602,8 @@ class BottomTabsController(
592
602
  }
593
603
  }
594
604
 
595
- // The repaint: the bar, the items of every tab, the row, then the made tabs that don't show, which the pass over
596
- // what shows doesn't reach.
605
+ // The repaint: the bar, the items of every tab, the row. Only what shows is repainted (the pass over what shows
606
+ // reaches the selected tab); a made tab that doesn't show is repainted when it is selected ([staleTabs]).
597
607
  internal fun repaint(options: Options) {
598
608
  if (isDestroyed) return
599
609
  ContainerBackground.apply(this, options)
@@ -603,7 +613,7 @@ class BottomTabsController(
603
613
  if (usesRow) layout.bar.customRowOptions = options.bottomTabs.customRow.copy()
604
614
  }
605
615
  cells.forEachIndexed { index, cell -> cell.update(cellState(index)) }
606
- tabs.forEach { if (it !== currentChild && it.isViewCreated) Navigator.repaintLayer(it) }
616
+ tabs.forEach { if (it !== currentChild && it.isViewCreated) staleTabs += it }
607
617
  }
608
618
 
609
619
  // The custom row: the cells first into the bar's external host, then the row holds them; its look follows the tabs'
@@ -651,15 +661,12 @@ class BottomTabsController(
651
661
  private fun makeCell(index: Int): CustomBottomTabItemView {
652
662
  val component = itemOptions(index).component
653
663
  val componentId = component.componentId.get(null) ?: "$id.$index"
654
- ScreenReadiness.setRendersAtOnce(componentId, true)
655
- // Another live root has the id (the same layout set again before the old one went): what it rendered isn't
656
- // this one's; the id's readiness goes only with its last root (as the bar's roots).
657
- if (BarComponents.isLive(componentId)) ScreenReadiness.forgetRender(componentId)
658
- BarComponents.rootMade(componentId)
659
664
  val cell = CustomBottomTabItemView(activity)
660
665
  val state = cellState(index)
661
- cell.show(ReactView(activity, componentId, component.name.get(""), cell.bundleOf(state)), state)
662
- cell.onDestroyed = { if (BarComponents.rootStopped(componentId)) ScreenReadiness.remove(componentId) }
666
+ // Another live root may have the id (the same layout set again before the old one went): each answers by the
667
+ // render of its own surface, and the id's readiness goes with its last root.
668
+ val root = ReactView(activity, componentId, component.name.get(""), cell.bundleOf(state), rendersAtOnce = true)
669
+ cell.show(root, state)
663
670
  return cell
664
671
  }
665
672
 
@@ -681,6 +688,7 @@ class BottomTabsController(
681
688
  override fun destroy() {
682
689
  if (isDestroyed) return
683
690
  pendingSelected = null
691
+ staleTabs.clear()
684
692
  super.destroy()
685
693
  // A switch still running ends with its views at rest, without events: the tabs are gone.
686
694
  endSwitch?.invoke()
@@ -6,7 +6,6 @@ import android.animation.AnimatorSet
6
6
  import android.animation.ObjectAnimator
7
7
  import android.app.Activity
8
8
  import android.graphics.Color
9
- import android.graphics.drawable.Drawable
10
9
  import android.view.View
11
10
  import android.view.animation.LinearInterpolator
12
11
  import android.view.animation.PathInterpolator
@@ -20,8 +19,8 @@ import dev.apexnative.navigation.options.params.LayoutDirection
20
19
  import dev.apexnative.navigation.options.params.ThemeColour
21
20
  import dev.apexnative.navigation.options.parsers.TypefaceLoader
22
21
  import dev.apexnative.navigation.system.SystemUiOwner
23
- import dev.apexnative.navigation.utils.ImageLoader
24
- import dev.apexnative.navigation.utils.ImageLoadingListenerAdapter
22
+ import dev.apexnative.navigation.utils.ImageRequests
23
+ import dev.apexnative.navigation.utils.OptionImages
25
24
  import dev.apexnative.navigation.utils.TestIds
26
25
  import dev.apexnative.navigation.utils.UiUtils
27
26
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
@@ -67,8 +66,9 @@ internal open class BottomTabsPresenter(activity: Activity) : Presenter(activity
67
66
 
68
67
  private var layout: BottomTabsLayout? = null
69
68
  private val typefaces = TypefaceLoader(activity)
70
- private val icons = HashMap<String, Drawable>()
71
- private val applied = HashMap<Pair<Int, Class<*>>, TabItems.Call>()
69
+ // The icons of the items by item and kind (selected or not): those of the cache at once, the others as below.
70
+ private val icons = ImageRequests<Pair<Int, Boolean>>(OptionImages.source(activity))
71
+ private val applied = AppliedTabCalls(icons::key)
72
72
  private var motion: Animator? = null
73
73
  private var anyIcon = false
74
74
  private var testId: String? = null
@@ -100,25 +100,24 @@ internal open class BottomTabsPresenter(activity: Activity) : Presenter(activity
100
100
 
101
101
  /**
102
102
  * Runs [calls] on the bar's items; a call equal to the last of its kind for its item is skipped (an icon isn't
103
- * loaded and set again as a screen appears). The badge always goes: its default colors are read at each set.
103
+ * loaded and set again as a screen appears), except the badge and the font, an icon that didn't load and a `file:`
104
+ * icon whose file changed ([AppliedTabCalls]).
104
105
  */
105
106
  open fun run(calls: List<TabItems.Call>) {
106
107
  val bar = layout?.bar ?: return
107
108
  for (call in calls) {
108
109
  if (call is TabItems.Call.TestId && !itemTestIds) continue
109
- val key = call.index to call.javaClass
110
- if (call !is TabItems.Call.Badge && call !is TabItems.Call.Font && applied[key] == call) continue
111
- applied[key] = call
112
- run(bar, call)
110
+ val next = applied.next(call) ?: continue
111
+ run(bar, call, next)
113
112
  }
114
113
  }
115
114
 
116
- private fun run(bar: BottomTabs, call: TabItems.Call) {
115
+ private fun run(bar: BottomTabs, call: TabItems.Call, next: AppliedTabCalls.Next) {
117
116
  val index = call.index
118
117
  when (call) {
119
118
  is TabItems.Call.Title -> bar.setText(index, call.text)
120
- is TabItems.Call.Icon -> iconOf(call.uri)?.let { bar.setIcon(index, it) }
121
- is TabItems.Call.SelectedIcon -> iconOf(call.uri)?.let { bar.setSelectedIcon(index, it) }
119
+ is TabItems.Call.Icon -> icon(bar, call, call.uri, false, next)
120
+ is TabItems.Call.SelectedIcon -> icon(bar, call, call.uri, true, next)
122
121
  is TabItems.Call.Colors -> {
123
122
  bar.setIconActiveColor(index, call.iconActive)
124
123
  bar.setIconInactiveColor(index, call.iconInactive)
@@ -344,17 +343,19 @@ internal open class BottomTabsPresenter(activity: Activity) : Presenter(activity
344
343
 
345
344
  private fun colorOf(colour: ThemeColour): Int? = colour.get(activity, null)
346
345
 
347
- // Icons load as they apply and never throw: an icon that doesn't load leaves the item as it was.
348
- private fun iconOf(uri: String): Drawable? {
349
- icons[uri]?.let { return it.constantState?.newDrawable()?.mutate() ?: it }
350
- var loaded: Drawable? = null
351
- ImageLoader().loadIcon(activity, uri, object : ImageLoadingListenerAdapter() {
352
- override fun onComplete(drawable: Drawable) {
353
- loaded = drawable
354
- }
355
- })
356
- loaded?.let { icons[uri] = it }
357
- return loaded
346
+ // Icons never throw: one that doesn't load leaves the item without it (without its selected icon, the item shows
347
+ // its icon when selected). An item's first icon loads at once, its tab's first frame shows it; a change of it on
348
+ // the bar that shows loads off the main thread when it can, and the item takes it when ready, unless a newer icon
349
+ // of the item came first.
350
+ // The key of a `file:` icon was built for the comparison: the request takes it, its file read once.
351
+ private fun icon(bar: BottomTabs, call: TabItems.Call, uri: String, selected: Boolean, next: AppliedTabCalls.Next) {
352
+ val index = call.index
353
+ val key = next.image ?: icons.key(uri)
354
+ icons.request(index to selected, key, mayWait = next.isChange && bar.isAttachedToWindow) { icon ->
355
+ if (icon == null) applied.failed(call)
356
+ val shown = layout?.bar ?: return@request
357
+ if (selected) shown.setSelectedIcon(index, icon) else shown.setIcon(index, icon)
358
+ }
358
359
  }
359
360
 
360
361
  private companion object {
@@ -14,6 +14,7 @@ import dev.apexnative.navigation.react.ScreenInsets
14
14
  import dev.apexnative.navigation.react.ScreenRegions
15
15
  import dev.apexnative.navigation.react.events.AppearanceDispatcher
16
16
  import dev.apexnative.navigation.react.events.ComponentType
17
+ import dev.apexnative.navigation.react.events.EventEmitter
17
18
  import dev.apexnative.navigation.slots.SlotScreenView
18
19
  import dev.apexnative.navigation.system.SystemUiOwner
19
20
  import dev.apexnative.navigation.viewcontrollers.child.ChildController
@@ -177,8 +178,11 @@ class ComponentViewController(
177
178
  topFree = parent?.leavesTopEdgeFree(this) == true,
178
179
  bottomFree = parent?.leavesBottomEdgeFree(this) == true,
179
180
  )
180
- ScreenInsets.update(activity, id, insets, ScreenSafeArea.padding(insets, edges))
181
- ScreenRegions.update(activity, id, Rect(place.left, place.top, place.right, place.bottom), windowInsets)
181
+ // The events of the screen go to the JavaScript it was made for, and to no other.
182
+ val emitter = EventEmitter.of(jsGeneration)
183
+ ScreenInsets.update(id, insets, ScreenSafeArea.padding(insets, edges), emitter)
184
+ val area = Rect(place.left, place.top, place.right, place.bottom)
185
+ ScreenRegions.update(activity, id, area, windowInsets, emitter)
182
186
  }
183
187
 
184
188
  // A layout asked for on the content or on a view around it: it isn't in its place yet.
@@ -4,8 +4,8 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
4
4
 
5
5
  /**
6
6
  * Whether a screen's content rendered, which a command waits for before the screen comes in: a new root (`setRoot`),
7
- * a push with `waitForRender: true`, the new top of a `setStackRoot`. A wait never listens to the readiness store
8
- * itself: the content keeps the one listener of its id and tells every waiter. Main thread only.
7
+ * a push (unless `waitForRender: false`), the new top of a `setStackRoot`. A wait never listens to the readiness store
8
+ * itself: the content keeps its own listener of its id and tells every waiter. Main thread only.
9
9
  */
10
10
  internal interface Readiness {
11
11
  /** The content of [screen] rendered; a screen without content to wait for is ready. */