Compose By Exampleを読んでのまとめ

# 概要

今回は、Google社で提供されているComposeのpathwayの一環として、Compose By Exampleを視聴した内容をまとめていきたいと思います。

参考リンク：https://www.youtube.com/watch?v=DDd6IOlH3io



## Jetpack Composeとは？
![キャプチャ.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/fcb43a76-0509-0c8e-7b70-ff0dd92eb31a.jpeg)
まず初めに、Jetpack Composeには以下のような定義があります。

* モダンな宣言的UIツールキット
* Kotlinに基づいている
* Unbundled(個別に独立している)である

![Feature.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/cbb9bfe8-26f3-2751-cc2f-d54fbafa6656.jpeg)

また、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.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/60d23f12-4a64-3026-99ad-ad87d9249a1c.jpeg)
JetchatとJetsurveyプロジェクトは、TextInputやStateManagementなどの基本となるような要素が含まれており、Compose学習の入門用に推薦されているプロジェクトです。 

###Jetcaster 
![jetcaster.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/31ae81cc-4c81-82d1-0a9e-638e9f638e48.jpeg)              
  AdancedThemingやAnimationなどが組み込まれた、より複雑なUIのサンプルプロジェクトです。 

###Owl
![owl.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/317f9db1-606b-358c-3b38-be0f48ebb20e.jpeg)
MaterialDesignやAnimationなどに、フォーカスしたサンプルプロジェクトです。

## Theming(テーマ)
Jetpack ComposeではMaterialDesignに特化した実装が組み込まれています。、JetPackColorからダークテーマ対応までは **"Make the easy things easy"** と定義されています。

Themeに関して、color、typography、shapeの３つの引数を定義することによって、任意にカスタマイズすることも可能です。

```kotlin
MaterialTheme(
  colors = ...
  typography = ...
  shapes = ...
) {
}
```

サンプルプロジェクトのOwlではこのビルダー関数を使用して、各画面ごとにテーマを作成しています。カスタムThemeを使用することによって、DarkThemeにも柔軟に対応することが可能になっています。
![multitheme.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/4139eeb1-85ae-068b-5308-8f75d4859d2b.jpeg)

例）https://github.com/android/compose-samples/blob/34a75fb3672622a3fb0e6a78adc88bbc2886c28f/Owl/app/src/main/java/com/example/owl/ui/theme/Theme.kt#L48


### Color

Composeでは、下記のMaterial color stytemをモデリングしたクラスが用意されています。
![colorsystem.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/6997402d-9f66-b321-9e23-1fccfb244988.jpeg)
コードで表すとこちら↓

```kotlin
val colors = Colors(
  primary = ...
  primaryVariant = ...
```
Material Themeでは、Colorsクラスを継承した、ベースラインのカラーパレットから作るデフォルトのビルダー関数もいくつか提供されており、色だけを変更したい場合などに用いることができます。

```kotlin
val lightColors = lightColors(
  primary = ...
  secondary = ...
```

### Typography

同様に、Material ThemeでデフォルトのTypographyクラスが定義されており、ビルダー関数を用いてカスタマイズするための上書きもできます。


デフォルトで使用する場合↓
```kotlin
val typography = Typography()
```

TextStyleを用いたカスタマイズの場合↓

```kotlin
val customTypography = Typography(
  h1 = TextStyle(
    fontFamily = Rubik,
    fontSize = 96.sp,
    fontWeight = FontWeight.Bold, 
    lineHeight = 120.sp,
  )
 h2 = ...
 subtitle = ...
 body = ...
```
図式化したものがこちら↓
![font.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/bc413c2a-88af-17d4-6798-f5b9f3f95e5c.jpeg)


### Shape

Composeでは、small, medium, largeの３種類のサイズのコンポーネントを定義することによって、デフォルトまたはカスタムのShapeを作成することができます。それぞれのサイズの例として、ボタン、カード及びシートなどが挙げられます。

```kotlin
val shapes = Shapes(
  small = ...
  medium = ...
  large = ...
```

角丸にしたい場合↓(サイズとパーセントの両方でしていできます)

```kotlin
    small = RoundedCornerShape(size = 4.dp),
```
または　

```kotlin
    small = RoundedCornerShape(percent = 50),
```

左上の角をカットしたい場合。

```kotlin
small = CutCornerShape(topLeft = 16.dp)
```

### 推奨されるアプリでのTheme適応方法

以下のサンプルコードのように、Compasable関数の中に内包する事により、コード上で横断して使えるようにすることができます。

```kotlin
@Compose
fun YelloTheme(
  content: @Composable () -> Unit
) {
  MaterialTheme(
    colors = YellowLightColors,
    typography = OwlTypography, 
    ...
  )
}
```
なにもThemeが設定されていないコンテンツに対し
![before.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/b0a118d4-8eec-332a-5f29-1ebd04afc5bb.jpeg)

