たくさんの自由帳

追記 2021/02/15:Jetpack Composeのバージョンがalpha 12になりました。
影響があったといえば、

  • vectorResourceが非推奨。painterResourceを使うように。
    • よってIconへDrawableを渡すときの引数はimageVectorではなく、painterになります。
Icon(
    painter = painterResource(R.drawable.ic_outline_open_in_browser_24px),
    contentDescription = "ブラウザ起動"
)   
  • Context取得時に使う、AmbientContext.currentLocalContext.currentに変更になりました。

  • Android Studioが対応してないのか、Kotlinのバージョンを1.4.30にしても、バージョンが古いので利用できませんってIDEに言われます。

    • エラー出るけど実行はできる。修正待ち
    • 'padding(Dp): Modifier' is only available since Kotlin 1.4.30 and cannot be used in Kotlin 1.4
    • 解決方法は、設定を開き、Languages & Frameworksへ進み、Kotlinを選び、Update ChannelEarly Access Preview 1.4.xにしてInstallを押せばいいらしい。
      • ここらへん参照:https://github.com/android/compose-samples/pull/387#issuecomment-777515590

追記:Iconの増えてるのでコピペじゃ動かなくなりました。
contentDescriptionという文字列を入れる引数が増えてます。ので、コピペしたらIconの引数を足してください。以下のように

Icon(
    imageVector = Icons.Outlined.Home,
    contentDescription = "アイコンの説明。なければnullでもいい"
)

この続きです。そのうち追記しに来る

https://takusan.negitoro.dev/posts/android_jc/

Snackbar表示

Scaffold { }で囲ってあげる必要があります。
Snackbar表示以外でもアプリバーとかドロワーの表示でも使うので置いておいて損はないはず。

また、Compose内で利用できるコルーチン(rememberCoroutineScope())を利用する必要があります。

@ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        MaterialTheme {
            val state = rememberScaffoldState()
            val scope = rememberCoroutineScope()
            Scaffold(
                scaffoldState = state,
                topBar = {
                    TopAppBar() {
                        Column(
                            horizontalAlignment = Alignment.CenterHorizontally,
                            verticalArrangement = Arrangement.Center,
                            modifier = Modifier.fillMaxHeight().padding(10.dp),
                        ) {
                            Text(text = "ブログ一覧")
                        }
                    }
                }
            ) {
                OutlinedButton(onClick = {
                    scope.launch {
                        val result = state.snackbarHostState.showSnackbar(
                            message = "Snackbar表示",
                            actionLabel = "押せます",
                            duration = SnackbarDuration.Short,
                        )
                        // 押せたかどうか
                        if (result == SnackbarResult.ActionPerformed) {
                            Toast.makeText(this@MainActivity, "押せました!", Toast.LENGTH_SHORT).show()
                        }
                    }
                }) {
                    Text(text = "Snackbar表示")
                }
            }
        }
    }
}

実行結果

Imgur

参考にしました:https://gist.github.com/gildor/82ec960cc0c5873453f024870495eab3

Context取得

リソース取得等は用意されてるけど、それ以外でContextを使いたい場合はこうです!

@Composable
fun needContext() {
    val context = LocalContext.current
}

コルーチンは?

val scope = rememberCoroutineScope()
scope.launch {

}

テーマとか文字の色にカラーコードを使いたい!

Color.parseColor()がそのままでは使えないので、androidx.compose.ui.graphicsの方のColorの引数に入れてあげます。

Text(
    text = AnniversaryDate.makeAnniversaryMessage(anniversary),
    color = Color(android.graphics.Color.parseColor("#252525"))
)

ダークモード

まずはThemeColor.ktみたいな色だけを書いておくクラスを作ってはりつけ
なんかisDarkMode@Composableを付ける理由はわかりません。サンプルコードがそうなってたので便乗

/**
 * [MaterialTheme]に渡すテーマ。コードでテーマの設定ができるってマジ?
 * */

/** ダークモード。OLED特化 */
val DarkColors = darkColors(
    primary = Color.White,
    secondary = Color.Black,
)

