Evernote (iOS) — Jetpack Compose Implementation Guide
Companion to DESIGN.md — the framework-neutral spec. This file ports Evernote’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, the note editor + checklist + FAB, motion, and haptics.
Why a Compose guide for an iOS-referenced app? The DESIGN.md tokens are platform-neutral. This file keeps the visual identity (Evernote’s calm neutral canvas, the single brand green, green tag pills, the raised circular FAB, roomy reading rhythm) while making everything idiomatic Android —
NavigationBar+ a centerFloatingActionButton(docked) instead of a UITabBar with embedded FAB,SwipeToDismissBoxfor swipe actions,sp/dpinstead ofpt.
Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for thumbnails. No Material You dynamic color — Evernote’s brand green is fixed; a full dark scheme is provided.
1. Color Tokens
// ui/theme/EvernoteColors.kt
import androidx.compose.ui.graphics.Color
object EverColors {
// Brand (interactive)
val Green = Color(0xFF00A82D)
val GreenBright = Color(0xFF2DBE60) // interactive on dark
val GreenPressed = Color(0xFF007A20)
val GreenDeep = Color(0xFF108A00)
val Ink = Color(0xFF1C2B33)
// Canvas & Surfaces (Light)
val Canvas = Color(0xFFFFFFFF)
val OffWhite = Color(0xFFFAFAFA)
val Pressed = Color(0xFFF1F2F2)
val Divider = Color(0xFFE4E5E5)
val Border = Color(0xFFDCDEDE)
// Canvas & Surfaces (Dark) — true near-black
val DarkCanvas = Color(0xFF1C1C1E)
val DarkSurface1 = Color(0xFF242426)
val DarkSurface2 = Color(0xFF2E2E30)
val DarkSurface3 = Color(0xFF3A3A3C)
val DarkDivider = Color(0xFF38383A)
// Text
val TextPrimary = Color(0xFF1C2B33) // Evernote warm ink — NOT pure black
val TextSecondary = Color(0xFF5C6970)
val TextTertiary = Color(0xFF8C969C)
val DarkTextPrimary = Color(0xFFE4E4E6) // NOT pure white — reading comfort
val DarkTextSecondary = Color(0xFF9A9A9E)
val DarkTextTertiary = Color(0xFF6E6E72)
// Tag chips
val TagBgLight = Color(0xFFE6F4EA); val TagTextLight = Color(0xFF1F7A33)
val TagBgDark = Color(0xFF1C3A24); val TagTextDark = Color(0xFF5FD68A)
// Semantic
val Blue = Color(0xFF2F80ED)
val Warn = Color(0xFFF6A609)
val Error = Color(0xFFE5484D)
val Yellow = Color(0xFFF6C544)
val HiLight = Color(0xFFFFF3B0)
val HiDark = Color(0xFF5A4A12)
// Notebook accents
val NbBlue = Color(0xFF2F80ED)
val NbYellow = Color(0xFFF6C544)
val NbRed = Color(0xFFE5484D)
val NbPurple = Color(0xFF7A5AF8)
val NbTeal = Color(0xFF1AAE9F)
}
Wire it into both schemes. Evernote is light-first; the dark scheme uses a true near-black #1C1C1E for reading comfort and brightens interactive green.
// ui/theme/Theme.kt
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
private val EverLight = lightColorScheme(
primary = EverColors.Green,
onPrimary = Color.White,
background = EverColors.OffWhite,
onBackground = EverColors.TextPrimary,
surface = EverColors.Canvas,
onSurface = EverColors.TextPrimary,
surfaceVariant = EverColors.Pressed,
outline = EverColors.Divider,
error = EverColors.Error,
)
private val EverDark = darkColorScheme(
primary = EverColors.GreenBright,
onPrimary = Color.White,
background = EverColors.DarkCanvas,
onBackground = EverColors.DarkTextPrimary,
surface = EverColors.DarkSurface1,
onSurface = EverColors.DarkTextPrimary,
surfaceVariant = EverColors.DarkSurface2,
outline = EverColors.DarkDivider,
error = EverColors.Error,
)
@Composable
fun EvernoteTheme(
dark: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) = MaterialTheme(
colorScheme = if (dark) EverDark else EverLight,
typography = EverTypography,
content = content,
)
2. Typography
Evernote uses the iOS system font; on Android use Inter (SIL OFL) in res/font/ for cross-platform parity. Body is 16sp at 26sp line-height (≈1.6) — reading comfort first.
// ui/theme/EverType.kt
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val Inter = FontFamily(
Font(R.font.inter_regular, FontWeight.Normal),
Font(R.font.inter_medium, FontWeight.Medium),
Font(R.font.inter_semibold, FontWeight.SemiBold),
Font(R.font.inter_bold, FontWeight.Bold),
)
object EverText {
val ScreenTitle = TextStyle(Inter, fontWeight = FontWeight.Bold, fontSize = 30.sp, lineHeight = 36.sp, letterSpacing = (-0.4).sp)
val NoteTitle = TextStyle(Inter, fontWeight = FontWeight.Bold, fontSize = 24.sp, lineHeight = 30.sp, letterSpacing = (-0.3).sp)
val NoteH1 = TextStyle(Inter, fontWeight = FontWeight.Bold, fontSize = 20.sp, lineHeight = 26.sp, letterSpacing = (-0.2).sp)
val NoteH2 = TextStyle(Inter, fontWeight = FontWeight.Bold, fontSize = 17.sp, lineHeight = 23.sp, letterSpacing = (-0.1).sp)
val Body = TextStyle(Inter, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 26.sp) // ≈1.6
val ListTitle = TextStyle(Inter, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, lineHeight = 20.sp)
val Checklist = TextStyle(Inter, fontWeight = FontWeight.Normal, fontSize = 15.sp, lineHeight = 22.sp)
val Snippet = TextStyle(Inter, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp)
val Tag = TextStyle(Inter, fontWeight = FontWeight.SemiBold, fontSize = 12.sp, lineHeight = 14.sp, letterSpacing = 0.1.sp)
val Meta = TextStyle(Inter, fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 16.sp)
val Button = TextStyle(Inter, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, lineHeight = 15.sp)
val Tab = TextStyle(Inter, fontWeight = FontWeight.Medium, fontSize = 10.sp, lineHeight = 11.sp, letterSpacing = 0.1.sp)
val Code = TextStyle(FontFamily.Monospace, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 21.sp)
}
val EverTypography = Typography(
headlineLarge = EverText.ScreenTitle,
headlineMedium = EverText.NoteTitle,
titleMedium = EverText.NoteH2,
bodyMedium = EverText.Body,
labelSmall = EverText.Tab,
)
3. Signature Components
Tag Pill
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
@Composable
fun TagPill(text: String, dark: Boolean = isSystemInDarkTheme()) {
Box(
Modifier
.clip(RoundedCornerShape(999.dp))
.background(if (dark) EverColors.TagBgDark else EverColors.TagBgLight)
.padding(horizontal = 9.dp, vertical = 3.dp)
) {
Text(text, style = EverText.Tag, color = if (dark) EverColors.TagTextDark else EverColors.TagTextLight)
}
}
Checklist Item
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextDecoration
@Composable
fun ChecklistItem(initialDone: Boolean = false, text: String) {
val dark = isSystemInDarkTheme()
var done by remember { mutableStateOf(initialDone) }
val haptics = LocalHapticFeedback.current
val pop by animateFloatAsState(if (done) 1.12f else 1f, spring(dampingRatio = 0.4f), label = "pop")
val green = if (dark) EverColors.GreenBright else EverColors.Green
Row(verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Box(
Modifier
.size(44.dp)
.clickable {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
done = !done
},
contentAlignment = Alignment.TopStart,
) {
Box(
Modifier
.size(19.dp)
.scale(if (done) pop else 1f)
.clip(RoundedCornerShape(5.dp))
.then(if (done) Modifier.background(green)
else Modifier.border(2.dp, EverColors.TextTertiary, RoundedCornerShape(5.dp))),
contentAlignment = Alignment.Center,
) {
if (done) Icon(Icons.Filled.Check, contentDescription = null, tint = Color.White, modifier = Modifier.size(11.dp))
}
}
Text(
text,
style = EverText.Checklist.copy(textDecoration = if (done) TextDecoration.LineThrough else TextDecoration.None),
color = if (done) EverColors.TextTertiary
else if (dark) EverColors.DarkTextPrimary else EverColors.TextPrimary,
)
}
}
Note List Row
import androidx.compose.material3.HorizontalDivider
import coil.compose.AsyncImage
@Composable
fun NoteRow(
title: String, snippet: String, date: String, notebook: String, thumbUrl: String? = null,
) {
val dark = isSystemInDarkTheme()
Column {
Row(
Modifier.fillMaxWidth().padding(vertical = 14.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Column(Modifier.weight(1f)) {
Text(title, style = EverText.ListTitle, maxLines = 1,
color = if (dark) EverColors.DarkTextPrimary else EverColors.TextPrimary)
Text(snippet, style = EverText.Snippet, maxLines = 2,
color = if (dark) EverColors.DarkTextSecondary else EverColors.TextSecondary)
Text("$date · $notebook", style = EverText.Meta, modifier = Modifier.padding(top = 3.dp),
color = if (dark) EverColors.DarkTextTertiary else EverColors.TextTertiary)
}
if (thumbUrl != null) {
AsyncImage(
model = thumbUrl, contentDescription = null,
modifier = Modifier.size(52.dp).clip(RoundedCornerShape(8.dp))
.background(if (dark) EverColors.DarkSurface3 else EverColors.Pressed),
)
}
}
HorizontalDivider(color = if (dark) EverColors.DarkDivider else EverColors.Divider, thickness = 1.dp)
}
}
Format Toolbar (bottom accessory)
import androidx.compose.material.icons.filled.*
@Composable
fun FormatToolbar() {
val dark = isSystemInDarkTheme()
val tools = listOf(
Icons.Filled.FormatBold, Icons.Filled.FormatItalic, Icons.Filled.FormatListBulleted,
Icons.Filled.CheckBox, Icons.Filled.Title, Icons.Filled.AttachFile, Icons.Filled.Link,
)
Row(
Modifier.fillMaxWidth()
.background(if (dark) EverColors.DarkSurface2 else EverColors.Canvas)
.padding(vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
) {
tools.forEach {
Icon(it, contentDescription = null, modifier = Modifier.size(20.dp),
tint = if (dark) EverColors.DarkTextSecondary else EverColors.TextSecondary)
}
}
}
4. Navigation
Evernote has a 5-slot bottom strip with a raised green circular new-note button in the center. On Android, model this as a Scaffold with a BottomAppBar/NavigationBar plus a center-docked FloatingActionButton. No tint pill — active is brand green.
import androidx.compose.material3.*
@Composable
fun EvernoteBottomBar(selected: Int, onSelect: (Int) -> Unit, onNewNote: () -> Unit) {
val dark = isSystemInDarkTheme()
Box {
NavigationBar(containerColor = MaterialTheme.colorScheme.surface, tonalElevation = 0.dp) {
val items = listOf(
"Home" to Icons.Filled.Home,
"Notes" to Icons.Filled.Description,
null to null, // FAB gap
"Notebooks" to Icons.Filled.LibraryBooks,
"Search" to Icons.Filled.Search,
)
items.forEachIndexed { i, pair ->
if (pair.first == null) {
NavigationBarItem(selected = false, onClick = {}, enabled = false, icon = {}, label = {})
} else {
NavigationBarItem(
selected = selected == i,
onClick = { onSelect(i) },
icon = { Icon(pair.second!!, contentDescription = pair.first, modifier = Modifier.size(24.dp)) },
label = { Text(pair.first!!, style = EverText.Tab) },
colors = NavigationBarItemDefaults.colors(
selectedIconColor = if (dark) EverColors.GreenBright else EverColors.Green,
selectedTextColor = if (dark) EverColors.GreenBright else EverColors.Green,
unselectedIconColor = EverColors.TextTertiary,
unselectedTextColor = EverColors.TextTertiary,
indicatorColor = Color.Transparent, // no Material pill
),
)
}
}
}
FloatingActionButton(
onClick = onNewNote,
containerColor = EverColors.Green,
contentColor = Color.White,
shape = CircleShape,
modifier = Modifier.align(Alignment.TopCenter).offset(y = (-22).dp).size(52.dp),
) { Icon(Icons.Filled.Add, contentDescription = "New note") }
}
}
Tapping the FAB opens a ModalBottomSheet (20.dp top radius) with capture rows: Text Note, Camera, Photo, Audio, Reminder — each a tall row with a leading green icon. The notebooks screen lists notebook rows (spine icon + name + count) and stacks as expandable group headers.
5. Motion
Evernote motion is calm — 150–300ms ease-out / soft spring, never aggressive.
| Moment | Compose recipe |
|---|---|
| Checkbox toggle | box scale via animateFloatAsState(spring(dampingRatio = 0.4f)) + strikethrough applied; light haptic |
| Note open (list → editor) | Nav3/NavHost slide push tween(300) |
| FAB → capture sheet | ModalBottomSheet default slide-up (≈300ms); FAB press scale 0.96 via interactionSource |
| Sync toast | Snackbar / AnimatedVisibility fadeIn + slideInVertically(tween(200)), auto-dismiss 2s |
| Stack expand | chevron animateFloatAsState 0→90° tween(150); rows expandVertically(tween(200)) + fadeIn |
| Tag add | AnimatedVisibility scaleIn(initialScale = 0.8f, tween(150)) + fadeIn |
| Swipe actions | SwipeToDismissBox — leading pin/move, trailing delete; medium haptic on commit |
// Checkbox pop — the canonical Evernote micro-motion
val pop by animateFloatAsState(if (done) 1.12f else 1f, spring(dampingRatio = 0.4f), label = "pop")
Haptics: LocalHapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) for the light tick on checkbox toggle and tab change; medium-equivalent on swipe-action commit and note delete; HapticFeedbackConstants.CLOCK_TICK for the soft FAB long-press. Auto-save is silent — only show a Snackbar on explicit save or sync error.
6. Icons
Evernote’s UI maps to filled Material icons; brand green carries state. Add androidx.compose.material:material-icons-extended for the format set.
| Purpose | SF Symbol (iOS) | Material Icon (Compose) |
|---|---|---|
| Home (tab) | house |
Icons.Filled.Home |
| Notes (tab) | note.text |
Icons.Filled.Description |
| FAB / new note | plus |
Icons.Filled.Add |
| Notebooks (tab) | books.vertical |
Icons.Filled.LibraryBooks |
| Search (tab) | magnifyingglass |
Icons.Filled.Search |
| Back | chevron.left |
Icons.Filled.ArrowBack |
| Overflow | ellipsis |
Icons.Filled.MoreHoriz |
| Note info | info.circle |
Icons.Filled.InfoOutline |
| Checkbox empty | square |
Icons.Outlined.CheckBoxOutlineBlank |
| Checkbox done | checkmark.square.fill |
Icons.Filled.CheckBox |
| Bold | bold |
Icons.Filled.FormatBold |
| Italic | italic |
Icons.Filled.FormatItalic |
| List | list.bullet |
Icons.Filled.FormatListBulleted |
| Checklist | checklist |
Icons.Filled.CheckBox |
| Heading | textformat.size |
Icons.Filled.Title |
| Attach | paperclip |
Icons.Filled.AttachFile |
| Camera / scan | doc.viewfinder |
Icons.Filled.DocumentScanner |
| Audio | mic |
Icons.Filled.Mic |
| Link | link |
Icons.Filled.Link |
| Highlight | highlighter |
Icons.Filled.FormatColorFill |
| Tag | tag |
Icons.Filled.LocalOffer |
| Pin | pin |
Icons.Filled.PushPin |
| Reminder | clock |
Icons.Filled.Schedule |
| Notebook | book.closed |
Icons.Filled.MenuBook |
| Stack | square.stack |
Icons.Filled.Layers |
7. Minimum SDK & Accessibility Notes
- minSdk 24 (Compose floor 21;
ModalBottomSheet,SwipeToDismissBox, modern motion comfortable at 24).compileSdk 34+, Compose BOM2024.09+, Kotlin2.0+. - Edge-to-edge: call
enableEdgeToEdge(); light canvas wants dark-content system bars (light-content in dark mode). The docked FAB must clear the gesture nav inset. - Font scaling:
sphonors the user’s scale — keep it on titles, headings, body, snippets, checklist (it stays readable as it scales). Pin layout-sensitive text (10sp tab labels, tag pills, sync-toast) so the bottom strip doesn’t break. - Reading comfort: keep
Body.lineHeight = 26.sp(≈1.6) and 22.dp horizontal editor padding — do not tighten; this is the brand’s core feel. - TalkBack: label note rows “{title}, {snippet}, {date}, {notebook}”; checklist items “{text}, checked/unchecked” with a toggle action; the FAB “New note, button”; tag pills “Tag: {name}, filters notes”. Never convey checklist done-state by color alone — strikethrough + dim is the secondary cue.
- Touch targets: Material guidance is 48.dp — the checkbox sits in a 44–48.dp hit
Box; note rows are full-row tappable (≥64.dp); FAB is 52.dp; format-toolbar icons get 48.dp hit padding. - Contrast:
#1C2B33on#FFFFFFand#E4E4E6on#1C1C1Epass WCAG AA for body; use#2DBE60(not#00A82D) for small interactive green on the dark canvas to clear AA. - Reduce motion: when
Settings.Global.ANIMATOR_DURATION_SCALE == 0, disable the checkbox pop and stack slide — substituteCrossfade; keep instant state changes. - Dark mode: invert via the
Dark*palette — the dark canvas is a true near-black#1C1C1E, NOT charcoal-gray;#1C2B33text →#E4E4E6(not pure white). Shadows nearly vanish on dark, so signal elevation via the surface step (#242426→#2E2E30→#3A3A3C) + a 1.dpDarkDividerborder on menus and sheets; the FAB keeps its green-tinted glow. Do not enabledynamicColorScheme()— Evernote’s single brand green must hold regardless of wallpaper.