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
|
* **Where the note stands.** There is no save button, and there should not be — a
|
||||||
* button offering to do what already happened is a lie with a tap attached — but it
|
* note is saved continuously, so a button offering to do what already happened is a
|
||||||
* leaves nothing on screen saying the work is safe, and "closing this saves it" is
|
* lie with a tap attached. But that left nothing on screen saying the work is safe,
|
||||||
* not a thing anyone should have to be told twice.
|
* 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
|
* **The way out.** Down here because of where hands are. Moving the toolbar to the
|
||||||
* instruction. Not saved yet → Saving… → Edited just now is the whole lifecycle,
|
* top took the back arrow with it, which left the only exit from a full-screen
|
||||||
* visible in the corner, and someone who watches it once never has to wonder again.
|
* 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
|
* [DateUtils] rather than a hand-rolled formatter: it is localised, it already
|
||||||
* knows the difference between minutes, hours and yesterday, and getting plurals
|
* knows the difference between minutes, hours and yesterday, and getting plurals
|
||||||
* right in every language is not this app's problem to solve twice.
|
* right in every language is not this app's problem to solve twice.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun EditorSavedLine(
|
fun EditorFooter(
|
||||||
updatedAt: String?,
|
updatedAt: String?,
|
||||||
saving: Boolean,
|
saving: Boolean,
|
||||||
|
onClose: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
// Rides above the keyboard, like the bar it replaced. The content
|
// Rides above the keyboard, like the bar that used to be here. The
|
||||||
// Column deliberately does not also inset for the IME: Scaffold
|
// content Column deliberately does not also inset for the IME:
|
||||||
// measures this at its lifted height and passes the inset down.
|
// Scaffold measures this row at its lifted height and passes the
|
||||||
|
// inset down.
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.navigationBarsPadding()
|
.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,
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = savedLabel(updatedAt, saving),
|
text = savedLabel(updatedAt, saving),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
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
|
@Composable
|
||||||
private fun savedLabel(
|
private fun savedLabel(
|
||||||
updatedAt: String?,
|
updatedAt: String?,
|
||||||
|
|||||||
@@ -53,8 +53,9 @@ import kotlinx.coroutines.delay
|
|||||||
* opening a note reads as the same object growing to fill the display.
|
* 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
|
* 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
|
* what already happened would be a lie with a tap attached; [EditorFooter] in the
|
||||||
* bottom corner says the same thing as a fact instead.
|
* bottom corner says the same thing as a fact instead, beside the Done that
|
||||||
|
* leaves.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -182,16 +183,23 @@ fun NoteEditorScreen(
|
|||||||
onAction = onAction,
|
onAction = onAction,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
// Where the action bar used to be. A note is saved as it is written,
|
// Where the action bar used to be, carrying the two things that
|
||||||
// so what belongs at the bottom of the screen is not a control but
|
// belong within reach of a thumb: whether the note is safe, and the
|
||||||
// the answer to "did that land" — see [EditorSavedLine].
|
// way out. See [EditorFooter] for why the exit is down here and not
|
||||||
bottomBar = { EditorSavedLine(updatedAt = note.updatedAt, saving = saving) },
|
// only in the top-left corner.
|
||||||
|
bottomBar = {
|
||||||
|
EditorFooter(
|
||||||
|
updatedAt = note.updatedAt,
|
||||||
|
saving = saving,
|
||||||
|
onClose = leave,
|
||||||
|
)
|
||||||
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
// No imePadding here: EditorSavedLine carries it, so
|
// No imePadding here: EditorFooter carries it, so
|
||||||
// Scaffold measures that row at its keyboard-lifted
|
// Scaffold measures that row at its keyboard-lifted
|
||||||
// height and the inset already reaches this Column
|
// height and the inset already reaches this Column
|
||||||
// through `padding`. Adding it again would inset for the
|
// through `padding`. Adding it again would inset for the
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
<string name="editor_unsaved">Not saved yet</string>
|
<string name="editor_unsaved">Not saved yet</string>
|
||||||
<string name="editor_edited">Edited %1$s</string>
|
<string name="editor_edited">Edited %1$s</string>
|
||||||
<string name="editor_just_now">just now</string>
|
<string name="editor_just_now">just now</string>
|
||||||
|
<string name="editor_done">Done</string>
|
||||||
<string name="editor_pin">Pin</string>
|
<string name="editor_pin">Pin</string>
|
||||||
<string name="editor_unpin">Unpin</string>
|
<string name="editor_unpin">Unpin</string>
|
||||||
<string name="editor_labels">Labels…</string>
|
<string name="editor_labels">Labels…</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user