Skip to content

Commit fbecb5e

Browse files
committed
feat(theme): full M3 color (dynamic color + content-based palettes); fix invisible/untappable FAB
M3 color, to the bone: - Apply Material 3 dynamic color in ThemedActivity. New DYNAMIC (Material You) theme option seeds the palette from the wallpaper on Android 12+; every other theme seeds a content-based M3 palette from its own colorPrimary, so all M3 roles (primaryContainer/ surfaceVariant/outline/...) are generated correctly on all API levels without hand- authoring 21 palettes. - Add DYNAMIC theme (id 24) to Theme.kt (activity + dialog mapping) and a picker swatch. FAB fix (regressed by M3 BottomAppBar dropping the FAB cradle): - The airplane FAB was declared BEFORE the StatsBar in the CoordinatorLayout, so the bar drew on top and intercepted its touches when connected (invisible + needed a swipe to reach). Reordered the FAB (and its progress) to draw AFTER the bar, and tint it with colorTertiaryContainer/onTertiaryContainer so it contrasts the colorPrimary bar. Verified on-device: launch/connect/disconnect/reconnect clean; the FAB is visible and disconnects on tap while connected without swiping; no crashes.
1 parent 5fe66d9 commit fbecb5e

4 files changed

Lines changed: 82 additions & 25 deletions

File tree

app/src/main/java/io/nekohasekai/sagernet/ui/ThemedActivity.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.nekohasekai.sagernet.ui
33
import android.content.res.Configuration
44
import android.os.Build
55
import android.os.Bundle
6+
import android.util.TypedValue
67
import android.widget.TextView
78
import androidx.annotation.StringRes
89
import androidx.appcompat.app.AppCompatActivity
@@ -12,6 +13,8 @@ import androidx.core.view.WindowCompat
1213
import androidx.core.view.WindowInsetsCompat
1314
import androidx.core.view.updatePadding
1415
import com.google.android.material.appbar.AppBarLayout
16+
import com.google.android.material.color.DynamicColors
17+
import com.google.android.material.color.DynamicColorsOptions
1518
import com.google.android.material.snackbar.Snackbar
1619
import io.nekohasekai.sagernet.R
1720
import io.nekohasekai.sagernet.database.DataStore
@@ -33,6 +36,13 @@ abstract class ThemedActivity : AppCompatActivity {
3336
}
3437
Theme.applyNightTheme()
3538

39+
// Material 3 "to the bone": derive a full, correct M3 tonal palette for the active
40+
// theme. When the user picks the Dynamic (Material You) theme on Android 12+, seed
41+
// from the system wallpaper; otherwise seed from the selected theme's colorPrimary so
42+
// every theme (and pre-12 device) gets proper M3 roles (container/surface-variant/
43+
// outline/...) generated from its seed instead of hand-authored values.
44+
if (!isDialog) applyDynamicColors()
45+
3646
super.onCreate(savedInstanceState)
3747

3848
uiMode = resources.configuration.uiMode
@@ -63,6 +73,39 @@ abstract class ThemedActivity : AppCompatActivity {
6373
themeResId = resId
6474
}
6575

76+
/**
77+
* Apply Material 3 dynamic colors. For the Dynamic (Material You) theme on Android 12+ the
78+
* palette comes from the system wallpaper; for every other theme we seed a content-based
79+
* palette from the theme's own colorPrimary so all M3 roles are generated correctly
80+
* (works on all API levels, no hand-authored per-theme palettes).
81+
*/
82+
private fun applyDynamicColors() {
83+
if (DataStore.appTheme == Theme.DYNAMIC) {
84+
// Wallpaper-based; only takes effect on Android 12+, otherwise a no-op and the
85+
// base theme's colors stand.
86+
DynamicColors.applyToActivityIfAvailable(this)
87+
return
88+
}
89+
val seed = resolveColorAttr(com.google.android.material.R.attr.colorPrimary)
90+
if (seed == 0) return
91+
DynamicColors.applyToActivityIfAvailable(
92+
this,
93+
DynamicColorsOptions.Builder()
94+
.setContentBasedSource(seed)
95+
.build()
96+
)
97+
}
98+
99+
private fun resolveColorAttr(attr: Int): Int {
100+
val tv = TypedValue()
101+
theme.resolveAttribute(attr, tv, true)
102+
return if (tv.resourceId != 0) {
103+
androidx.core.content.ContextCompat.getColor(this, tv.resourceId)
104+
} else {
105+
tv.data
106+
}
107+
}
108+
66109
override fun onConfigurationChanged(newConfig: Configuration) {
67110
super.onConfigurationChanged(newConfig)
68111

app/src/main/java/io/nekohasekai/sagernet/utils/Theme.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ object Theme {
3232
const val BLACK = 21
3333
const val VERDANT_MINT = 22
3434
const val DRACULA = 23
35+
const val DYNAMIC = 24
3536

3637
private fun defaultTheme() = PINK_SSR
3738

@@ -76,6 +77,7 @@ object Theme {
7677
BLACK -> R.style.Theme_SagerNet_Black
7778
VERDANT_MINT -> R.style.Theme_SagerNet_VerdantMint
7879
DRACULA -> R.style.Theme_SagerNet_Dracula
80+
DYNAMIC -> R.style.Theme_SagerNet
7981
else -> getTheme(defaultTheme())
8082
}
8183
}
@@ -105,6 +107,7 @@ object Theme {
105107
BLACK -> R.style.Theme_SagerNet_Dialog_Black
106108
VERDANT_MINT -> R.style.Theme_SagerNet_Dialog_VerdantMint
107109
DRACULA -> R.style.Theme_SagerNet_Dialog_Dracula
110+
DYNAMIC -> R.style.Theme_SagerNet_Dialog
108111
else -> getDialogTheme(defaultTheme())
109112
}
110113
}

