brand: loading state, themes, manifest, art preview, pairing screen updates
This commit is contained in:
parent
fed91dbe46
commit
95cad84cb5
|
|
@ -75,3 +75,5 @@ ClaudeReport.md
|
|||
docs/brand/visual-identity.md
|
||||
docs/brand/asset-system.md
|
||||
docs/brand/visual-identity.md
|
||||
docs/brand/asset-system.md
|
||||
ClaudeBrandingReview.md
|
||||
|
|
|
|||
|
|
@ -99,8 +99,9 @@ Legend: ✅ on-brand / no art needed · ➕ add/þwire art (prompt below) ·
|
|||
Priority order = the moments a user feels most. Each is self-contained after you paste the House Style block first.
|
||||
|
||||
**A1 · Pairing success celebration** — *1:1, transparent bg.*
|
||||
> Scene: two equal heart-halves (soft pink + soft lavender) sliding together into one whole heart at the center, with
|
||||
> a gentle burst of small floating hearts and petals around it. Convey "you're connected now." Calm, joyful, no text.
|
||||
> Scene: the Closer mark resolving into place — a soft-pink upper C-arc and a lavender lower sweep curving together to
|
||||
> enclose a heart-shaped space with a small keyhole at its center — with a gentle burst of small floating hearts and
|
||||
> petals around it. Convey "you're connected now." Calm, joyful, no text. (White keyhole if on a dark background.)
|
||||
|
||||
**A2 · Answer history empty state** — *1:1.*
|
||||
> Scene: a small soft journal/photo-album, slightly open, with two paired cards tucked inside and a few faint floating
|
||||
|
|
@ -151,7 +152,7 @@ Priority order = the moments a user feels most. Each is self-contained after you
|
|||
> alarm imagery.
|
||||
|
||||
**G-set · Notification + relationship glyphs** — *single-color vector, square, legible at 24 dp.* (One prompt each:)
|
||||
> Simple single-color flat glyph in the Closer style, no text, no background: **heart-of-two-halves**, **paired sealed
|
||||
> Simple single-color flat glyph in the Closer style, no text, no background: **the Closer C-heart-keyhole mark**, **paired sealed
|
||||
> cards**, **daily card**, **sealed answer (card with small lock)**, **memory capsule**, **date-card with heart**,
|
||||
> **quiet-hours moon**, **couple-premium (heart + small crown/spark, tasteful)**, **export-data**, **delete-account**.
|
||||
> Export in lavender for in-app and as high-contrast monochrome for the platform notification glyph.
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ dependencies {
|
|||
implementation(composeBom)
|
||||
|
||||
implementation("androidx.core:core-ktx:1.15.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
|
||||
implementation("androidx.activity:activity-compose:1.9.3")
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.Closer">
|
||||
android:theme="@style/Theme.Closer.Splash">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -62,7 +63,21 @@ class MainActivity : AppCompatActivity() {
|
|||
private val pendingDeepLink = mutableStateOf<String?>(null)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
// Gentle scale-up + fade exit, handing off to the animated in-app logo loader.
|
||||
splashScreen.setOnExitAnimationListener { provider ->
|
||||
provider.iconView.animate()
|
||||
.scaleX(1.15f)
|
||||
.scaleY(1.15f)
|
||||
.setDuration(300L)
|
||||
.start()
|
||||
provider.view.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(300L)
|
||||
.withEndAction { provider.remove() }
|
||||
.start()
|
||||
}
|
||||
maybeRequestNotificationPermission()
|
||||
registerFcmToken()
|
||||
pendingDeepLink.value = deepLinkRouteFromIntent(intent)
|
||||
|
|
|
|||
|
|
@ -4,29 +4,43 @@ import app.closer.R
|
|||
import app.closer.ui.theme.closerCardColor
|
||||
import android.provider.Settings
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.imageResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
|
|
@ -68,6 +82,15 @@ fun CloserHeartLoader(
|
|||
CloserMarkLoader(modifier = modifier, size = size)
|
||||
}
|
||||
|
||||
/** Below this size we use a lightweight inline spinner instead of the full app-icon chip. */
|
||||
private val ChipLoaderMinSize = 40.dp
|
||||
|
||||
/**
|
||||
* The brand loader. At prominent sizes it renders the **white-keyhole logo on an aubergine app-icon
|
||||
* chip** with a soft fill that rises through the mark (the "charging the connection" motion). At small
|
||||
* inline sizes (e.g. inside buttons) it falls back to a lightweight indeterminate arc tinted to the
|
||||
* current content color, so it never shows a dark square on a colored button. Honors "remove animations".
|
||||
*/
|
||||
@Composable
|
||||
fun CloserMarkLoader(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -81,21 +104,48 @@ fun CloserMarkLoader(
|
|||
1f
|
||||
) == 0f
|
||||
}
|
||||
if (size >= ChipLoaderMinSize) {
|
||||
CloserMarkChipLoader(modifier = modifier, size = size, reducedMotion = reducedMotion)
|
||||
} else {
|
||||
CloserInlineSpinner(
|
||||
modifier = modifier,
|
||||
size = size,
|
||||
color = LocalContentColor.current,
|
||||
reducedMotion = reducedMotion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CloserMarkChipLoader(
|
||||
modifier: Modifier,
|
||||
size: Dp,
|
||||
reducedMotion: Boolean
|
||||
) {
|
||||
val transition = rememberInfiniteTransition(label = "closerMarkLoader")
|
||||
val animatedPulse = transition.animateFloat(
|
||||
initialValue = 0.96f,
|
||||
targetValue = 1.04f,
|
||||
val fill = if (reducedMotion) 1f else transition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 1500, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
),
|
||||
label = "closerMarkFill"
|
||||
).value
|
||||
val pulse = if (reducedMotion) 1f else transition.animateFloat(
|
||||
initialValue = 0.97f,
|
||||
targetValue = 1.03f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 900, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "closerMarkPulse"
|
||||
)
|
||||
val pulse = if (reducedMotion) 1f else animatedPulse.value
|
||||
).value
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.closer_mark_loader),
|
||||
contentDescription = null,
|
||||
// White-keyhole mark (already safe-zone padded) drawn over the aubergine app-icon chip.
|
||||
val mark = ImageBitmap.imageResource(R.drawable.closer_launcher_foreground)
|
||||
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.graphicsLayer {
|
||||
|
|
@ -103,5 +153,88 @@ fun CloserMarkLoader(
|
|||
scaleY = pulse
|
||||
}
|
||||
.clearAndSetSemantics {}
|
||||
)
|
||||
) {
|
||||
val w = this.size.width
|
||||
val h = this.size.height
|
||||
val corner = CornerRadius(w * 0.22f, h * 0.22f)
|
||||
// Aubergine gradient chip (matches ic_launcher_background palette).
|
||||
drawRoundRect(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(Color(0xFF5A2F74), Color(0xFF3A1D4D), Color(0xFF24122F)),
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(w, h)
|
||||
),
|
||||
cornerRadius = corner
|
||||
)
|
||||
val src = IntSize(mark.width, mark.height)
|
||||
val dst = IntSize(w.toInt(), h.toInt())
|
||||
// Ghost of the full mark…
|
||||
drawImage(
|
||||
image = mark,
|
||||
srcOffset = IntOffset.Zero,
|
||||
srcSize = src,
|
||||
dstOffset = IntOffset.Zero,
|
||||
dstSize = dst,
|
||||
alpha = 0.16f
|
||||
)
|
||||
// …then the fill rises bottom→top.
|
||||
clipRect(top = h * (1f - fill)) {
|
||||
drawImage(
|
||||
image = mark,
|
||||
srcOffset = IntOffset.Zero,
|
||||
srcSize = src,
|
||||
dstOffset = IntOffset.Zero,
|
||||
dstSize = dst,
|
||||
alpha = 1f
|
||||
)
|
||||
}
|
||||
// Faint ring so the dark chip separates on dark backgrounds.
|
||||
drawRoundRect(
|
||||
color = Color.White.copy(alpha = 0.10f),
|
||||
cornerRadius = corner,
|
||||
style = Stroke(width = 1.dp.toPx())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CloserInlineSpinner(
|
||||
modifier: Modifier,
|
||||
size: Dp,
|
||||
color: Color,
|
||||
reducedMotion: Boolean
|
||||
) {
|
||||
val angle = if (reducedMotion) 0f else rememberInfiniteTransition(label = "closerInlineSpinner")
|
||||
.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 360f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 900, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
),
|
||||
label = "closerInlineAngle"
|
||||
).value
|
||||
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.clearAndSetSemantics {}
|
||||
) {
|
||||
val stroke = this.size.minDimension * 0.12f
|
||||
val inset = stroke / 2f
|
||||
val d = this.size.minDimension - stroke
|
||||
rotate(degrees = angle) {
|
||||
drawArc(
|
||||
brush = Brush.sweepGradient(
|
||||
colors = listOf(color.copy(alpha = 0.12f), color)
|
||||
),
|
||||
startAngle = 0f,
|
||||
sweepAngle = 270f,
|
||||
useCenter = false,
|
||||
topLeft = Offset(inset, inset),
|
||||
size = Size(d, d),
|
||||
style = Stroke(width = stroke, cap = StrokeCap.Round)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp
|
|||
import app.closer.R
|
||||
import app.closer.ui.components.CelebrationOverlay
|
||||
import app.closer.ui.components.CloserActionButton
|
||||
import app.closer.ui.components.CloserMarkLoader
|
||||
import app.closer.ui.components.CloserButtonStyle
|
||||
import app.closer.ui.components.CloserCard
|
||||
import app.closer.ui.settings.SettingsMuted
|
||||
|
|
@ -84,6 +85,16 @@ fun ArtPreviewScreen(onNavigate: (String) -> Unit = {}) {
|
|||
Image(painterResource(R.drawable.particle_petal), null, Modifier.size(56.dp))
|
||||
}
|
||||
}
|
||||
ArtCard("Loading mark (animated)") {
|
||||
androidx.compose.foundation.layout.Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CloserMarkLoader(size = 76.dp) // chip + rising fill
|
||||
CloserMarkLoader(size = 40.dp) // chip threshold
|
||||
CloserMarkLoader(size = 24.dp) // inline arc
|
||||
}
|
||||
}
|
||||
|
||||
CloserActionButton(
|
||||
label = "Play the celebration",
|
||||
|
|
|
|||
|
|
@ -203,19 +203,25 @@ fun PairingSuccessScreen(
|
|||
.size(60.dp)
|
||||
.zIndex(2f)
|
||||
.align(Alignment.Center)
|
||||
.scale(pulse)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(5.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFFFFF8FC)),
|
||||
.padding(4.dp)
|
||||
.clip(CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// White-keyhole app-icon chip: aubergine gradient + the brand mark.
|
||||
Image(
|
||||
painter = painterResource(R.drawable.closer_mark_loader),
|
||||
painter = painterResource(R.drawable.ic_launcher_background),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(38.dp)
|
||||
.scale(pulse)
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.matchParentSize()
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.matchParentSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Cold-launch splash background: brand aubergine, behind the white-keyhole icon. -->
|
||||
<color name="splash_background">#FF24122F</color>
|
||||
</resources>
|
||||
|
|
@ -1,4 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Closer" parent="Theme.AppCompat.Light.NoActionBar" />
|
||||
|
||||
<!-- Cold-launch splash: aubergine background + the white-keyhole app icon, then hands off to
|
||||
Theme.Closer (and the animated in-app logo loader). Splash attrs are from core-splashscreen
|
||||
(no android: prefix) so they back-compat down to the minSdk. -->
|
||||
<style name="Theme.Closer.Splash" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/splash_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.Closer</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -97,18 +97,17 @@ masters (do not ship them directly); ship the exports and the wired Android rast
|
|||
| `docs/brand/sources/closer-approved-icon-square.png` | Square 1024 master | iOS `AppIcon` 1024, store 1024 |
|
||||
| `docs/brand/sources/closer-mark.svg` | Vector master of the mark | Source for all PNG exports |
|
||||
| `docs/brand/sources/closer-mark-transparent-keyhole-white.png` | Transparent mark, white keyhole (for dark aubergine/purple) | **Source of** `drawable-nodpi/closer_launcher_foreground.png` (adaptive fg over the aubergine bg) + the Auth/Onboarding logo tile (dark surface) |
|
||||
| `docs/brand/sources/closer-mark-transparent-keyhole-aubergine.png` | Transparent mark, dark keyhole (for light/blush surfaces) | **Source of** `drawable-nodpi/closer_mark_loader.png` (all loaders + the PairingSuccess hero badge on blush) |
|
||||
| `docs/brand/sources/closer-mark-transparent-keyhole-aubergine.png` | Transparent mark, dark keyhole (for light/blush surfaces) | Logo on light/blush placements + web/docs exports (no in-app wiring today — in-app loaders now use the white-keyhole chip) |
|
||||
| `docs/brand/sources/closer-mark-transparent-keyhole-black.png` | Mono/max-contrast mark | Print/legal, max-contrast light |
|
||||
| `docs/brand/exports/logo/transparent-keyhole-aubergine/closer-mark-{24..1024}.png` | Sized PNGs, dark keyhole | Web/email/social on light, docs, favicons |
|
||||
| `docs/brand/exports/logo/transparent-keyhole-white/closer-mark-{24..1024}.png` | Sized PNGs, white keyhole | Same, on dark surfaces |
|
||||
| `docs/brand/exports/logo/transparent-keyhole-black/closer-mark-{24..1024}.png` | Sized PNGs, mono | One-color/print/footer |
|
||||
| `docs/brand/exports/logo/social/closer-mark-social-{1024,2048}.png` | Square social avatars / OG | Social profile + open-graph square |
|
||||
| `docs/brand/exports/logo/app-icon/closer-app-icon-512.png` | Play listing icon | `docs/store/app-icon-512.png` (Play Console) |
|
||||
| `app/src/main/res/drawable-nodpi/closer_launcher_foreground.png` | Adaptive **foreground** (white-keyhole mark, safe-zone) | `drawable/ic_launcher_foreground.xml` → `mipmap-anydpi-*/ic_launcher*.xml`; also the Auth/Onboarding logo tile |
|
||||
| `app/src/main/res/drawable-nodpi/closer_launcher_foreground.png` | Adaptive **foreground** (white-keyhole mark, safe-zone) | `drawable/ic_launcher_foreground.xml` → `mipmap-anydpi-*/ic_launcher*.xml`; Auth/Onboarding logo tile; **`CloserMarkLoader` chip + `PairingSuccess` hero** (white-keyhole logo on the aubergine app-icon chip) |
|
||||
| `app/src/main/res/drawable/ic_launcher_background.xml` | Adaptive **background** (aubergine gradient) | `mipmap-anydpi-*/ic_launcher*.xml` |
|
||||
| `app/src/main/res/drawable-nodpi/closer_launcher_monochrome.png` | Themed-icon **monochrome** (grayscale+alpha) | `drawable/ic_launcher_monochrome.xml` → `mipmap-anydpi-v33/*` |
|
||||
| `app/src/main/res/drawable-nodpi/ic_notification_closer.png` | Notification small icon (white+alpha silhouette) | `NotificationHelper`, `PartnerNotificationManager` `setSmallIcon` |
|
||||
| `app/src/main/res/drawable-nodpi/closer_mark_loader.png` | Full-color transparent mark | `CloserMarkLoader` (all loaders), `PairingSuccessScreen` hero, `AuthLogoMark`/Onboarding tile |
|
||||
|
||||
Note: the section-1 rows `closer-mark-on-dark.svg` / `closer-mark-on-light.svg` and the lockup/favicon
|
||||
rows are still **to-build** — the on-disk equivalents today are the `transparent-keyhole-white` (dark)
|
||||
|
|
|
|||
|
|
@ -42,8 +42,11 @@ Use the keyhole color to support contrast while keeping the official icon visual
|
|||
the black-keyhole variant only when maximum contrast is needed.
|
||||
- **Notification small icon:** use the single-color platform glyph; do not use the full-color
|
||||
launcher art.
|
||||
- **Loading mark:** use the aubergine-keyhole variant on light/card surfaces so it stays calmer than
|
||||
the launcher icon.
|
||||
- **Loading mark:** the in-app loader is the **white-keyhole logo on an aubergine app-icon chip** with a
|
||||
soft fill that rises through the mark (a "charging" loop). Below ~40dp (e.g. inside buttons) it falls
|
||||
back to a lightweight indeterminate arc tinted to the surrounding content color.
|
||||
- **Cold-launch splash:** aubergine background + the white-keyhole app icon (static, system-masked), with
|
||||
a scale/fade exit that hands off to the in-app loading mark above.
|
||||
|
||||
## Core colors
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue