@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.
- package/CHANGELOG.md +22 -0
- package/android/CLAUDE.md +26 -9
- package/android/CORE.md +32 -17
- package/android/RECIPES.md +9 -3
- package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +25 -9
- 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/AnimationsOptions.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/HardwareBackButtonOptions.kt +9 -1
- package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
- package/android/src/main/java/dev/apexnative/navigation/options/StackAnimationOptions.kt +40 -2
- package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
- package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +6 -6
- package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +29 -36
- package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
- package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +33 -22
- 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/ReactView.kt +39 -20
- package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +7 -4
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +6 -6
- package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +10 -5
- package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +47 -46
- package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +34 -7
- package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationState.kt +154 -0
- package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +6 -4
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
- package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +12 -12
- 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/arrangement/ArrangementController.kt +3 -2
- 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 +25 -17
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentViewController.kt +6 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +2 -2
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenReadiness.kt +103 -56
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +5 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackCheck.kt +50 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +45 -14
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +25 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +8 -5
- 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 +61 -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 +25 -12
- 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/ScreenSearch.kt +3 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +5 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarComponents.kt +5 -51
- 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 +14 -11
- 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/bottomtabs/CustomBottomTabItemView.kt +0 -5
- 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/doctor/checks/splash.js +4 -22
- package/cli/migrate/native.js +18 -0
- package/cli/setup/source.js +2 -1
- package/cli/setup/steps/mainApplication.js +134 -19
- package/cli/splash/index.js +3 -1
- package/docs-md/architecture/transitions.md +6 -7
- package/docs-md/containers/modals.md +2 -2
- package/docs-md/guides/cli.md +1 -2
- package/docs-md/help/glossary.md +1 -1
- package/docs-md/help/troubleshooting.md +22 -5
- 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 +24 -2
- package/docs-md/start/installation.md +4 -2
- package/ios/CLAUDE.md +17 -5
- package/ios/CORE.md +18 -13
- package/ios/Contract/ANNCommandAnswer.h +9 -2
- package/ios/Contract/ANNCommandAnswer.mm +28 -30
- package/ios/Contract/ANNCommandContract.h +15 -0
- package/ios/Contract/ANNCommandContract.mm +78 -0
- package/ios/Core/ANNAppearanceDispatcher.h +5 -4
- package/ios/Core/ANNAppearanceDispatcher.mm +12 -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 -5
- package/ios/Core/ANNNavigationState.mm +8 -7
- 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/Root/ANNSplashViewController.h +5 -3
- package/ios/Layouts/Root/ANNSplashViewController.mm +147 -0
- 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/ANNPreviewPush.h +5 -5
- package/ios/Layouts/Stack/ANNPreviewPush.mm +85 -22
- 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/commands/commands.js +54 -6
- package/lib/module/api/commands/commands.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 +52 -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/commands/commands.d.ts.map +1 -1
- package/lib/typescript/api/commands/presentation.d.ts +4 -3
- package/lib/typescript/api/commands/presentation.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 +7 -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.d.ts +1 -1
- package/lib/typescript/interfaces/Options.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/animations.d.ts +23 -4
- 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/stack.d.ts +9 -0
- package/lib/typescript/interfaces/options/stack.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/commands/commands.ts +48 -7
- package/src/api/commands/presentation.ts +4 -3
- 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 +65 -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/Options.ts +1 -0
- package/src/interfaces/contract/commands.json +101 -0
- package/src/interfaces/options/animations.ts +24 -4
- package/src/interfaces/options/buttons.ts +13 -5
- package/src/interfaces/options/stack.ts +9 -0
- package/src/interfaces/options/topBar.ts +22 -9
- package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationStateReporter.kt +0 -62
- 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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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:
|
|
315
|
-
*
|
|
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
|
|
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
|
|
596
|
-
//
|
|
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)
|
|
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
|
-
|
|
662
|
-
|
|
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.
|
|
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 {
|
|
@@ -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
|
-
|
|
181
|
-
|
|
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
|
|
8
|
-
* itself: the content keeps
|
|
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. */
|