YellowThemeを適用した結果↓
![after.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/089fd165-bfd7-6195-d432-2ee5c8c97553.jpeg)


以下のように記述することで、**画面の一部分だけ違うテーマを適応する**こともできます。

```kotlin
fun CourseDetails(...) {
  PinkTheme {
    ...
    BlueTheme {
      RelatedCourses(...)
    }
  }
}
```
関連ページの部分だけ、Themeを変更した例↓
![partial.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/2bf0f94c-9166-1fa0-2acd-0aea384bb1d6.jpeg)


### テーマの要素について

MaterialThemeの要素には、それぞれ型安全にアクセスして利用できます。

```kotlin
Text(
  text = ...
  style = MaterialTheme.typography.subtitle1,
  color = MaterialTheme.colors.(ドットのあとはシステムが自動で補完してくれます)
)
```

色だけをコピーして利用することもできるので、色のためのハードコーディングを防止し、複数のテーマをサポートすることにも役立ちます。

```kotlin
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など)。

```kotlin
Surface(color = MaterialTheme.colors.primary) {
  // ここでのデフォルトカラーは `onPrimary` になる。
  Text(...)
  Button(...)
}
```

```kotlin
@Composable
fun FloatingActionButton(color = MaterialTheme.colors.secondary) {
  // ここでのデフォルトカラーは `secondary` になる。
}
```

### ダークテーマ対応

isSystemInDarkTheme()でダークモードの判別をし、対応したカラーリストを渡すだけで完了です！
すごく便利！

```kotlin
@Composable
fun PinkTheme(
  darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  if (darkTheme) PinkDarkColors else PinkLightColors
...
```
(おまけ)
判別式では、どのようなことが行われているか調査してみました。
isSystemInDarkTheme()関数の中では、下記のような判定が行われています。

```kotlin
@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を取得して、それを使っている。
これでアニメーションもできる。(軽く書いているけどすごい。。)

(動画内のコードと現状のコードでは、異なる場合があります)

```kotlin
val currentImage = ...
val palette = // Paletteライブラリを使って画像からpaletteを取得
val dominantColor = // paletteからdominant colorを取得

val colors = MaterialTheme.colors.copy(
  primary = animate(dominantColor),
)

MaterialTheme(colors = colors) {
  content()
}
```
https://github.com/android/compose-samples/blob/34a75fb3672622a3fb0e6a78adc88bbc2886c28f/Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt#L161


# Layout

* Colum: コンポーネントを縦に配置する
* Row: コンポーネントを横に配置する
* Stack: コンポーネント同士を上に積み重ねる 
* ConstraintLayout: おなじみの制約でレイアウト

Modifierでクリックやpadding、toggleable、verticalScroll()、zoomable()などのイベントも付与することが可能です。


## カスタムレイアウトを作るには?

以下のようなレイアウトを**Layoutブロックを用いたカスタムレイアウト**でサンプルアプリで実際に実装されています。このセクションでは切り出して別記事で記載しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/c88915ad-4310-7d22-3a9d-725d51db1fde.png)

カスタムレイアウトの紹介ページ：https://qiita.com/tmasa517/items/8bb39ba2d54f54dca567

# Animation(アニメーション)
animateDpAsStateなどの、Compose側で用意されているanimate関数を使用してスムーズなアニメーションを行うことが可能です。
## 基本的なアニメーション構文
```kotlin
 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")
        }
    }
...
```
サンプルリンク：https://github.com/tmasa517/ComposeDemo/blob/f22d54de74e9588a0945805f2eb70d76c28f9ce7/ComposeSampleSolution/app/src/main/java/com/example/composesample/AnimationTopic.kt#L22


## Transition
Transtitionを用いて、各状態ごとに対応した値を設定でき、それに応じてのアニメーションの設定も可能とされています。


private enum class SelectionState { Unselected, Selected }

/**
 * Class holding animating values when transitioning topic chip states.
 */
private class TopicChipTransition(
    cornerRadius: State<Dp>,
    selectedAlpha: State<Float>,
    checkScale: State<Float>
) {
    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
        }
    }


サンプルコード：https://github.com/android/compose-samples/blob/01d35510a3d5d72099ffbb2c6e25ef4782291d3d/Owl/app/src/main/java/com/example/owl/ui/onboarding/Onboarding.kt#L164


### 今後Android Studioにアニメーションのinspectorが追加される
今後canaryのリリースで、以下のようにアニメーションをキーフレームごとで確認できる機能が追加される予定です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27388/918727c7-b667-d833-51bc-e334a728635b.png)
https://www.youtube.com/watch?v=DDd6IOlH3io より

### アニメーションのスクショ＆テストができる
clockTestRuleにアクセスし、時間軸を操作することにより、スクショなどの比較などが可能です。また、それを用いてのアニメーションテストもできます。

```kotlin
    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()) // 保存されている画像とスクショを比較する
    }
```