This is the ash shelf, the system interface surface that allows users to launch application shortcuts or go to the home screen, among other things.
The shelf contains the following components, each of which lives in its own widget:
The shelf widget contains no actionable UI but contains the semi-opaque background shown behind the whole shelf as well as the drag handle (in certain circumstances) to give users a hint that gestures can be performed. In that sense, even though the shelf widget does not actually contain other components, it usually serves as a backdrop for them.
The navigation widget contains the home and back buttons. It is usually shown in clamshell mode (but only with the home button) and hidden in tablet mode, unless the activation of select accessibility features forces it to be shown. When the navigation widget is not shown, the user can achieve the same actions by performing gestures.
The hotseat widget contains icons for application shortcuts and running applications. In clamshell mode, it is always visually contained within the shelf widget; in tablet mode, it can appear and move independently.
The status area widget (whose code lives in ash/system
) shows information such as the clock or current battery level, and can toggle the system tray.
The shelf is aligned to the bottom of the screen by default, but the user can choose (only in clamshell mode) to align it to the left or right of the screen. It always occupies the entirety of the corresponding dimension (width for a horizontal shelf, height otherwise), with the navigation widget shown at the start (top or left in left-to-right interfaces, bottom or right in right-to-left) and the status area at the other end.
The system allows the user to set a boolean preference, on a per-display basis, specifying whether the shelf should “auto-hide”. In that case, the shelf and its components will be hidden from the screen most of the time, unless there are no un-minimized windows or unless the user actively brings up the shelf with the mouse or with a swipe.
The hotseat widget is centered on the screen according to the following principle:
All icons are placed at the center of the whole display if they can fit without overlapping with any other shelf component.
Otherwise, they are centered within the space available to the hotseat.
If there are too many icons to fit in that space, the hotseat becomes scrollable.
The shelf and its components need to adjust to a certain number of changes that may or may not be user-triggered:
Switching between clamshell and tablet mode.
Changing the display size (for smaller displays, the shelf becomes more compact) or orientation.
Changing the shelf alignment.
User events (clicks, taps, swipes).
All shelf components need to react to these changes in a coordinated manner to maintain the smoothness of animations.
Components should not register themselves as observers of these changes and react to them on their own, because an adequate reaction may involve other components as well. For instance, whether the navigation widget is shown (or is scheduled to be shown at the end of the animation) will influence the amount of space the hotseat widget can occupy.
Instead, listening to those changes are handled at the ShelfLayoutManager
level, which is then responsible for making the changes trickling down to each component as necessary.
In reaction to any of these global changes, each component must first determine where it wants to be at the end of the animation (“aim”). That calculation may depend on the other shelf components. Then, and only then, should the change of bounds be actually committed to each widget and the animations triggered (“move”). Failing to respect this “two-phase” approach may lead to janky animations as each component may realize, only after it has started moving, that another component's movement forces it to alter its final destination.
ShelfComponent
interfaceEach of the shelf components exposes an API to other classes in order to ease the process of responding to layout changes:
CalculateTargetBounds
is the “aim” phase, where each component figures out where it wants to be given the new conditions. This method must be called on each component by order of dependency (a component B “depends” on another component A if B needs to know A's target bounds before calculating its own).
GetTargetBounds
allows for components depending on this one to calculate their own target bounds accordingly.
UpdateLayout
is the “move” phase, where each component actually changes it bounds according to its target.
UpdateTargetBoundsForGesture
allows each component to respond to a gesture in progress by determining how (and whether) it should follow other components along in the gesture.
Each shelf component is aware of the set of inputs that can cause its layout to change. Each time the UpdateLayout
method is called on it, it determines whether any of its inputs has changed. If not, the method returns early and avoids any actual re-layout for itself as well as other components that depend solely on it.
In order for keyboard users to navigate smoothly between the various parts of the shelf as they would expect, the ShelfFocusCycler
class passes the focus to each shelf component as appropriate, depending on which component has just reliquished focus and on which direction the focus is going. The ShelfWidget
class is the only shelf component that doesn't receive keyboard focus since it does not have any activatable elements.
The base class for all buttons on shelf components is ShelfButton
, which handles basic logic for keyboard navigation and ink drops. This class is then derived into ShelfControlButton
for things like the home or back button, and ShelfAppButton
for application shortcuts.
Tooltips for elements on the shelf require some specific logic on top of the common tooltips because as a user hovers over each app shortcut, trying to figure out what each one does, we do not want to adopt the default tooltip behavior which would be to dismiss the previous tooltip and make the user wait for the common timeout before showing the next one.