@apexnative/navigation 1.0.0-alpha.3 → 1.0.0-alpha.4
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.
- package/CHANGELOG.md +9 -0
- package/android/CLAUDE.md +17 -7
- package/android/CORE.md +8 -3
- package/android/RECIPES.md +9 -3
- package/android/src/main/java/dev/apexnative/navigation/NavigationApplication.kt +34 -13
- package/android/src/main/java/dev/apexnative/navigation/layout/LayoutChecks.kt +5 -1
- package/android/src/main/java/dev/apexnative/navigation/layout/LayoutNode.kt +6 -2
- package/android/src/main/java/dev/apexnative/navigation/nativescreen/NativeScreenContext.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/options/ButtonMenuOptions.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
- package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
- package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +2 -24
- package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
- package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +27 -19
- package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +5 -4
- package/android/src/main/java/dev/apexnative/navigation/react/PendingCalls.kt +48 -0
- package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +17 -7
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +4 -3
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotViews.kt +40 -0
- package/android/src/main/java/dev/apexnative/navigation/utils/ImageKey.kt +66 -0
- package/android/src/main/java/dev/apexnative/navigation/utils/ImageLoader.kt +7 -89
- package/android/src/main/java/dev/apexnative/navigation/utils/ImageRequests.kt +76 -0
- package/android/src/main/java/dev/apexnative/navigation/utils/OptionImages.kt +262 -0
- package/android/src/main/java/dev/apexnative/navigation/utils/ViewController.kt +3 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/AppliedTabCalls.kt +49 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +16 -6
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +5 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +4 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +6 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +8 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitPanes.kt +23 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +58 -15
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +28 -18
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +16 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +2 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +85 -29
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +7 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/MenuChecks.kt +13 -56
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +10 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +56 -21
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
- package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
- package/cli/doctor/checks/app.js +61 -13
- package/cli/migrate/native.js +18 -0
- package/cli/setup/source.js +2 -1
- package/cli/setup/steps/mainApplication.js +134 -19
- package/docs-md/architecture/transitions.md +6 -7
- package/docs-md/help/glossary.md +1 -1
- package/docs-md/help/troubleshooting.md +20 -3
- package/docs-md/index.json +4 -0
- package/docs-md/migration/from-react-native-navigation.md +8 -2
- package/docs-md/screen-ui/back-and-gestures.md +2 -2
- package/docs-md/start/installation.md +3 -1
- package/ios/CLAUDE.md +17 -5
- package/ios/CORE.md +13 -8
- package/ios/Contract/ANNCommandAnswer.mm +5 -20
- package/ios/Contract/ANNCommandContract.h +15 -0
- package/ios/Contract/ANNCommandContract.mm +78 -0
- package/ios/Core/ANNAppearanceDispatcher.mm +1 -1
- package/ios/Core/ANNErrorHandler.h +1 -1
- package/ios/Core/ANNErrorHandler.mm +3 -1
- package/ios/Core/ANNEventEmitter.h +1 -1
- package/ios/Core/ANNEventEmitter.mm +2 -2
- package/ios/Core/ANNJavaScriptCalls.h +34 -0
- package/ios/Core/ANNJavaScriptCalls.mm +61 -0
- package/ios/Core/ANNNavigationState.h +7 -3
- package/ios/Core/ANNNavigationState.mm +10 -5
- package/ios/Core/ANNNodeHelper.h +32 -12
- package/ios/Core/ANNNodeHelper.mm +100 -3
- package/ios/Core/ANNRenderWait.h +15 -1
- package/ios/Core/ANNRenderWait.mm +119 -17
- package/ios/Core/UIViewController+LayoutProtocol.h +9 -2
- package/ios/Core/UIViewController+LayoutProtocol.mm +8 -0
- package/ios/Layouts/Modal/ANNModalCoordinator.h +1 -1
- package/ios/Layouts/Modal/ANNModalCoordinator.mm +55 -14
- package/ios/Layouts/Screen/ANNComponentViewController.h +8 -0
- package/ios/Layouts/Screen/ANNComponentViewController.mm +32 -5
- package/ios/Layouts/Screen/ANNScreenPlace.h +41 -0
- package/ios/Layouts/Screen/ANNScreenPlace.mm +175 -0
- package/ios/Layouts/SplitView/ANNSplitViewCommands.h +3 -2
- package/ios/Layouts/SplitView/ANNSplitViewCommands.mm +105 -39
- package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -2
- package/ios/Layouts/Stack/ANNStackCommands.mm +1 -0
- package/ios/Layouts/Stack/ANNStackController.mm +26 -3
- package/ios/Layouts/Stack/ANNStackQueue.h +3 -1
- package/ios/Layouts/Stack/ANNStackQueue.mm +56 -43
- package/ios/Layouts/Stack/UINavigationController+ANNCommands.h +4 -2
- package/ios/Layouts/Stack/UINavigationController+ANNCommands.mm +2 -3
- package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.h +4 -0
- package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.mm +11 -0
- package/ios/Layouts/Tabs/ANNBottomTabsController.mm +13 -5
- package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +2 -2
- package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +109 -7
- package/ios/Options/ANNOptionsCache.h +7 -6
- package/ios/Options/ANNSearchBarOptions.h +4 -3
- package/ios/Options/ANNUserChoice.h +57 -0
- package/ios/Options/ANNUserChoice.mm +44 -0
- package/ios/Options/Params/ANNImageSource.h +61 -0
- package/ios/Options/Params/ANNImageSource.mm +101 -0
- package/ios/Options/Params/ANNOptionImages.h +37 -0
- package/ios/Options/Params/ANNOptionImages.mm +284 -0
- package/ios/Options/Params/Image.h +11 -2
- package/ios/Options/Params/Image.mm +15 -2
- package/ios/Options/Params/ImageParser.h +5 -5
- package/ios/Options/Params/ImageParser.mm +28 -46
- package/ios/Public/ApexNavigation/ApexNavigation.h +1 -1
- package/ios/RECIPES.md +11 -5
- package/ios/React/Slots/ANNSlotComponentView.mm +24 -17
- package/ios/React/Slots/ANNSlotContainers.h +14 -6
- package/ios/React/Slots/ANNSlotContainers.mm +75 -2
- package/ios/TopBar/ANNBarButtons.h +9 -5
- package/ios/TopBar/ANNBarButtons.mm +211 -63
- package/ios/TopBar/ANNIconButtonImage.h +1 -0
- package/ios/TopBar/ANNIconButtonImage.mm +34 -1
- package/ios/TopBar/ANNLargeTitlePresenter.h +13 -0
- package/ios/TopBar/ANNLargeTitlePresenter.mm +26 -3
- package/ios/TopBar/ANNSearchPresenter.mm +170 -19
- package/ios/TopBar/ANNTopBarPresenter.h +5 -0
- package/ios/TopBar/ANNTopBarPresenter.mm +60 -11
- package/ios/TurboModules/ANNTurboModule.mm +39 -21
- package/lib/module/ARCHITECTURE.md +18 -18
- package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
- package/lib/module/api/navigation.js.map +1 -1
- package/lib/module/engine/commands.js +9 -7
- package/lib/module/engine/commands.js.map +1 -1
- package/lib/module/engine/layout.js +13 -9
- package/lib/module/engine/layout.js.map +1 -1
- package/lib/module/engine/options.js +44 -11
- package/lib/module/engine/options.js.map +1 -1
- package/lib/module/engine/places.js +44 -7
- package/lib/module/engine/places.js.map +1 -1
- package/lib/module/engine/props.js +30 -1
- package/lib/module/engine/props.js.map +1 -1
- package/lib/module/engine/rememberedOptions.js +1 -1
- package/lib/module/engine/rememberedOptions.js.map +1 -1
- package/lib/module/engine/screens.js +22 -12
- package/lib/module/engine/screens.js.map +1 -1
- package/lib/module/interfaces/contract/commands.json +101 -0
- package/lib/typescript/adapters/NativeANNTurboModule.d.ts +5 -2
- package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
- package/lib/typescript/api/events/navigationEvents.d.ts +1 -1
- package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
- package/lib/typescript/api/hooks/useNavigation.d.ts +1 -1
- package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
- package/lib/typescript/api/navigation.d.ts +4 -1
- package/lib/typescript/api/navigation.d.ts.map +1 -1
- package/lib/typescript/engine/commands.d.ts.map +1 -1
- package/lib/typescript/engine/options.d.ts +5 -2
- package/lib/typescript/engine/options.d.ts.map +1 -1
- package/lib/typescript/engine/places.d.ts +5 -2
- package/lib/typescript/engine/places.d.ts.map +1 -1
- package/lib/typescript/engine/props.d.ts +8 -1
- package/lib/typescript/engine/props.d.ts.map +1 -1
- package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
- package/lib/typescript/engine/screens.d.ts.map +1 -1
- package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
- package/lib/typescript/interfaces/ComponentEvents.d.ts.map +1 -1
- package/lib/typescript/interfaces/Events.d.ts +19 -10
- package/lib/typescript/interfaces/Events.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/animations.d.ts +5 -2
- package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/buttons.d.ts +13 -5
- package/lib/typescript/interfaces/options/buttons.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/topBar.d.ts +22 -9
- package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +18 -18
- package/src/adapters/NativeANNTurboModule.ts +5 -2
- package/src/api/events/navigationEvents.ts +1 -1
- package/src/api/hooks/useNavigation.ts +1 -1
- package/src/api/navigation.ts +4 -1
- package/src/engine/commands.ts +19 -7
- package/src/engine/layout.ts +16 -9
- package/src/engine/options.ts +39 -11
- package/src/engine/places.ts +56 -7
- package/src/engine/props.ts +30 -1
- package/src/engine/rememberedOptions.ts +4 -1
- package/src/engine/screens.tsx +23 -13
- package/src/interfaces/ComponentEvents.ts +2 -2
- package/src/interfaces/Events.ts +19 -10
- package/src/interfaces/contract/commands.json +101 -0
- package/src/interfaces/options/animations.ts +5 -2
- package/src/interfaces/options/buttons.ts +13 -5
- package/src/interfaces/options/topBar.ts +22 -9
|
@@ -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
|
|
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
|
|
@@ -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
|
+
}
|
|
@@ -74,6 +74,10 @@ class BottomTabsController(
|
|
|
74
74
|
private var pendingSelected: (() -> Unit)? = null
|
|
75
75
|
// Ends the fade through of a switch still running, with its events.
|
|
76
76
|
private var endSwitch: (() -> Unit)? = null
|
|
77
|
+
|
|
78
|
+
// The made tabs that didn't show at the last repaint (new default options, a light/dark switch): each is repainted
|
|
79
|
+
// as it is selected, not while hidden.
|
|
80
|
+
private val staleTabs = mutableSetOf<ViewController<*>>()
|
|
77
81
|
// The bar goes with a stack's transition: the options of the new top don't move it themselves.
|
|
78
82
|
private var riding = false
|
|
79
83
|
private var restAttached = false
|
|
@@ -247,7 +251,7 @@ class BottomTabsController(
|
|
|
247
251
|
// `mergeOptions` with the tabs' id: their own options, then what they show, then the layouts around without
|
|
248
252
|
// `bottomTabs`; the one-shot options act once, also while the view isn't shown.
|
|
249
253
|
override fun mergeOptions(options: Options) {
|
|
250
|
-
|
|
254
|
+
mergeOwnOptions(options)
|
|
251
255
|
applyMerge(options, null)
|
|
252
256
|
performOnParentController { it.mergeChildOptions(withoutBottomTabs(options), this) }
|
|
253
257
|
}
|
|
@@ -311,8 +315,10 @@ class BottomTabsController(
|
|
|
311
315
|
private enum class How { Press, Code, Back }
|
|
312
316
|
|
|
313
317
|
/**
|
|
314
|
-
* A tap on the tab at [index], on the bar or on its cell of the custom row:
|
|
315
|
-
*
|
|
318
|
+
* 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
|
|
319
|
+
* running ends first, with its `ANN.BottomTabSelected`; then this press's `ANN.BottomTabPressed`, then the
|
|
320
|
+
* selection (`ANN.BottomTabSelected` after the new screen's DidAppear), the repeat (the pop's own events), or
|
|
321
|
+
* nothing, as its resolved options say. The same order as `-[ANNBottomTabsController pressTabAtIndex:source:]`.
|
|
316
322
|
*/
|
|
317
323
|
internal fun onTabPressed(index: Int) {
|
|
318
324
|
if (isDestroyed || index !in tabs.indices) return
|
|
@@ -363,6 +369,9 @@ class BottomTabsController(
|
|
|
363
369
|
}
|
|
364
370
|
selectedIndex = index
|
|
365
371
|
attach(index)
|
|
372
|
+
// A tab hidden at a repaint takes it now, before its screens appear: its bar's parts, its search's and its
|
|
373
|
+
// FAB's colors follow the theme and the defaults only through a repaint.
|
|
374
|
+
if (staleTabs.remove(shown)) Navigator.repaintLayer(shown)
|
|
366
375
|
peekView()?.bar?.let {
|
|
367
376
|
it.setCurrentItem(index, false)
|
|
368
377
|
it.onCustomItemViewSelectionChanged(index)
|
|
@@ -592,8 +601,8 @@ class BottomTabsController(
|
|
|
592
601
|
}
|
|
593
602
|
}
|
|
594
603
|
|
|
595
|
-
// The repaint: the bar, the items of every tab, the row
|
|
596
|
-
//
|
|
604
|
+
// The repaint: the bar, the items of every tab, the row. Only what shows is repainted (the pass over what shows
|
|
605
|
+
// reaches the selected tab); a made tab that doesn't show is repainted when it is selected ([staleTabs]).
|
|
597
606
|
internal fun repaint(options: Options) {
|
|
598
607
|
if (isDestroyed) return
|
|
599
608
|
ContainerBackground.apply(this, options)
|
|
@@ -603,7 +612,7 @@ class BottomTabsController(
|
|
|
603
612
|
if (usesRow) layout.bar.customRowOptions = options.bottomTabs.customRow.copy()
|
|
604
613
|
}
|
|
605
614
|
cells.forEachIndexed { index, cell -> cell.update(cellState(index)) }
|
|
606
|
-
tabs.forEach { if (it !== currentChild && it.isViewCreated)
|
|
615
|
+
tabs.forEach { if (it !== currentChild && it.isViewCreated) staleTabs += it }
|
|
607
616
|
}
|
|
608
617
|
|
|
609
618
|
// The custom row: the cells first into the bar's external host, then the row holds them; its look follows the tabs'
|
|
@@ -681,6 +690,7 @@ class BottomTabsController(
|
|
|
681
690
|
override fun destroy() {
|
|
682
691
|
if (isDestroyed) return
|
|
683
692
|
pendingSelected = null
|
|
693
|
+
staleTabs.clear()
|
|
684
694
|
super.destroy()
|
|
685
695
|
// A switch still running ends with its views at rest, without events: the tabs are gone.
|
|
686
696
|
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.
|
|
24
|
-
import dev.apexnative.navigation.utils.
|
|
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
|
-
|
|
71
|
-
private val
|
|
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)
|
|
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
|
|
110
|
-
|
|
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 ->
|
|
121
|
-
is TabItems.Call.SelectedIcon ->
|
|
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
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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 {
|
|
@@ -4,7 +4,7 @@ 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
|
|
7
|
+
* a push (unless `waitForRender: false`), the new top of a `setStackRoot`. A wait never listens to the readiness store
|
|
8
8
|
* itself: the content keeps the one listener of its id and tells every waiter. Main thread only.
|
|
9
9
|
*/
|
|
10
10
|
internal interface Readiness {
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt
CHANGED
|
@@ -16,8 +16,10 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
|
|
|
16
16
|
* without a limit.
|
|
17
17
|
*
|
|
18
18
|
* What differs between its users are its values, never its steps: the readiness and the clock (fakes in the unit
|
|
19
|
-
* tests), [countsUnknownContent], how a readiness signal is delivered ([deliver]: at once,
|
|
20
|
-
*
|
|
19
|
+
* tests), [countsUnknownContent], how a readiness signal is delivered ([deliver]: at once, posted to the next turn of
|
|
20
|
+
* the main loop, or behind the calls of JavaScript), whether content ready at once is delivered too
|
|
21
|
+
* ([deliversReadyContent]), and [step], the guard the user runs every later step in (a timer, a signal). Main thread
|
|
22
|
+
* only.
|
|
21
23
|
*/
|
|
22
24
|
internal class RenderWait(
|
|
23
25
|
private val readiness: Readiness,
|
|
@@ -32,6 +34,12 @@ internal class RenderWait(
|
|
|
32
34
|
private val countsUnknownContent: Boolean,
|
|
33
35
|
/** How a readiness signal reaches the wait: at once by default; posted, when a signal may come inside a layout. */
|
|
34
36
|
private val deliver: (() -> Unit) -> Unit = { it() },
|
|
37
|
+
/**
|
|
38
|
+
* Content ready when the wait starts (or retakes) still reaches its end through [deliver], within the limit that
|
|
39
|
+
* runs: what [deliver] waits behind (a push's: the calls JavaScript made in the commit of that content) comes
|
|
40
|
+
* first. Off: such content ends the wait at once.
|
|
41
|
+
*/
|
|
42
|
+
private val deliversReadyContent: Boolean = false,
|
|
35
43
|
/** Runs a later step of the wait (a timer, a signal): the user's guards and protection; as it is by default. */
|
|
36
44
|
private val step: (() -> Unit) -> Unit = { it() },
|
|
37
45
|
private val onEnd: () -> Unit,
|
|
@@ -70,7 +78,8 @@ internal class RenderWait(
|
|
|
70
78
|
|
|
71
79
|
/**
|
|
72
80
|
* Starts waiting for [screens], and for the content of [unseen] within its first [UNSEEN_LIMIT_MILLIS]; with all
|
|
73
|
-
* of them ready (and their bars' components, when they count within the limit) it ends at once, before it returns
|
|
81
|
+
* of them ready (and their bars' components, when they count within the limit) it ends at once, before it returns,
|
|
82
|
+
* or through [deliver] with [deliversReadyContent].
|
|
74
83
|
*/
|
|
75
84
|
fun start(screens: List<ViewController<*>>, unseen: List<ViewController<*>> = emptyList()) {
|
|
76
85
|
this.screens = screens
|
|
@@ -78,15 +87,18 @@ internal class RenderWait(
|
|
|
78
87
|
unseenDone = unseen.isEmpty()
|
|
79
88
|
waiting = true
|
|
80
89
|
token = Any()
|
|
81
|
-
// Content ready at once ends the wait, unless the bar's components count within a limit
|
|
82
|
-
|
|
90
|
+
// Content ready at once ends the wait, unless the bar's components count within a limit (it runs for them) or
|
|
91
|
+
// the content goes through [deliver] too.
|
|
92
|
+
val ready = contentReady()
|
|
93
|
+
if (ready && !deliversReadyContent && !(bar == Bar.WithinLimit && limitMillis != null)) return contentDone()
|
|
94
|
+
// The timers first: a [deliver] that runs at once may end the wait before this returns.
|
|
83
95
|
cancelContentTimer = when {
|
|
84
96
|
limitMillis != null -> scheduler.schedule(limitMillis) { step { atLimit() } }
|
|
85
97
|
countsUnknownContent -> scheduler.schedule(UNKNOWN_CONTENT_MILLIS) { step { atUnknownContentLimit() } }
|
|
86
98
|
else -> null
|
|
87
99
|
}
|
|
88
100
|
startUnseenLimit()
|
|
89
|
-
await()
|
|
101
|
+
if (ready && deliversReadyContent) deliverReady() else await()
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
/**
|
|
@@ -98,7 +110,7 @@ internal class RenderWait(
|
|
|
98
110
|
this.screens = screens
|
|
99
111
|
this.unseen = unseen
|
|
100
112
|
token = Any()
|
|
101
|
-
await()
|
|
113
|
+
if (deliversReadyContent && contentReady()) deliverReady() else await()
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
/** Ends the wait without [onEnd]: its timers stop, and the signals still to come do nothing. */
|
|
@@ -129,6 +141,14 @@ internal class RenderWait(
|
|
|
129
141
|
if (contentReady() && cancelBarPhase == null) contentDone()
|
|
130
142
|
}
|
|
131
143
|
|
|
144
|
+
// The content is ready already: it goes once through [deliver] as a signal would, then on as [await] says.
|
|
145
|
+
private fun deliverReady() {
|
|
146
|
+
val token = token
|
|
147
|
+
deliver {
|
|
148
|
+
step { if (waiting && this.token === token) await() }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
132
152
|
// The content of every screen ready: on to the bars; else the signals of those not ready.
|
|
133
153
|
private fun await() {
|
|
134
154
|
if (contentReady()) return contentDone()
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt
CHANGED
|
@@ -731,7 +731,7 @@ internal class ModalCoordinator(
|
|
|
731
731
|
// without the events not sent yet; what waits for it rejects, the layer it took out comes back, and the modals that
|
|
732
732
|
// wait go on.
|
|
733
733
|
private fun dismissFailed(record: ModalRecord, e: Throwable) {
|
|
734
|
-
val text = ErrorTexts.couldNot("dismiss the modal
|
|
734
|
+
val text = ErrorTexts.couldNot("dismiss the modal '${record.rootId}'", ErrorTexts.reasonOf(e))
|
|
735
735
|
val listed = record in records
|
|
736
736
|
val under = layerUnder(record)
|
|
737
737
|
runCatching { record.stopAnimation?.invoke() }
|
|
@@ -835,8 +835,8 @@ internal class ModalCoordinator(
|
|
|
835
835
|
State.Presenting -> record.waitingDismissals += dismissal
|
|
836
836
|
State.Shown -> if (isTop(record)) beginDismissal(record, dismissal) else closeBelow(record, dismissal)
|
|
837
837
|
State.Dismissing -> dismissal.answer?.let(record.answers::add)
|
|
838
|
-
|
|
839
|
-
State.WithOldRoot, State.Gone -> dismissal.answer?.
|
|
838
|
+
// It went already, or goes with the old root: the dismissal just completes, as on iOS.
|
|
839
|
+
State.ClosedBelow, State.WithOldRoot, State.Gone -> dismissal.answer?.resolve(record.rootId)
|
|
840
840
|
}
|
|
841
841
|
}
|
|
842
842
|
|
|
@@ -850,7 +850,7 @@ internal class ModalCoordinator(
|
|
|
850
850
|
record.state = State.Gone
|
|
851
851
|
records.remove(record)
|
|
852
852
|
views.remove(record)
|
|
853
|
-
val screen = AppearanceDispatcher.
|
|
853
|
+
val screen = AppearanceDispatcher.modalScreen(record.root)
|
|
854
854
|
record.showAnswer?.resolve(record.rootId)
|
|
855
855
|
record.showAnswer = null
|
|
856
856
|
record.chain?.let {
|
|
@@ -1012,7 +1012,7 @@ internal class ModalCoordinator(
|
|
|
1012
1012
|
val wasOut = isOut(revealed)
|
|
1013
1013
|
val counted = members.filter { it.state != State.ClosedBelow && !it.reported }
|
|
1014
1014
|
batch.count = counted.size
|
|
1015
|
-
batch.screen = counted.lastOrNull()?.let { AppearanceDispatcher.
|
|
1015
|
+
batch.screen = counted.lastOrNull()?.let { AppearanceDispatcher.modalScreen(it.root) }
|
|
1016
1016
|
// The top that leaves with its animation: one already leaving keeps its own.
|
|
1017
1017
|
val top = members.lastOrNull { it.state in ANIMATED_IN_BATCH }
|
|
1018
1018
|
batch.top = top
|
package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt
CHANGED
|
@@ -439,9 +439,10 @@ class Navigator(
|
|
|
439
439
|
}
|
|
440
440
|
|
|
441
441
|
/**
|
|
442
|
-
* The activity is destroyed: every layer goes, with the events of a destroy; a `setRoot`
|
|
443
|
-
* `''` (JavaScript sets its root again in the next activity),
|
|
444
|
-
* wait for a modal with their roots' ids.
|
|
442
|
+
* The activity is destroyed: every layer goes, with the events of a destroy; a `setRoot` whose root still waited
|
|
443
|
+
* resolves with `''` (JavaScript sets its root again in the next activity), one whose root showed resolves with
|
|
444
|
+
* its id, and a modal's or an overlay's show and the commands that wait for a modal resolve with their roots' ids.
|
|
445
|
+
* Main thread only.
|
|
445
446
|
*/
|
|
446
447
|
internal fun destroyWithActivity() {
|
|
447
448
|
destroyLayers(answerPending = true)
|