概要
今回は、Google社で提供されているComposeのpathwayの一環として、Compose By Exampleを視聴した内容をまとめていきたいと思います。
参考リンク:https://www.youtube.com/watch?v=DDd6IOlH3io
Jetpack Composeとは?
まず初めに、Jetpack Composeには以下のような定義があります。
- モダンな宣言的UIツールキット
- Kotlinに基づいている
- Unbundled(個別に独立している)である
また、Compoeの一つの大きなポイントとして**"Make the easy things easy and the hard things possible"** というのがあり、動画内でも何度も語られていました。
Theming, Layout, Animation
サンプル
Github: https://github.com/android/compose-samples/tree/34a75fb3672622a3fb0e6a78adc88bbc2886c28f
Composeのサンプルとして、何種類かデモアプリが上記のリポジトリで提供されています。
※ビルドが失敗する場合は、Dependencies.ktの以下のラインを
const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-beta01"
このように変更してみてください↓
const val androidGradlePlugin = "com.android.tools.build:gradle:7.1.0-alpha01"
###JetChat&Jetsurvey
JetchatとJetsurveyプロジェクトは、TextInputやStateManagementなどの基本となるような要素が含まれており、Compose学習の入門用に推薦されているプロジェクトです。
###Jetcaster
AdancedThemingやAnimationなどが組み込まれた、より複雑なUIのサンプルプロジェクトです。
###Owl
MaterialDesignやAnimationなどに、フォーカスしたサンプルプロジェクトです。
Theming(テーマ)
Jetpack ComposeではMaterialDesignに特化した実装が組み込まれています。、JetPackColorからダークテーマ対応までは "Make the easy things easy" と定義されています。
Themeに関して、color、typography、shapeの3つの引数を定義することによって、任意にカスタマイズすることも可能です。
MaterialTheme(
colors = ...
typography = ...
shapes = ...
) {
}
サンプルプロジェクトのOwlではこのビルダー関数を使用して、各画面ごとにテーマを作成しています。カスタムThemeを使用することによって、DarkThemeにも柔軟に対応することが可能になっています。
Color
Composeでは、下記のMaterial color stytemをモデリングしたクラスが用意されています。
コードで表すとこちら↓
val colors = Colors(
primary = ...
primaryVariant = ...
Material Themeでは、Colorsクラスを継承した、ベースラインのカラーパレットから作るデフォルトのビルダー関数もいくつか提供されており、色だけを変更したい場合などに用いることができます。
val lightColors = lightColors(
primary = ...
secondary = ...
Typography
同様に、Material ThemeでデフォルトのTypographyクラスが定義されており、ビルダー関数を用いてカスタマイズするための上書きもできます。
デフォルトで使用する場合↓
val typography = Typography()
TextStyleを用いたカスタマイズの場合↓
val customTypography = Typography(
h1 = TextStyle(
fontFamily = Rubik,
fontSize = 96.sp,
fontWeight = FontWeight.Bold,
lineHeight = 120.sp,
)
h2 = ...
subtitle = ...
body = ...
Shape
Composeでは、small, medium, largeの3種類のサイズのコンポーネントを定義することによって、デフォルトまたはカスタムのShapeを作成することができます。それぞれのサイズの例として、ボタン、カード及びシートなどが挙げられます。
val shapes = Shapes(
small = ...
medium = ...
large = ...
角丸にしたい場合↓(サイズとパーセントの両方でしていできます)
small = RoundedCornerShape(size = 4.dp),
または
small = RoundedCornerShape(percent = 50),
左上の角をカットしたい場合。
small = CutCornerShape(topLeft = 16.dp)
推奨されるアプリでのTheme適応方法
以下のサンプルコードのように、Compasable関数の中に内包する事により、コード上で横断して使えるようにすることができます。
@Compose
fun YelloTheme(
content: @Composable () -> Unit
) {
MaterialTheme(
colors = YellowLightColors,
typography = OwlTypography,
...
)
}
以下のように記述することで、画面の一部分だけ違うテーマを適応することもできます。
fun CourseDetails(...) {
PinkTheme {
...
BlueTheme {
RelatedCourses(...)
}
}
}
テーマの要素について
MaterialThemeの要素には、それぞれ型安全にアクセスして利用できます。
Text(
text = ...
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.(ドットのあとはシステムが自動で補完してくれます)
)
色だけをコピーして利用することもできるので、色のためのハードコーディングを防止し、複数のテーマをサポートすることにも役立ちます。
val background =
MaterialTheme.colors.onSurface.copy(
alpha = 0.2f
)
Surface(color = background) {...}
Smart Default
Surfaceコンポーネントを例として挙げた時、バックグラウンドの色を設定するとWrapされているコンテンツの色が自動でつくSmart Defaultが適用されます。例えば以下のサンプルではcolor = primaryと設定しているので、ラッピングされているコンテンツの色は自動的にprimary色が適用されます。
同様にComposableなコンポーネントでもこの仕組は適用されます(Floating Action Buttonなど)。
Surface(color = MaterialTheme.colors.primary) {
// ここでのデフォルトカラーは `onPrimary` になる。
Text(...)
Button(...)
}
@Composable
fun FloatingActionButton(color = MaterialTheme.colors.secondary) {
// ここでのデフォルトカラーは `secondary` になる。
}
ダークテーマ対応
isSystemInDarkTheme()でダークモードの判別をし、対応したカラーリストを渡すだけで完了です!
すごく便利!
@Composable
fun PinkTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
if (darkTheme) PinkDarkColors else PinkLightColors
...
(おまけ)
判別式では、どのようなことが行われているか調査してみました。
isSystemInDarkTheme()関数の中では、下記のような判定が行われています。
@Composable
@ReadOnlyComposable
fun isSystemInDarkTheme(): Boolean {
val uiMode = LocalConfiguration.current.uiMode
return (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
}
Dynamic Theme(ダイナミックテーマ)
ここからの内容は、 Make the easy things easy and the hard things possible の the hard things possibleに関わってくる部分の内容になります。
ダイナミックテーマのサンプルとしてJetcasterが用意されており、一番手前に表示されているアルバムに対応した色を取得して、それを動的にテーマに適用しています。
例。
既存のPaletteライブラリを使って画像からdominantColorを取得して、それを使っている。
これでアニメーションもできる。(軽く書いているけどすごい。。)
(動画内のコードと現状のコードでは、異なる場合があります)
val currentImage = ...
val palette = // Paletteライブラリを使って画像からpaletteを取得
val dominantColor = // paletteからdominant colorを取得
val colors = MaterialTheme.colors.copy(
primary = animate(dominantColor),
)
MaterialTheme(colors = colors) {
content()
}
Layout
- Colum: コンポーネントを縦に配置する
- Row: コンポーネントを横に配置する
- Stack: コンポーネント同士を上に積み重ねる
- ConstraintLayout: おなじみの制約でレイアウト
Modifierでクリックやpadding、toggleable、verticalScroll()、zoomable()などのイベントも付与することが可能です。
カスタムレイアウトを作るには?
以下のようなレイアウトをLayoutブロックを用いたカスタムレイアウトでサンプルアプリで実際に実装されています。このセクションでは切り出して別記事で記載しています。
カスタムレイアウトの紹介ページ:https://qiita.com/tmasa517/items/8bb39ba2d54f54dca567
Animation(アニメーション)
animateDpAsStateなどの、Compose側で用意されているanimate関数を使用してスムーズなアニメーションを行うことが可能です。
基本的なアニメーション構文
var sizeState by remember { mutableStateOf(200.dp)}
val size by animateDpAsState(
targetValue = sizeState,
tween(
durationMillis = 1000
)
)
...
Box(modifier = Modifier
.size(size)
.background(color),
contentAlignment = Alignment.Center){
Button(onClick = { sizeState += 50.dp }) {
Text("Increase Size")
}
}
...
Transition
Transtitionを用いて、各状態ごとに対応した値を設定でき、それに応じてのアニメーションの設定も可能とされています。
private enum class SelectionState { Unselected, Selected }
/**
- Class holding animating values when transitioning topic chip states.
*/
private class TopicChipTransition(
cornerRadius: State,
selectedAlpha: State,
checkScale: State
) {
val cornerRadius by cornerRadius
val selectedAlpha by selectedAlpha
val checkScale by checkScale
}
@Composable
private fun topicChipTransition(topicSelected: Boolean): TopicChipTransition {
val transition = updateTransition(
targetState = if (topicSelected) SelectionState.Selected else SelectionState.Unselected
)
val corerRadius = transition.animateDp { state ->
when (state) {
SelectionState.Unselected -> 0.dp
SelectionState.Selected -> 28.dp
}
}
val selectedAlpha = transition.animateFloat { state ->
when (state) {
SelectionState.Unselected -> 0f
SelectionState.Selected -> 0.8f
}
}
val checkScale = transition.animateFloat { state ->
when (state) {
SelectionState.Unselected -> 0.6f
SelectionState.Selected -> 1f
}
}
今後Android Studioにアニメーションのinspectorが追加される
今後canaryのリリースで、以下のようにアニメーションをキーフレームごとで確認できる機能が追加される予定です。
https://www.youtube.com/watch?v=DDd6IOlH3io より
アニメーションのスクショ&テストができる
clockTestRuleにアクセスし、時間軸を操作することにより、スクショなどの比較などが可能です。また、それを用いてのアニメーションテストもできます。
private fun compareTimeScreenshot(timeMs: Long, goldenName: String) {
// Start with a paused clock
composeTestRule.clockTestRule.pauseClock()
// Start the unit under test
showAnimatedCircle()
// Advance clock (keeping it paused)
composeTestRule.clockTestRule.advanceClock(timeMs) // 時間を操作する
// Take screenshot and compare with golden image in androidTest/assets
assertScreenshotMatchesGolden(goldenName, onRoot()) // 保存されている画像とスクショを比較する
}