Arrange the navigation panels, and optionally do without them - #813
Merged
Conversation
Owner
Author
|
Left undone: The arc labels have no backdrop and are hard to read over busy content. A scrim behind the arc, or a pill behind each label, would fix it.
On #532: hiding the Store panel is not the option requested there. It removes the panel from the navigation, but the store still exists, update checking still runs, and the activity feed and framework update check still reach the network. Supporting a second repository is not planned at the moment, though not ruled out later. This does not close it. |
The four panels have been in the same order, all of them always shown, since the bar was written. That order is a guess about how someone uses the app, and it is wrong for anyone who lives in the log viewer or has never opened the store. Long-pressing any item now enters an edit mode: every panel gains a badge in its top corner that hides or restores it, and dragging an item reorders it, the way icons are arranged on a launcher. All four are shown while editing, hidden ones dimmed, because that is the only way back for a panel that has been hidden -- an edit mode that showed only what is already visible would be a one-way door. The badge is withheld entirely from the last visible panel rather than disabled, so there is never a control that does nothing; NavPanels.canHide answers that question once for the badge, the screen-reader action and anything added later. Hiding is not deletion, and the distinction is load-bearing. NavKeySerializer resolves a persisted key with a bare Class.forName and NavDisplay's entryProvider throws for a key it was never given, so a back stack saved before a panel was hidden would take the app down rather than show a stale tab. Every route stays declared and stays registered; only the container changes. The arrangement is stored as one delimited string of route keys with '!' marking hidden -- "logs,home,!store,modules". A string rather than a set because putStringSet does not preserve order and the order is the point, and keys rather than ordinals or class names because R8 rewrites class names in a release build and an ordinal would silently name a different panel the day a fifth one is declared. decodeNavPanels is total: unknown and duplicate keys are dropped, and any destination the string does not name is appended visible, so a panel added next year still appears for the readers who arranged theirs today. Three assumptions about Home had to go with it. The start destination, the highlighted item's fallback and a restored root are now the first *visible* panel, and reconcilePanels corrects a root naming a panel that is no longer drawn. currentTopLevel keeps its own visibility test even so: hiding the panel you are standing on recomposes the container a frame before the effect runs, and a bar highlighting nothing is worse than one highlighting where you are about to be. The appearance sheet gains a way in, because nothing on Android teaches that a navigation bar can be long-pressed at all. The arrangement itself stays on the container, where a drag can be watched. Three things the implementation could not do the obvious way: - combinedClickable cannot carry the long press. ShortNavigationBarItem applies selectable() internally, so the item is the nearer pointer node and handles the release first: a long press on Logs would open edit mode *and* switch to Logs. A pointer-input state machine on the slot watches the Initial pass instead, consuming nothing while the press might still be a tap and taking the gesture the moment the hold lands. - The reorder measures from onGloballyPositioned rather than item size. WideNavigationRail folds the inter-item spacing into each item's size, so on the rail axis every shift would lag the finger by that gap. - The dragged item uses absoluteOffset. Recorded positions and drag deltas both count pixels rightwards, and offset mirrors under RTL -- which values-ar makes reachable.
An option in the appearance sheet, off by default. With it on there is no bar and no rail: the suite type becomes NavigationSuiteType.None, which is the part that makes this worth having at all -- the container is not merely hidden but never laid out, so the strip it costs every screen comes back as content. On a small phone reading a log, that strip is the expensive part. Long-pressing the ball fans the visible panels into an arc around it; keep dragging to highlight one and release to go there. A plain tap opens the same arc latched, so each panel is then an ordinary tappable target -- that is the accessible path, and it is not decoration: a drag-to-select gesture is invisible to TalkBack, so without it the whole style would be unreachable for anyone using a screen reader. It is an ordinary composable drawn over the destination, inside the app window. Never a system overlay: parasitically this app *is* com.android.shell, and asking for SYSTEM_ALERT_WINDOW from there is not something we should ever do. It follows the rule the container already follows -- present at the root of a panel, gone on a detail screen, which has its own back affordance. The arc opens inward from whichever edge the ball is parked on and stays inside the window, including at the corners. Only the side and a fractional height are persisted, not a coordinate: the ball always snaps to an edge, so an x position would be a lie the moment the window is a different width, which unfolded and in landscape it routinely is. Every coordinate here is counted from the left of the window, because that is what the insets, the constraints and a pointer's own position are counted from, so the placement has to be absolute as well: Alignment.TopStart puts a child against the right edge under RTL and Modifier.offset negates its x there, and values-ar makes both reachable. Mirrored, the ball is drawn against the edge opposite the one it is parked at, the arc is drawn a panel's width off the ball it belongs to, and the finger picks by pointing away from what it is over. Which side the ball parks on is decided once, by atEnd against the layout direction, and the layout is not allowed a second opinion. Edit mode overrules the setting for as long as it lasts, since there is nothing to rearrange otherwise, and the sheet's rearrange row stops being merely a discoverability aid and becomes the only way in -- there is no bar left to long-press.
Twelve units across eighteen languages: the four spoken labels a rearrange needs -- the hide and show badges, the two moves a screen reader makes in place of a drag -- the ball's own open and close, the edit-mode Done, and the five that describe all of it in the appearance sheet. The terms follow what each translator already chose in these files rather than being coined again: the word for the bar is theirs, and the panels take the word that file uses for a section of the app, which is not always the English one. Russian and Ukrainian keep the two apart deliberately -- панель is what they call the bar, so the panels are разделы and розділи, or the summary would be replacing a thing with itself. The hide and show badges quote the panel name in those two, since a bare label in the nominative reads wrongly after their verbs, and Turkish takes the suffix-free 'öğesini' form for the same reason.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two commits:
The second is separable: the first builds without
FloatingPanelNav.kt, and reverting the second lands on the first with no conflicts.Arranging
Long-press any item to enter edit mode. Each panel gains a badge that hides or restores it; dragging reorders. All four are shown while editing, hidden ones dimmed — the only way to restore one. The last visible panel gets no hide badge, so one always remains. Works on the bar and the rail.
The appearance sheet gains a way in, since nothing on Android teaches that a navigation bar can be long-pressed.
Floating ball
The suite type becomes
NavigationSuiteType.None, so the container is never laid out and the strip it costs every screen returns as content. Long-press fans the visible panels into an arc, dragging highlights one, releasing navigates. A tap opens the same arc latched so each panel is an ordinary tappable target — required, as drag-to-select is invisible to TalkBack.Drawn inside the app window, never a system overlay: parasitically this app is
com.android.shell, which must not requestSYSTEM_ALERT_WINDOW.Notes
Stored as one string of route keys with
!marking hidden —logs,home,!store,modules. Not aStringSet, which does not preserve order; not ordinals or class names, since R8 rewrites class names and an ordinal shifts once a fifth panel is added.decodeNavPanelsis total: unknown and duplicate keys are dropped, unnamed destinations appended visible.Hiding is not deletion.
NavKeySerializerresolves keys with a bareClass.forNameandentryProviderthrows for one it was never given, so a back stack saved before a panel was hidden would crash. Every route stays registered.The start destination, the highlight fallback and a restored root are now the first visible panel.
currentTopLevelkeeps its own visibility test alongsidereconcilePanels, as hiding the current panel recomposes the container a frame before the effect runs.Three non-obvious points:
combinedClickablecannot carry the long press.ShortNavigationBarItemappliesselectable()internally and handles the release first, so a long press would also switch tab. A pointer-input state machine on the slot watches the Initial pass instead.onGloballyPositionedrather than item size, asWideNavigationRailfolds inter-item spacing into each item's size.absoluteOffset;offsetmirrors under RTL, whichvalues-armakes reachable.Testing
Pixel 7a emulator, API 36, via the debug demo host. Edit mode, hide, restore, reorder on both axes, the minimum-one-panel rule, hiding the current panel, back leaving edit mode, and the arrangement surviving a
force-stop. For the ball: the latched arc, hold-and-drag selection, and arc bounds in landscape.The rail required forcing
navigationSuiteTypein a throwaway build, ascurrentWindowAdaptiveInfo()ignoreswm size/wm densityoverrides — the scaffold still selected a bar atw1200dp-h1800dp. Rail layout, edit mode and vertical drag are confirmed; the automatic switch on real large-screen hardware is untested.