android: put the way out of a note back within reach
Moving the toolbar to the top took the back arrow with it, and left the only exit from a full-screen editor in the top-left corner — the furthest point on the display from a right-handed thumb, reached over the whole note to get to. Reported on the first device pass, and correctly. So the footer carries a Done as well as the timestamp. With the keyboard up it sits directly above the thumb, which is where a hand already is for every other part of writing a note. The top-left arrow stays. Two affordances for one action is usually clutter, but this is the case that earns it: the arrow is what habit, the system back gesture and TalkBack all expect of a full-screen surface, and removing it would strand the reflex to strike a duplicate costing one icon slot. A word rather than a checkmark, on the same argument the overflow menu makes: a tick in a notes app is a checklist item to anyone who has used one, and "Done" cannot be misread, including aloud. EditorSavedLine is now EditorFooter, since it is no longer only a line.
This commit is contained in:
@@ -157,48 +157,70 @@ fun EditorTopBar(
|
||||
}
|
||||
|
||||
/**
|
||||
* When the note was last written, in the bottom corner.
|
||||
* The footer: when the note was last written, and the way out.
|
||||
*
|
||||
* There is no save button. That is right — a note is saved continuously, so a
|
||||
* button offering to do what already happened is a lie with a tap attached — but it
|
||||
* leaves nothing on screen saying the work is safe, and "closing this saves it" is
|
||||
* not a thing anyone should have to be told twice.
|
||||
* **Where the note stands.** There is no save button, and there should not be — a
|
||||
* note is saved continuously, so a button offering to do what already happened is a
|
||||
* lie with a tap attached. But that left nothing on screen saying the work is safe,
|
||||
* and "closing this keeps it" is not a thing anyone should have to be told twice. So
|
||||
* the state says it, as a fact rather than an instruction: Not saved yet → Saving… →
|
||||
* Edited just now is the whole lifecycle, and someone who watches it once never has
|
||||
* to wonder again.
|
||||
*
|
||||
* So the state says it instead, and it says it as a fact rather than an
|
||||
* instruction. Not saved yet → Saving… → Edited just now is the whole lifecycle,
|
||||
* visible in the corner, and someone who watches it once never has to wonder again.
|
||||
* **The way out.** Down here because of where hands are. Moving the toolbar to the
|
||||
* top took the back arrow with it, which left the only exit from a full-screen
|
||||
* editor in the top-left corner — the furthest point on the display from a
|
||||
* right-handed thumb, and reached over the whole note to get to. The operator hit
|
||||
* that on the first device pass and was right to. So the exit lives in the bottom
|
||||
* corner, which with the keyboard up sits directly above it.
|
||||
*
|
||||
* The top-left arrow stays as well. Two affordances for one action is usually
|
||||
* clutter, but this is the case that earns it: the arrow is what habit, the system
|
||||
* back gesture and TalkBack all expect of a full-screen surface, and removing it
|
||||
* would strand the reflex to strike a duplicate that costs one icon slot.
|
||||
*
|
||||
* A WORD rather than a checkmark, like the overflow menu below and for the same
|
||||
* reason. A tick in a notes app is a checklist item to anyone who has used one, and
|
||||
* "Done" cannot be misread — including aloud.
|
||||
*
|
||||
* [DateUtils] rather than a hand-rolled formatter: it is localised, it already
|
||||
* knows the difference between minutes, hours and yesterday, and getting plurals
|
||||
* right in every language is not this app's problem to solve twice.
|
||||
*/
|
||||
@Composable
|
||||
fun EditorSavedLine(
|
||||
fun EditorFooter(
|
||||
updatedAt: String?,
|
||||
saving: Boolean,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
// Rides above the keyboard, like the bar it replaced. The content
|
||||
// Column deliberately does not also inset for the IME: Scaffold
|
||||
// measures this at its lifted height and passes the inset down.
|
||||
// Rides above the keyboard, like the bar that used to be here. The
|
||||
// content Column deliberately does not also inset for the IME:
|
||||
// Scaffold measures this row at its lifted height and passes the
|
||||
// inset down.
|
||||
.imePadding()
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
// Asymmetric: the button brings its own padding, the text does not.
|
||||
.padding(start = 16.dp, end = 4.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = savedLabel(updatedAt, saving),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
TextButton(onClick = onClose) {
|
||||
Text(stringResource(R.string.editor_done))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The three things [EditorSavedLine] can be saying, in the order it says them. */
|
||||
/** The three things the footer can be saying, in the order it says them. */
|
||||
@Composable
|
||||
private fun savedLabel(
|
||||
updatedAt: String?,
|
||||
|
||||
@@ -53,8 +53,9 @@ import kotlinx.coroutines.delay
|
||||
* opening a note reads as the same object growing to fill the display.
|
||||
*
|
||||
* No save button, deliberately. Writes are continuous, so a button offering to do
|
||||
* what already happened would be a lie with a tap attached; [EditorSavedLine] in the
|
||||
* bottom corner says the same thing as a fact instead.
|
||||
* what already happened would be a lie with a tap attached; [EditorFooter] in the
|
||||
* bottom corner says the same thing as a fact instead, beside the Done that
|
||||
* leaves.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -182,16 +183,23 @@ fun NoteEditorScreen(
|
||||
onAction = onAction,
|
||||
)
|
||||
},
|
||||
// Where the action bar used to be. A note is saved as it is written,
|
||||
// so what belongs at the bottom of the screen is not a control but
|
||||
// the answer to "did that land" — see [EditorSavedLine].
|
||||
bottomBar = { EditorSavedLine(updatedAt = note.updatedAt, saving = saving) },
|
||||
// Where the action bar used to be, carrying the two things that
|
||||
// belong within reach of a thumb: whether the note is safe, and the
|
||||
// way out. See [EditorFooter] for why the exit is down here and not
|
||||
// only in the top-left corner.
|
||||
bottomBar = {
|
||||
EditorFooter(
|
||||
updatedAt = note.updatedAt,
|
||||
saving = saving,
|
||||
onClose = leave,
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
// No imePadding here: EditorSavedLine carries it, so
|
||||
// No imePadding here: EditorFooter carries it, so
|
||||
// Scaffold measures that row at its keyboard-lifted
|
||||
// height and the inset already reaches this Column
|
||||
// through `padding`. Adding it again would inset for the
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
<string name="editor_unsaved">Not saved yet</string>
|
||||
<string name="editor_edited">Edited %1$s</string>
|
||||
<string name="editor_just_now">just now</string>
|
||||
<string name="editor_done">Done</string>
|
||||
<string name="editor_pin">Pin</string>
|
||||
<string name="editor_unpin">Unpin</string>
|
||||
<string name="editor_labels">Labels…</string>
|
||||
|
||||
Reference in New Issue
Block a user