/** ライトテーマ */
val LightColors = lightColors(
    primary = Color(android.graphics.Color.parseColor("#757575")),
    primaryVariant = Color(android.graphics.Color.parseColor("#494949")),
    secondary = Color(android.graphics.Color.parseColor("#a4a4a4")),
)

/** ダークモードかどうか */
@Composable
fun isDarkMode(context: Context): Boolean {
    val conf = context.resources.configuration
    val nightMode = conf.uiMode and Configuration.UI_MODE_NIGHT_MASK
    return nightMode == Configuration.UI_MODE_NIGHT_YES // ダークモードなら true
}

そしたらMaterialTheme { }に渡してあげます。if文を一行で書く

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme(
                // 色の設定
                colors = if (isDarkMode(AmbientContext.current)) DarkColors else LightColors
            ) {
                Scaffold {
                    Column(
                        modifier = Modifier.fillMaxWidth().fillMaxHeight(),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally,
                    ) {
                        // この2つ、ダークモードなら白色、それ以外なら黒色になるはず
                        Text(text = "もじのいろ")
                        Icon(imageVector = Icons.Outlined.Home)
                    }
                }
            }
        }
    }
}

ちゃんと動けばダークモードのときは真っ暗になると思います。AOD

Imgur

ちなみに黒基調にするとIcon()等が勝手に検知してアイコンの色を白色に変更してくれるそうです。ダークモード対応の手間が減る

タブレイアウト

見つけたので報告しますね。そんなに難しくない。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme(
                // 色の設定
                colors = if (isDarkMode(AmbientContext.current)) DarkColors else LightColors
            ) {
                Scaffold {
                    Column(
                        modifier = Modifier.fillMaxWidth().fillMaxHeight(),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally,
                    ) {

                        // 選択中タブ
                        var selectTabIndex by remember { mutableStateOf(0) }

                        TabLayout(
                            selectTabIndex = selectTabIndex,
                            tabClick = { index -> selectTabIndex = index }
                        )
                        Text(text = "選択中:$selectTabIndex")

                    }
                }
            }
        }
    }
}


/**
 * タブレイアウト
 *
 * @param selectTabIndex 選択するタブを入れてね
 * @param tabClick タブを押した時
 * */
@Composable
fun TabLayout(selectTabIndex: Int, tabClick: (Int) -> Unit) {
    TabRow(
        modifier = Modifier.padding(10.dp),
        selectedTabIndex = selectTabIndex,
        backgroundColor = Color.Transparent,
    ) {
        Tab(selected = selectTabIndex == 0, onClick = {
            tabClick(0)
        }) {
            Icon(imageVector = Icons.Outlined.Android)
            Text(text = "Android 9")
        }
        Tab(selected = selectTabIndex == 1, onClick = {
            tabClick(1)
        }) {
            Icon(imageVector = Icons.Outlined.Android)
            Text(text = "Android 10")
        }
        Tab(selected = selectTabIndex == 2, onClick = {
            tabClick(2)
        }) {
            Icon(imageVector = Icons.Outlined.Android)
            Text(text = "Android 11")
        }
    }
}

動作結果

Imgur

動的にテーマを変える

MaterialThemecolorsの部分を変えることでテーマを切り替えられるようになりました。これ従来のレイアウトじゃできないからComposeの強みじゃない?

まずは色の情報を置いておくクラスを作成して、以下をコピペします。

ThemeColor.kt

/** ダークモード。OLED特化 */
val DarkColors = darkColors(
    primary = Color.White,
    secondary = Color.Black,
)

/** ライトテーマ */
val LightColors = lightColors(
    primary = Color(android.graphics.Color.parseColor("#FF6200EE")),
    primaryVariant = Color(android.graphics.Color.parseColor("#FF3700B3")),
    secondary = Color(android.graphics.Color.parseColor("#FFFFFF")),
)

/** 青基調 */
val blueTheme = lightColors(
    primary = Color(android.graphics.Color.parseColor("#0277bd")),
    primaryVariant = Color(android.graphics.Color.parseColor("#58a5f0")),
    secondary = Color(android.graphics.Color.parseColor("#004c8c")),
)

