「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") } } } } }
関連記事: