[Android] 007. Layout / Material3 Control

詳しくは以下とチュートリアルを参照
https://developer.android.com/jetpack/compose/layouts/basics?hl=ja

Layout

Surface / Scaffold

TopAppBarやFABなどを規定の場所に配置したい場合はScaffoldをでない場合はSurfaceをベースのレイアウトに使用
Navigationなどで画面遷移する場合で前の画面に戻るUIや処理が無い場合はScaffoldでTopbarの設定をしBackボタンを配置してください
Scaffoldを使用する場合は使用するメソッドなどで@OptIn(ExperimentalMaterial3Api::class)などを付ける

Scaffoldでtopbarを指定した場合contentがtopbarの後ろにめり込みます
LazyColumn(上下スクロールリスト)だけの画面などの場合は問題無いかもしれませんが困るので以下のようにする

Scaffold { innerPadding ->
    Box(modifier = Modifier.padding(innerPadding)) {
        ...          
    }
}

Box / BoxWithConstraints

コントロールを重ねて表示したい場合などに使う
親の制約(例えば画面サイズ)を把握して配置したい場合はBoxWithConstraintsを使う

Column / Row

基本の縦横レイアウト

サイズはModifierで指定しなかった場合はcontentのサイズに依存する
画面サイズの割合でサイズを指定したい場合はModifier.weightを使う
weightは2箇所以上指定して、指定した数値の合計の割合のサイズになる

左右中央寄せをしたい場合は以下のような実装

Row(modifier = Modifier.fillMaxWidth().padding(8.dp, 0.dp, 8.dp, 0.dp),
    verticalAlignment = Alignment.CenterVertically,
    horizontalArrangement = Arrangement.SpaceBetween
) {
    Text("左")
    Text("中央")
    Text("右")
}

リスト / グリット

LazyColumn / LazyRow / LazyVerticalGrid / LazyHorizontalGrid
詳しくは以下参照
https://developer.android.com/jetpack/compose/lists?hl=ja

余白

余白を追加したい場合はSpacerを使う

Spacer(modifier = Modifier.height(10.dp))

Material3 Contorl

Material 3で使用できるコントロール一覧は以下にあります
https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary

FABが円から角丸四角になってたり、BottomAppBarにFABがドッキングできなくなっていたり、おそらくまだ未実装のコントロールなど、以前のMaterialと実装上の違いもあるようです

ここではよく使いそうで用意されているものに少し手をいれたものの覚書きを残します

この記事を書いている時点でDividerの存在に気がついてなかったです…
単純な線ならDividerを使用してください

画面に線を引く

/**
 * シンプルな横線描画
 *
 * @param color 線の色
 * @param border 線の太さ
 * @param margin 線の描画開始/終了のマージン
 */
@Composable fun Line(
    modifier: Modifier = Modifier,
    color: Color = if (isSystemInDarkTheme()) Color.White else Color.Black,
    border: Dp = 1.dp,
    margin: Dp = 0.dp
) {
    Canvas(modifier.fillMaxWidth().height(border)) {
        drawLine(
            color = color,
            start = Offset(margin.toPx(), border.toPx() / 2),
            end = Offset(size.width - margin.toPx() * 2, border.toPx() / 2),
            strokeWidth = border.toPx(),
            //pathEffect = PathEffect.dashPathEffect(listOf(24f, 24f).toFloatArray(), 0f),
        )
    }
}

背景に画像を表示

/**
 * 画面背景(イメージ)
 *
 * @param fileName Assetsに配置した画像ファイル名
 */
@Composable fun ImageBackground(
    fileName: String,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    val context = LocalContext.current
    val bitmap: Bitmap? by rememberSaveable { mutableStateOf(context.assetsToBitmap(fileName)) }

    BoxWithConstraints(modifier = modifier.fillMaxSize()) {
        bitmap?.let {
            Image(
                bitmap = it.asImageBitmap(),
                contentDescription = null,
                modifier = Modifier.fillMaxSize(),
                contentScale = ContentScale.Crop
            )
        }
        content()
    }
}

/**
 * Assetsにあるイメージファイルを読込みBitmapを返す
 */
fun Context.assetsToBitmap(fileName: String): Bitmap? {
    return try {
        with(assets.open(fileName)) {
            BitmapFactory.decodeStream(this)
        }
    } catch (e: IOException) { null }
}

背景をグラデーション

/**
 * 画面背景(グラデーション)
 *
 * 参考: Now in Android / Android Open Source Project
 * https://github.com/android/nowinandroid
 *
 * @param topColor 上から下に向かうグラデーション色
 * @param bottomColor 下から上に向かうグラデーション色
 */
@Composable fun GradientBackground(
    modifier: Modifier = Modifier,
    topColor: Color = Color.Unspecified,
    bottomColor: Color = Color.Unspecified,
    content: @Composable () -> Unit
) {
    val currentTopColor by rememberUpdatedState(topColor)
    val currentBottomColor by rememberUpdatedState(bottomColor)

    BoxWithConstraints(modifier = modifier.fillMaxSize().drawWithCache {
        // Compute the start and end coordinates such that the gradients are angled 11.06
        // degrees off the vertical axis
        val offset = size.height * tan(Math.toRadians(11.06).toFloat())

        val start = Offset(size.width / 2 + offset / 2, 0f)
        val end = Offset(size.width / 2 - offset / 2, size.height)

        // Create the top gradient that fades out after the halfway point vertically
        val topGradient = Brush.linearGradient(
            0f to if (currentTopColor == Color.Unspecified) {
                Color.Transparent
            }
            else {
                currentTopColor
            },
            0.724f to Color.Transparent,
            start = start,
            end = end,
        )
        // Create the bottom gradient that fades in before the halfway point vertically
        val bottomGradient = Brush.linearGradient(
            0.2552f to Color.Transparent,
            1f to if (currentBottomColor == Color.Unspecified) {
                Color.Transparent
            }
            else {
                currentBottomColor
            },
            start = start,
            end = end,
        )

        onDrawBehind {
            // There is overlap here, so order is important
            drawRect(topGradient)
            drawRect(bottomGradient)
        }
    }) {
        content()
    }
}

ソフトキーボードを画面タップで隠す

ControlではないですがTextField使用時にほしい機能なので…

// フォーカスリセットまたはkeyboardControllerで隠す
val focusRequester = remember { FocusRequester() }
val interactionSource = remember { MutableInteractionSource() }
val keyboardController = LocalSoftwareKeyboardController.current

Surface (modifier = Modifier
    .clickable(
        interactionSource = interactionSource,
        enabled = true,
        indication = null,
        onClick = {
            focusRequester.requestFocus()
        }
    )
    .focusRequester(focusRequester)
    .focusTarget()
    .onFocusChanged {              
        if (it.isFocused) {
            //keyboardController?.hide()
        }
    },
) {}

Android Studio Dolphin 2021.3.1 Patch 1 built on September 30, 2022