/** 赤基調 */
val redTheme = lightColors(
    primary = Color(android.graphics.Color.parseColor("#c2185b")),
    primaryVariant = Color(android.graphics.Color.parseColor("#8c0032")),
    secondary = Color(android.graphics.Color.parseColor("#fa5788")),
)

/** 緑基調 */
val greenTheme = lightColors(
    primary = Color(android.graphics.Color.parseColor("#1b5e20")),
    primaryVariant = Color(android.graphics.Color.parseColor("#003300")),
    secondary = Color(android.graphics.Color.parseColor("#4c8c4a")),
)

/** ダークモードかどうか */
@Composable
fun isDarkMode(context: Context): Boolean {
    val conf = context.resources.configuration
    val nightMode = conf.uiMode and Configuration.UI_MODE_NIGHT_MASK
    return nightMode == Configuration.UI_MODE_NIGHT_YES // ダークモードなら true
}

その後に書いていきます。ダークモードを一緒に書いた人はこっから掛けばいいです。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {

            // デフォルト
            val defaultTheme = if (isDarkMode(AmbientContext.current)) DarkColors else LightColors

            // 色を保持する
            val themes = remember { mutableStateOf(defaultTheme) }

            MaterialTheme(
                // 色の設定
                colors = themes.value
            ) {
                Scaffold(
                    topBar = {
                        TopAppBar() {
                            Column(
                                horizontalAlignment = Alignment.CenterHorizontally,
                                verticalArrangement = Arrangement.Center,
                                modifier = Modifier.fillMaxHeight().padding(10.dp),
                            ) {
                                Text(text = "動的テーマ")
                            }
                        }
                    }
                ) {
                    // テーマ切り替え
                    DynamicThemeButtons(themeClick = { themes.value = it})
                }
            }
        }
    }
}

/**
 * 動的にテーマを切り替える。
 *
 * @param themeClick ボタンを押したときに呼ばれる。引数にはテーマ([Colors])が入ってる
 * */
@Composable
fun DynamicThemeButtons(
    themeClick: (Colors) -> Unit,
) {

    // デフォルト
    val defaultTheme = if (isDarkMode(context = AmbientContext.current)) DarkColors else LightColors

    Column(
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Button(
            modifier = Modifier.padding(5.dp),
            onClick = { themeClick(blueTheme) }
        ) {
            Text(text = "青")
        }
        Button(modifier = Modifier.padding(5.dp),
            onClick = { themeClick(redTheme) }
        ) {
            Text(text = "赤")
        }
        Button(modifier = Modifier.padding(5.dp),
            onClick = { themeClick(greenTheme) }
        ) {
            Text(text = "緑")
        }
        Button(modifier = Modifier.padding(5.dp),
            onClick = { themeClick(defaultTheme) }
        ) {
            Text(text = "デフォルト")
        }
    }

}

実行結果

ボタンを押すと色が切り替わると思います。

Imgur

Imgur

表示、非表示をアニメーションしてほしい

AnimatedVisibilityってのがあります。

class MainActivity : AppCompatActivity() {
    
    @ExperimentalAnimationApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {

            // デフォルト
            val defaultTheme = if (isDarkMode(AmbientContext.current)) DarkColors else LightColors

            // 色を保持する
            val themes = remember { mutableStateOf(defaultTheme) }

            MaterialTheme(
                // 色の設定
                colors = themes.value
            ) {
                VisibilityAnimationSample()
            }
        }
    }
}

@ExperimentalAnimationApi
@Composable
fun VisibilityAnimationSample() {
    // 表示するか
    val isShow = remember { mutableStateOf(false) }
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        AnimatedVisibility(visible = isShow.value) {
            // この中に書いたやつがアニメーションされながら表示される
            Column {
                // 10個ぐらい
                repeat(10) {
                    Icon(imageVector = Icons.Outlined.Android, modifier = Modifier.rotate(90f * it))
                }
            }
        }
        // 表示、非表示切り替え
        Button(onClick = { isShow.value = !isShow.value }) {
            Text(text = "アニメーションさせながら表示")
        }
    }
}

