「Android Studio Giraffe」の作成するプロジェクトは、Jetpack Composeの利用が推奨されます。
そして、作成されたプロジェクトは、Material Designeに準拠したテーマが指定されます。
※環境:Android Studio Giraffe | 2022.3.1 Patch 1
Kotlin 1.8.10
Compose Compiler 1.4.3
テンプレートのUIツリー
テンプレートは図のようなUIツリー(階層化されたComposable関数)になっています。


そして、各々の階層は次のような役割を持っています。
- (1)テーマの定義と選択(ライトorダーク)
- (2)テーマの指定
- (3)バックグラウンドの指定
- (4)機能ブロックの定義
- (5)”Hello Android”の表示
テーマの定義・選択・指定は、(1)、(2)の部分で行われています。
※「(数字)」は図中の「丸数字」に対応
テーマの定義
テンプレートは、ダークテーマとライトテーマの2つがデフォルトで定義されています。
Material Designeの指標
Material Designeは、様々な表示要素のデザイン(美的な造形性)を決める指標と、その指標のルールを定義した仕様です。
Material Designeの指標は3つあります。
| 指標 | 概要 | クラス |
|---|---|---|
| Color | 配色(Primary、Backgroundなど) | ColorScheme |
| Typography | 文字の体裁(デザイン・配置、など) | Typography |
| Shapes | 形状(角のまるみ、など) | Shapes |
各々の指標は専用のクラスを持ち、プロパティへ具体的な値を格納することで定義されます。
例えば、Color(ColorSchemeクラス)は次のようになっています。
@Stable
class ColorScheme(
primary: Color, // ... プロパティへ格納
onPrimary: Color, // ... プロパティへ格納
primaryContainer: Color,
onPrimaryContainer: Color,
inversePrimary: Color,
secondary: Color,
onSecondary: Color,
secondaryContainer: Color,
onSecondaryContainer: Color,
tertiary: Color,
onTertiary: Color,
tertiaryContainer: Color,
onTertiaryContainer: Color,
background: Color,
onBackground: Color,
surface: Color,
onSurface: Color,
surfaceVariant: Color,
onSurfaceVariant: Color,
surfaceTint: Color,
inverseSurface: Color,
inverseOnSurface: Color,
error: Color,
onError: Color,
errorContainer: Color,
onErrorContainer: Color,
outline: Color,
outlineVariant: Color,
scrim: Color,
) {
var primary by mutableStateOf(primary, structuralEqualityPolicy())
internal set
var onPrimary by mutableStateOf(onPrimary, structuralEqualityPolicy())
internal set
...
}
ダーク・ライトテーマ
テーマはMaterial Designeを使って、アプリ全体の見栄えを定義したものです。
テーマの定義はColorSchemeクラスへ色情報を格納することで行われます。
fun darkColorScheme(
primary: Color = ColorDarkTokens.Primary, // デフォルト値(定数)
onPrimary: Color = ColorDarkTokens.OnPrimary, // デフォルト値(定数)
...
): ColorScheme =
ColorScheme(
primary = primary,
onPrimary = onPrimary,
...
)
これを、ui.theme/Theme.ktでアプリ専用のダークテーマ(DarkColorScheme)へ改変しています。
private val DarkColorScheme = darkColorScheme(
primary = Purple80, // アプリ専用の色
secondary = PurpleGrey80, // 同上
tertiary = Pink80 // 同上
)
val Purple80 = Color(0xFFD0BCFF) // Dark用 val PurpleGrey80 = Color(0xFFCCC2DC) val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) // Light用 val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260)
ライトテーマ(LightColorScheme)も同様です。ここでは説明を省略します。
ダイナミックカラーテーマ
ダイナミックダークカラーテーマ(dynamicDarkColorScheme)の定義はDynamicTonalPalette.ktで行われています
@RequiresApi(Build.VERSION_CODES.S)
fun dynamicDarkColorScheme(context: Context): ColorScheme {
val tonalPalette = dynamicTonalPalette(context) // 壁紙に合わせた配色が返る
return darkColorScheme(
primary = tonalPalette.primary80, // アプリ専用の色
onPrimary = tonalPalette.primary20, // 同上
...
)
}
ライトテーマ(dynamicLightColorScheme)も同様です。ここでは説明を省略します。
Android 12(≧API 31)で登場しました。
今まで、アプリは開発側の方針で配色されてきました。「方針」には様々なものがあります。例えば、アプリの機能を連想させる色や、企業のブランドカラーなどです。
これに対し、「端末のユーザーがアプリの配色を決める機能」がダイナミックカラーです。
ダイナミックカラーに対応した全てのアプリの配色が変更されるので、端末をユーザの好みの色へカスタマイズできる点が売りのようです。
「どのくらいの需要があるか?!」は不明ですが …
具体的には、壁紙の色に合わせてテーマの配色が半自動的に決定されます。
例えば、図はAPI 33のSettingsアプリで、ランチャーの壁紙を変えた場合です。