app/src/main/res/layout/layout_main.xml

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,6 @@
1919
android:layout_width="match_parent"
2020
android:layout_height="match_parent" />
2121

22-
<!-- We double trackThickness as half of it will be invisible -->
23-
<com.google.android.material.progressindicator.CircularProgressIndicator
24-
android:id="@+id/fabProgress"
25-
android:layout_width="wrap_content"
26-
android:layout_height="wrap_content"
27-
android:max="1"
28-
android:visibility="invisible"
29-
app:indicatorColor="?primaryOrTextSecondary"
30-
app:layout_anchor="@+id/fab"
31-
app:layout_anchorGravity="center"
32-
app:layout_behavior="io.nekohasekai.sagernet.widget.FabProgressBehavior"
33-
app:trackCornerRadius="@dimen/mtrl_progress_track_thickness"
34-
app:trackThickness="8dp" />
35-
36-
<io.nekohasekai.sagernet.widget.ServiceButton
37-
android:id="@+id/fab"
38-
android:layout_width="wrap_content"
39-
android:layout_height="wrap_content"
40-
android:elevation="6dp"
41-
android:nextFocusDown="@+id/stats"
42-
app:backgroundTint="?colorPrimary"
43-
app:layout_anchor="@id/stats"
44-
app:pressedTranslationZ="6dp"
45-
app:srcCompat="@drawable/ic_service_idle" />
46-
4722
<io.nekohasekai.sagernet.widget.StatsBar
4823
android:id="@+id/stats"
4924
android:layout_width="match_parent"
@@ -99,6 +74,38 @@
9974

10075
</io.nekohasekai.sagernet.widget.StatsBar>
10176

77+
<!-- FAB + its progress are declared AFTER the StatsBar so the airplane draws on top
78+
of the bottom bar and wins touch in the overlap region. (M2's BottomAppBar cut a
79+
cradle hole for the FAB; M3's bar fills it, so without this the bar covered and
80+
intercepted the FAB when connected.) -->
81+
<!-- We double trackThickness as half of it will be invisible -->
82+
<com.google.android.material.progressindicator.CircularProgressIndicator
83+
android:id="@+id/fabProgress"
84+
android:layout_width="wrap_content"
85+
android:layout_height="wrap_content"
86+
android:max="1"
87+
android:visibility="invisible"
88+
app:indicatorColor="?primaryOrTextSecondary"
89+
app:layout_anchor="@+id/fab"
90+
app:layout_anchorGravity="center"
91+
app:layout_behavior="io.nekohasekai.sagernet.widget.FabProgressBehavior"
92+
app:trackCornerRadius="@dimen/mtrl_progress_track_thickness"
93+
app:trackThickness="8dp" />
94+
95+
<io.nekohasekai.sagernet.widget.ServiceButton
96+
android:id="@+id/fab"
97+
android:layout_width="wrap_content"
98+
android:layout_height="wrap_content"
99+
android:layout_margin="16dp"
100+
android:elevation="6dp"
101+
android:nextFocusDown="@+id/stats"
102+
app:backgroundTint="?attr/colorTertiaryContainer"
103+
app:tint="?attr/colorOnTertiaryContainer"
104+
app:layout_anchor="@id/stats"
105+
app:layout_anchorGravity="top|end"
106+
app:pressedTranslationZ="6dp"
107+
app:srcCompat="@drawable/ic_service_idle" />
108+
102109
</androidx.coordinatorlayout.widget.CoordinatorLayout>
103110

104111
<com.google.android.material.navigation.NavigationView

app/src/main/res/values/colors.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<item>@color/material_light_black</item>
2828
<item>@color/color_verdant_mint</item>
2929
<item>@color/color_dracula</item>
30+
<item>@color/color_dynamic_swatch</item>
3031
</integer-array>
3132

3233
<color name="material_amber_100">#FFECB3</color>
@@ -296,6 +297,9 @@
296297
pink accent. Surfaces are overridden in values-night for the true
297298
#282a36 canvas; see Theme.SagerNet.Dracula. -->
298299
<color name="color_dracula">#BD93F9</color>
300+
<!-- Representative swatch for the Dynamic (Material You) theme option (wallpaper-based on
301+
Android 12+); a neutral system-accent stand-in since it has no fixed color. -->
302+
<color name="color_dynamic_swatch">#5E6472</color>
299303
<color name="color_dracula_dark">#6272A4</color>
300304
<color name="color_dracula_accent">#FF79C6</color>
301305
<color name="color_dracula_100">#E9DDFB</color>

0 commit comments

Comments
 (0)