画像じゃわからんけど、ちゃんとアニメーションされてます。

Imgur

右寄せ

android:gravity="right"をJetpack Composeでもやりたいわけですね。重要な点はfillMaxWidth()を使うところです

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            GravityRight()
        }
    }
}

@Composable
fun GravityRight() {
    Column {
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.End,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Text(text = "右に寄ってる Row")
            IconButton(onClick = { /*TODO*/ }) {
                Icon(imageVector = Icons.Outlined.Adb)
            }
        }
        Divider() // 区切り
        Column(
            modifier = Modifier.fillMaxWidth(),
            horizontalAlignment = Alignment.End,
            verticalArrangement = Arrangement.Center
        ) {
            Text(text = "右に寄ってる Column")
            IconButton(onClick = { /*TODO*/ }) {
                Icon(imageVector = Icons.Outlined.Adb)
            }
        }
    }
}

こうなるはず

Imgur

均等に並べる

LinearLayoutと同じようにweightを設定すればできます。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            WeightSample()
        }
    }
}

@Composable
fun WeightSample() {
    Row(
        modifier = Modifier.fillMaxWidth()
    ) {
        Button(modifier = Modifier.weight(1f).padding(5.dp), onClick = { /*TODO*/ }) {
            Text(text = "ぼたんだよー")
        }
        Button(modifier = Modifier.weight(1f).padding(5.dp), onClick = { /*TODO*/ }) {
            Text(text = "ぼたんだよー")
        }
        Button(modifier = Modifier.weight(1f).padding(5.dp), onClick = { /*TODO*/ }) {
            Text(text = "ぼたんだよー")
        }
    }
}

こうなるはず

Imgur

余りのスペースを埋める

埋めたい部品に対してweight(1f)を足してあげることで、他の部品の事を考えながら埋めたい部品で埋めてくれます。
こっちも親要素にfillMaxWidth()を指定してあげる必要があります。(画面いっぱい使うなら)

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MessageSendUI()
        }
    }
}

@Composable
fun MessageSendUI() {
    Row(
        modifier = Modifier.fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically
    ) {
        val message = remember { mutableStateOf("") }
        // この部品を最大まで広げたい
        OutlinedTextField(
            value = message.value,
            onValueChange = { message.value = it },
            modifier = Modifier
                .padding(5.dp)
                .weight(1f)
        )
        IconButton(
            onClick = { /*TODO*/ },
            modifier = Modifier.padding(5.dp)
        ) {
            Icon(imageVector = Icons.Outlined.Send)
        }
    }
}

こうなるはず

Imgur

もっとサンプル書け!

Jetpack Composeを書いてる人のサンプルには勝てないというわけでソースコードの探し方でも。

1.Android Studioで使いたいUI部品のソースコードを開く

ButtonとかOutlinedButtonとかMaterialThemeの部分でCtrl 押しながら クリックすることで飛べます。

こんなのが出ると思う

Imgur

2.ブラウザで

https://cs.android.com/

を開きます。

3.@sample の部分を探します。

さっきの画像だと、

@sample androidx.compose.material.samples.OutlinedButtonSample

のところですね

4.検索欄に入れる

で、何を検索欄に入れればいいんだって話ですが、さっき見つけた@sample ~の部分、
最後から.までの部分を入力します。

@sample androidx.compose.material.samples.OutlinedButtonSample

だと、OutlinedButtonSampleがそうです。

早速検索欄に入れましょう

Imgur

5.コードを読み解く

検索欄に入れたらおそらく一番最初のサジェストが使い方の例になってると思います。

あとは読んでいくしか無いです。

Imgur

サンプルアプリ

今までやったこと(だいたい)を一つのアプリにしてみました。どうぞ。

https://github.com/takusan23/JetpackComposeSampleApp

ダウンロード

https://github.com/takusan23/JetpackComposeSampleApp/releases/tag/1.0