壁紙が紫系⇒緑系に変わることで、テーマの配色(文字色)も同様に変更になり、複数の配色パターンから選択できるようになります。
なお、配色を決定する処理は「ダイナミックカラーフロー」と呼ばれ、システムに組み込まれています。
テーマの選択
引数に指定されたフラグdarkThemeとdynamicColorにより、4つの中から1つテーマがcolorScheme変数へ選択されます。
ただし、ダイナミックカラーはAPI≧31で有効です。
@Composable
fun MyApplicationTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view)
.isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
isSystemInDarkTheme()は、端末のテーマの状態(ダーク or ライト)を読み出します。


端末のテーマはSettingsアプリのDisplayページで指定されています。
テーマの指定
選択されたテーマはMaterialTheme関数でCompositionLocalと呼ばれる特殊な変数(Mapのような変数)の中に格納されます。
※CompositionLocalの動作は「Jetpack Compose:UIツリーにローカルな変数の確保(CompositionLocal)」を参照
【CompositionLocal】 LocalColorScheme :Colorを格納 LocalTypography :Typographyを格納 LocalShapes :Shapesを格納
@Composable
fun MyApplicationTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view)
.isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@Composable
fun MaterialTheme(
colorScheme: ColorScheme = MaterialTheme.colorScheme,
shapes: Shapes = MaterialTheme.shapes,
typography: Typography = MaterialTheme.typography,
content: @Composable () -> Unit
) {
val rememberedColorScheme = remember {
// Explicitly creating a new object here so we don't mutate the initial [colorScheme]
// provided, and overwrite the values set in it.
colorScheme.copy()
}.apply {
updateColorSchemeFrom(colorScheme)
}
val rippleIndication = rememberRipple()
val selectionColors = rememberTextSelectionColors(rememberedColorScheme)
CompositionLocalProvider(
LocalColorScheme provides rememberedColorScheme,
LocalIndication provides rippleIndication,
LocalRippleTheme provides MaterialRippleTheme,
LocalShapes provides shapes,
LocalTextSelectionColors provides selectionColors,
LocalTypography provides typography,
) {
ProvideTextStyle(value = typography.bodyLarge, content = content)
}
}
また、MaterialTheme関数が最初に呼ばれた時点で、MaterialThemeクラス(関数と同名)のインスタンスが作成されます。インスタンスはシングルトンです。
このMaterialThemeクラスはテーマの定義(Color、Typography、Shapes)を集約して管理するためのクラスです。
object MaterialTheme {
/**
* Retrieves the current [ColorScheme] at the call site's position in ...
*/
val colorScheme: ColorScheme
@Composable
@ReadOnlyComposable
get() = LocalColorScheme.current // 定義を取り出す
/**
* Retrieves the current [Typography] at the call site's position in ...
*/
val typography: Typography
@Composable
@ReadOnlyComposable
get() = LocalTypography.current // 定義を取り出す
/**
* Retrieves the current [Shapes] at the call site's position in ...
*/
val shapes: Shapes
@Composable
@ReadOnlyComposable
get() = LocalShapes.current // 定義を取り出す
}
アプリ内からテーマの定義を参照する場合は、このMaterialThemeクラスのインスタンを介して間接的に行います。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}
関連記事:
