@apexnative/navigation 1.0.0-alpha.2 → 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 +31 -0
- package/android/CLAUDE.md +38 -9
- package/android/CORE.md +11 -4
- package/android/RECIPES.md +9 -3
- package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +29 -1
- 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/StatusBarOptions.kt +7 -3
- 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/arrangement/SecondaryScreenPresenter.kt +13 -5
- 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 +92 -19
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentPresenter.kt +28 -7
- 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/component/ScreenFrame.kt +1 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalAnimations.kt +22 -24
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +79 -13
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalRecord.kt +21 -6
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +29 -12
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +22 -18
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +4 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +45 -6
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +35 -9
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +10 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ContainerBackground.kt +3 -3
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +5 -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 +72 -16
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +287 -0
- 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 +164 -13
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +26 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +136 -40
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackTransitions.kt +26 -8
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +107 -4
- 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 +131 -9
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +75 -21
- package/android/src/main/java/dev/apexnative/navigation/views/animations/Curves.kt +29 -11
- package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
- package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +15 -0
- package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +137 -1
- package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +8 -1
- package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +26 -4
- 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 +12 -7
- package/docs-md/containers/modals.md +4 -2
- package/docs-md/containers/overlays.md +3 -1
- package/docs-md/containers/side-menu.md +4 -2
- package/docs-md/containers/tabs.md +2 -0
- package/docs-md/flows/forms-in-a-modal.md +1 -1
- package/docs-md/help/glossary.md +2 -2
- package/docs-md/help/troubleshooting.md +20 -3
- package/docs-md/index.json +12 -0
- package/docs-md/migration/from-react-native-navigation.md +8 -2
- package/docs-md/screen-ui/back-and-gestures.md +8 -2
- package/docs-md/screen-ui/system-ui.md +1 -1
- 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/hooks/useNavigation.js +8 -3
- package/lib/module/api/hooks/useNavigation.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 +15 -7
- 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/modals.d.ts +18 -13
- package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/system.d.ts +6 -3
- package/lib/typescript/interfaces/options/system.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 +9 -4
- 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 +15 -7
- package/src/interfaces/options/buttons.ts +13 -5
- package/src/interfaces/options/modals.ts +18 -13
- package/src/interfaces/options/system.ts +6 -3
- package/src/interfaces/options/topBar.ts +22 -9
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package dev.apexnative.navigation.utils
|
|
2
|
+
|
|
3
|
+
import android.graphics.drawable.Drawable
|
|
4
|
+
|
|
5
|
+
/** Where the images of the options come from, for one context: [OptionImages.source], or a fake in the tests. */
|
|
6
|
+
internal interface ImageSource {
|
|
7
|
+
/** The key of [uri] in the context's configuration now; for a `file:` uri it reads the file's version. */
|
|
8
|
+
fun key(uri: String): ImageKey
|
|
9
|
+
|
|
10
|
+
/** A copy of the image of [key] when the cache has it, none otherwise; never loads. */
|
|
11
|
+
fun cached(key: ImageKey): Drawable?
|
|
12
|
+
|
|
13
|
+
/** A copy of the image of [key], from the cache or loaded on the caller's thread; none on a failure. */
|
|
14
|
+
fun now(key: ImageKey): Drawable?
|
|
15
|
+
|
|
16
|
+
/** Whether [uri] can load off the main thread: a file or a drawable of the app. */
|
|
17
|
+
fun loadsLater(uri: String): Boolean
|
|
18
|
+
|
|
19
|
+
/** Loads [key] off the main thread and gives a copy of it, or none on a failure, to [done] on the main thread. */
|
|
20
|
+
fun later(key: ImageKey, done: (Drawable?) -> Unit)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The images that the places of a view asked for (an item's icon, a button's icon), by place [P]: what the cache has
|
|
25
|
+
* comes at once; what it doesn't, when the place can show it later, loads off the main thread and comes when ready;
|
|
26
|
+
* everything else loads at once. The last request of a place wins: a load that arrives after the place asked again,
|
|
27
|
+
* or after [forget] or [clear], is dropped. A load that fails gives the place no image, as an option without one:
|
|
28
|
+
* the image it showed before doesn't stay. A request builds the key of its image once (a file's version is read once).
|
|
29
|
+
* Main thread only.
|
|
30
|
+
*/
|
|
31
|
+
internal class ImageRequests<P : Any>(private val source: ImageSource) {
|
|
32
|
+
// The open load of each place: its number, which a later request of the place replaces.
|
|
33
|
+
private val waiting = HashMap<P, Long>()
|
|
34
|
+
private var last = 0L
|
|
35
|
+
|
|
36
|
+
/** The key of [uri] now, for a caller that compares it before it asks ([request] with the key). */
|
|
37
|
+
fun key(uri: String): ImageKey = source.key(uri)
|
|
38
|
+
|
|
39
|
+
/** [request] for the image of [uri], its key built here. */
|
|
40
|
+
fun request(place: P, uri: String, mayWait: Boolean, show: (Drawable?) -> Unit) =
|
|
41
|
+
request(place, source.key(uri), mayWait, show)
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gives [show] the image of [key] for [place]: now when the cache has it, or when the place can't wait for it
|
|
45
|
+
* ([mayWait] false: the first frame needs it) or the uri can't load off the main thread (Metro's address, a content
|
|
46
|
+
* uri); otherwise later, unless [place] asks again first. Null when it doesn't load: the place shows none.
|
|
47
|
+
*/
|
|
48
|
+
fun request(place: P, key: ImageKey, mayWait: Boolean, show: (Drawable?) -> Unit) {
|
|
49
|
+
waiting.remove(place)
|
|
50
|
+
source.cached(key)?.let {
|
|
51
|
+
show(it)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if (!mayWait || !source.loadsLater(key.uri)) {
|
|
55
|
+
show(source.now(key))
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
val number = ++last
|
|
59
|
+
waiting[place] = number
|
|
60
|
+
source.later(key) { image ->
|
|
61
|
+
if (waiting[place] != number) return@later
|
|
62
|
+
waiting.remove(place)
|
|
63
|
+
show(image)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The place shows no image of the options now: a load it asked for earlier is dropped. */
|
|
68
|
+
fun forget(place: P) {
|
|
69
|
+
waiting.remove(place)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The view goes: every open load is dropped. */
|
|
73
|
+
fun clear() {
|
|
74
|
+
waiting.clear()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -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
|
|
@@ -2,24 +2,25 @@ package dev.apexnative.navigation.viewcontrollers.arrangement
|
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
4
|
import dev.apexnative.navigation.options.Options
|
|
5
|
+
import dev.apexnative.navigation.viewcontrollers.component.ComponentPresenter
|
|
5
6
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
|
|
6
7
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The presenter of the secondary screen of a pair ([ArrangementController]). The status bar, the navigation bar and
|
|
10
|
-
* the orientation are the primary's, as the top bar is: the secondary applies only its background
|
|
11
|
-
* front it gives them back to the primary.
|
|
11
|
+
* the orientation are the primary's, as the top bar is: the secondary applies only its background (the theme's
|
|
12
|
+
* without `layout.backgroundColor`, as any React screen), and brought to the front it gives them back to the primary.
|
|
12
13
|
*/
|
|
13
14
|
internal class SecondaryScreenPresenter(activity: Activity, defaultOptions: Options) :
|
|
14
15
|
Presenter(activity, defaultOptions) {
|
|
15
16
|
|
|
16
17
|
override fun applyOptions(view: ViewController<*>, options: Options) =
|
|
17
|
-
|
|
18
|
+
applyBackground(view, options.copy().withDefaultOptions(defaultOptions))
|
|
18
19
|
|
|
19
20
|
override fun mergeOptions(viewController: ViewController<*>, options: Options) {
|
|
20
21
|
if (!options.layout.backgroundColor.hasValue()) return
|
|
21
22
|
val merged = viewController.resolveCurrentOptions().copy().mergeWith(options)
|
|
22
|
-
|
|
23
|
+
applyBackground(viewController, merged.withDefaultOptions(defaultOptions))
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
override fun onViewBroughtToFront(viewController: ViewController<*>, options: Options) {
|
|
@@ -27,5 +28,12 @@ internal class SecondaryScreenPresenter(activity: Activity, defaultOptions: Opti
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
override fun onConfigurationChanged(controller: ViewController<*>, options: Options) =
|
|
30
|
-
|
|
31
|
+
applyBackground(controller, options.copy().withDefaultOptions(defaultOptions))
|
|
32
|
+
|
|
33
|
+
// `layout.backgroundColor`, else the theme's background (`ComponentPresenter.themeBackgroundOf`).
|
|
34
|
+
private fun applyBackground(view: ViewController<*>, options: Options) {
|
|
35
|
+
if (options.layout.backgroundColor.hasValue()) return applyBackgroundColor(view, options)
|
|
36
|
+
val color = ComponentPresenter.themeBackgroundOf(activity) ?: return
|
|
37
|
+
view.peekView()?.setBackgroundColor(color)
|
|
38
|
+
}
|
|
31
39
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
package dev.apexnative.navigation.viewcontrollers.bottomtabs
|
|
2
2
|
|
|
3
|
+
import android.animation.Animator
|
|
4
|
+
import android.animation.AnimatorListenerAdapter
|
|
5
|
+
import android.animation.AnimatorSet
|
|
3
6
|
import android.app.Activity
|
|
4
7
|
import android.graphics.Color
|
|
5
8
|
import android.util.Log
|
|
@@ -33,6 +36,7 @@ import dev.apexnative.navigation.viewcontrollers.stack.fab.FabPlacement
|
|
|
33
36
|
import dev.apexnative.navigation.viewcontrollers.stack.topbar.BarComponents
|
|
34
37
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
|
|
35
38
|
import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
|
|
39
|
+
import dev.apexnative.navigation.views.animations.FadeThrough
|
|
36
40
|
import dev.apexnative.navigation.views.bottomtabs.BottomTabItem
|
|
37
41
|
import dev.apexnative.navigation.views.bottomtabs.BottomTabsLayout
|
|
38
42
|
import dev.apexnative.navigation.views.bottomtabs.CustomBottomTabItemView
|
|
@@ -68,6 +72,12 @@ class BottomTabsController(
|
|
|
68
72
|
private var cells: List<CustomBottomTabItemView> = emptyList()
|
|
69
73
|
// `ANN.BottomTabSelected` of a press that waits for the new tab's screen to appear.
|
|
70
74
|
private var pendingSelected: (() -> Unit)? = null
|
|
75
|
+
// Ends the fade through of a switch still running, with its events.
|
|
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<*>>()
|
|
71
81
|
// The bar goes with a stack's transition: the options of the new top don't move it themselves.
|
|
72
82
|
private var riding = false
|
|
73
83
|
private var restAttached = false
|
|
@@ -150,8 +160,11 @@ class BottomTabsController(
|
|
|
150
160
|
/** The view of [tab] joins, or is there already: [visible], or invisible but laid out. */
|
|
151
161
|
fun attach(tab: ViewController<*>, visible: Boolean)
|
|
152
162
|
|
|
153
|
-
/**
|
|
154
|
-
|
|
163
|
+
/**
|
|
164
|
+
* The view of [shown] becomes visible and that of [hidden] invisible: at once, or with Material's fade through
|
|
165
|
+
* when [animated]; then [onEnd]. The function returned ends a switch still running at once, [onEnd] included.
|
|
166
|
+
*/
|
|
167
|
+
fun switch(shown: ViewController<*>, hidden: ViewController<*>, animated: Boolean, onEnd: () -> Unit): () -> Unit
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
/** The views of the tabs; none before the tabs' view is made. */
|
|
@@ -166,9 +179,43 @@ class BottomTabsController(
|
|
|
166
179
|
tab.view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
|
|
167
180
|
}
|
|
168
181
|
|
|
169
|
-
override fun switch(
|
|
170
|
-
shown
|
|
171
|
-
hidden
|
|
182
|
+
override fun switch(
|
|
183
|
+
shown: ViewController<*>,
|
|
184
|
+
hidden: ViewController<*>,
|
|
185
|
+
animated: Boolean,
|
|
186
|
+
onEnd: () -> Unit,
|
|
187
|
+
): () -> Unit {
|
|
188
|
+
val coming = shown.view
|
|
189
|
+
coming.visibility = View.VISIBLE
|
|
190
|
+
val going = hidden.peekView()
|
|
191
|
+
if (!animated || going == null) {
|
|
192
|
+
going?.visibility = View.INVISIBLE
|
|
193
|
+
onEnd()
|
|
194
|
+
return {}
|
|
195
|
+
}
|
|
196
|
+
val exit = FadeThrough.exit(going)
|
|
197
|
+
// Faded out, the tab that goes takes no more touches and no accessibility focus.
|
|
198
|
+
exit.addListener(object : AnimatorListenerAdapter() {
|
|
199
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
200
|
+
going.visibility = View.INVISIBLE
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
val set = AnimatorSet()
|
|
204
|
+
set.playTogether(exit, FadeThrough.enter(coming))
|
|
205
|
+
set.addListener(object : AnimatorListenerAdapter() {
|
|
206
|
+
override fun onAnimationEnd(animation: Animator) {
|
|
207
|
+
set.removeAllListeners()
|
|
208
|
+
// The tab that went keeps its look for its next selection; only its visibility hides it.
|
|
209
|
+
going.visibility = View.INVISIBLE
|
|
210
|
+
going.alpha = 1f
|
|
211
|
+
coming.alpha = 1f
|
|
212
|
+
coming.scaleX = 1f
|
|
213
|
+
coming.scaleY = 1f
|
|
214
|
+
onEnd()
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
set.start()
|
|
218
|
+
return set::end
|
|
172
219
|
}
|
|
173
220
|
}
|
|
174
221
|
|
|
@@ -204,7 +251,7 @@ class BottomTabsController(
|
|
|
204
251
|
// `mergeOptions` with the tabs' id: their own options, then what they show, then the layouts around without
|
|
205
252
|
// `bottomTabs`; the one-shot options act once, also while the view isn't shown.
|
|
206
253
|
override fun mergeOptions(options: Options) {
|
|
207
|
-
|
|
254
|
+
mergeOwnOptions(options)
|
|
208
255
|
applyMerge(options, null)
|
|
209
256
|
performOnParentController { it.mergeChildOptions(withoutBottomTabs(options), this) }
|
|
210
257
|
}
|
|
@@ -268,8 +315,10 @@ class BottomTabsController(
|
|
|
268
315
|
private enum class How { Press, Code, Back }
|
|
269
316
|
|
|
270
317
|
/**
|
|
271
|
-
* A tap on the tab at [index], on the bar or on its cell of the custom row:
|
|
272
|
-
*
|
|
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:]`.
|
|
273
322
|
*/
|
|
274
323
|
internal fun onTabPressed(index: Int) {
|
|
275
324
|
if (isDestroyed || index !in tabs.indices) return
|
|
@@ -302,10 +351,9 @@ class BottomTabsController(
|
|
|
302
351
|
}
|
|
303
352
|
|
|
304
353
|
/**
|
|
305
|
-
* Selects the tab at [index]:
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
* and the state at once. The history of back follows.
|
|
354
|
+
* Selects the tab at [index]: the bar shows it at once; on screen the tab it left fades through to it (Material's
|
|
355
|
+
* fade through), off screen one container becomes invisible and the other visible at once; then [switched]. The
|
|
356
|
+
* history of back follows.
|
|
309
357
|
*/
|
|
310
358
|
private fun select(index: Int, how: How) {
|
|
311
359
|
if (index == selectedIndex || index !in tabs.indices || isDestroyed) return
|
|
@@ -321,6 +369,9 @@ class BottomTabsController(
|
|
|
321
369
|
}
|
|
322
370
|
selectedIndex = index
|
|
323
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)
|
|
324
375
|
peekView()?.bar?.let {
|
|
325
376
|
it.setCurrentItem(index, false)
|
|
326
377
|
it.onCustomItemViewSelectionChanged(index)
|
|
@@ -328,10 +379,27 @@ class BottomTabsController(
|
|
|
328
379
|
// A layer that waits for the tabs waits for the tab they show now.
|
|
329
380
|
navigator?.shownScreensChanged(this)
|
|
330
381
|
if (onScreen) AppearanceDispatcher.beginTransition(listOf(shown))
|
|
331
|
-
views
|
|
382
|
+
if (views == null) return switched(index, previous, how, onScreen, hasViews = false)
|
|
383
|
+
endSwitch = views.switch(shown, left, animated = onScreen) {
|
|
384
|
+
endSwitch = null
|
|
385
|
+
if (!isDestroyed) switched(index, previous, how, onScreen, hasViews = true)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The switch to the tab at [index] from [previous] ended: what went disappears, what came appears — unless the tabs
|
|
391
|
+
* stopped showing during the fade (a modal over them): it appears when they show again; then for a press
|
|
392
|
+
* `ANN.BottomTabSelected` right after the new screen's DidAppear and the left tab's `popToRootOnBlur`, with the state
|
|
393
|
+
* as one update after them; from code and by back, `popToRootOnBlur` and the state at once. [onScreen]: the switch
|
|
394
|
+
* started on screen.
|
|
395
|
+
*/
|
|
396
|
+
private fun switched(index: Int, previous: Int, how: How, onScreen: Boolean, hasViews: Boolean) {
|
|
397
|
+
val left = tabs[previous]
|
|
398
|
+
val shown = tabs[index]
|
|
332
399
|
if (onScreen) {
|
|
333
|
-
|
|
334
|
-
|
|
400
|
+
val showing = if (isOnScreen) listOf(shown) else emptyList()
|
|
401
|
+
AppearanceDispatcher.endTransition(listOf(left), showing, null, null, null, reportsState = false)
|
|
402
|
+
} else if (hasViews) {
|
|
335
403
|
// Not on screen: the bar takes the new tab's options as they are, without appearance events.
|
|
336
404
|
val options = resolveShownOptions(DefaultOptions.current)
|
|
337
405
|
bar.applyBar(options.bottomTabs, directionOf(options))
|
|
@@ -358,8 +426,10 @@ class BottomTabsController(
|
|
|
358
426
|
if (screen == null) finish() else AppearanceDispatcher.afterDidAppear(screen, finish)
|
|
359
427
|
}
|
|
360
428
|
|
|
361
|
-
// A press's `ANN.BottomTabSelected` still waiting goes before anything
|
|
429
|
+
// A switch still running ends at once and a press's `ANN.BottomTabSelected` still waiting goes, before anything
|
|
430
|
+
// newer on these tabs.
|
|
362
431
|
private fun flushPendingSelected() {
|
|
432
|
+
endSwitch?.invoke()
|
|
363
433
|
pendingSelected?.invoke()
|
|
364
434
|
}
|
|
365
435
|
|
|
@@ -531,8 +601,8 @@ class BottomTabsController(
|
|
|
531
601
|
}
|
|
532
602
|
}
|
|
533
603
|
|
|
534
|
-
// The repaint: the bar, the items of every tab, the row
|
|
535
|
-
//
|
|
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]).
|
|
536
606
|
internal fun repaint(options: Options) {
|
|
537
607
|
if (isDestroyed) return
|
|
538
608
|
ContainerBackground.apply(this, options)
|
|
@@ -542,7 +612,7 @@ class BottomTabsController(
|
|
|
542
612
|
if (usesRow) layout.bar.customRowOptions = options.bottomTabs.customRow.copy()
|
|
543
613
|
}
|
|
544
614
|
cells.forEachIndexed { index, cell -> cell.update(cellState(index)) }
|
|
545
|
-
tabs.forEach { if (it !== currentChild && it.isViewCreated)
|
|
615
|
+
tabs.forEach { if (it !== currentChild && it.isViewCreated) staleTabs += it }
|
|
546
616
|
}
|
|
547
617
|
|
|
548
618
|
// The custom row: the cells first into the bar's external host, then the row holds them; its look follows the tabs'
|
|
@@ -620,7 +690,10 @@ class BottomTabsController(
|
|
|
620
690
|
override fun destroy() {
|
|
621
691
|
if (isDestroyed) return
|
|
622
692
|
pendingSelected = null
|
|
693
|
+
staleTabs.clear()
|
|
623
694
|
super.destroy()
|
|
695
|
+
// A switch still running ends with its views at rest, without events: the tabs are gone.
|
|
696
|
+
endSwitch?.invoke()
|
|
624
697
|
peekView()?.let { layout ->
|
|
625
698
|
layout.bar.destroyCustomItemViews()
|
|
626
699
|
layout.row?.let(CustomTabRowPlacer::forget)
|