nakashimaakioのブログ

Androidアプリエンジニア / 記事に「はてなスター」よろしくお願いします。

【Android Studio】サブモジュールのGitが表示されない

はじめに

Android Studioで、サブモジュールのGitが表示されない場合の対策を解説。

方法

左上のAndroid Studio→Settings…→Version Control→Directory Mappings→サブモジュールを追加

【Android Studio】プロジェクトカラー変更方法

はじめに

Android Studioのプロジェクトカラー変更方法について解説。

方法

  1. 左上のプロジェクト名を右クリック
  2. Change Project Colorにカーソルを合わせる
  3. 好きな色を選択

【Android Jetpack Compose】ModalBottomSheet使用時、ステータスバーの文字色調整

はじめに

Android Jetpack ComposeでModalBottomSheetを使用した際、ステータスバーの色が変更されることがある。

※ステータスバーとは、画面上部に表示されるアイコン(時刻・バッテリー残量など)が並んだ領域のこと。

原因

ModalBottomSheetメソッド内でisAppearanceLightStatusBarsの値を変更しているのが原因。

        WindowCompat.getInsetsController(window, window.decorView).apply {
            isAppearanceLightStatusBars = !darkThemeEnabled
            isAppearanceLightNavigationBars = !darkThemeEnabled
        }

※androidx.compose.material3:material3-android:1.3.1ライブラリを使用(2024年12月時点で最新版)

対策

ModalBottomSheetメソッドのcontent内で、再度isAppearanceLightStatusBarsの値を変更させる。(LocalView.current.parent as DialogWindowProvider).windowでwindowを取得するのがポイント。

            ModalBottomSheet(
                onDismissRequest = { showBottomSheet = false },
                sheetState = sheetState
            ) {
                //ステータスバーを変更
                val window = (LocalView.current.parent as DialogWindowProvider).window
                WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = false

                // Sheet content
                Button(onClick = {
                    scope.launch { sheetState.hide() }.invokeOnCompletion {
                        if (!sheetState.isVisible) showBottomSheet = false
                    }
                }) {
                    Text("Hide bottom sheet")
                }
            }

参考サイト

stackoverflow.com

【Android Jetpack Compose】下スワイプで画面を閉じる

はじめに

Android Jetpack Composeで、下スワイプで画面を閉じる方法を解説。ModalBottomSheetを用いる方法もあるが、この記事ではAnchoredDraggableStateによりスワイプ検知して画面を閉じる方法について解説。

方法

1. スワイプ開始・終了クラスの作成

スワイプ状態を定義するクラス作成。

enum class AnchorPos {
    Start, End;
}

2. モーダル画面の作成

モーダル画面を作成

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ModalScreen(navController: NavHostController) {
    val screenHeight = with(LocalDensity.current) { LocalConfiguration.current.screenHeightDp.dp.toPx() }
    val anchors = DraggableAnchors {
        AnchorPos.Start at 0f
        AnchorPos.End at screenHeight
    }

    // ドラッグ可能な距離と状態を設定
    val draggableState = remember {
        AnchoredDraggableState(
            initialValue = AnchorPos.Start,
            anchors = anchors,
            positionalThreshold = { it * 0.5f },
            velocityThreshold = { screenHeight * 0.5f },
            snapAnimationSpec = SpringSpec(),
            decayAnimationSpec = exponentialDecay(),
            confirmValueChange = {
                when (it) {
                    AnchorPos.Start -> {}
                    AnchorPos.End -> {
                        //複数回popが行われることを防ぐため、現在の画面でのみpopを行う。
                        if (navController.currentDestination?.route == NavItem.MODAL.route) {
                            navController.popBackStack()
                        }
                    }
                }
                true
            }
        )
    }

    Box(
        modifier = Modifier
            .offset {
                IntOffset(
                    x = 0,
                    y = draggableState
                        .requireOffset()
                        .roundToInt()
                )
            }
            .anchoredDraggable(
                state = draggableState,
                orientation = Orientation.Vertical
            )
            .fillMaxSize()
            .background(Color.DarkGray),
    ) {
        //戻るボタン
        Button(
            onClick = { navController.popBackStack() },
            colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
            contentPadding = PaddingValues(0.dp)
        ) {
            Icon(
                modifier = Modifier.size(32.dp),
                contentDescription = "Close",
                imageVector = Icons.Default.Close
            )
        }
    }
}

参考サイト

zenn.dev

【Android Jetpack Compose】ボトムナビゲーション& 複数バックスタックの実装

はじめに

Android Jetpack Composeで、ボトムナビゲーション& 複数バックスタックの実装について解説。

処理の説明

各ボトムタブごとにバックスタックを保持。

例えば、ホーム1→ホーム2→他タブ押下→ホームタブ押下の際、ホーム1に遷移するのではなく、ホーム2に遷移させる。

方法

まず、画面のRoute定義するためNavItemenumクラス作成。

enum class NavItem(val route: String, val icon: ImageVector) {
    //BottomNavigation
    HOME("Home", Icons.Default.Home),
    EMAIL("Email", Icons.Default.Email),
    SETTINGS("Settings", Icons.Default.Settings),

    //Screen
    HOME_1("Home_1", Icons.Default.Home),
    HOME_2("Home_2", Icons.Default.Home),
    EMAIL_1("Email_1", Icons.Default.Email),
    EMAIL_2("Email_2", Icons.Default.Email),
    SETTINGS_1("Settings_1", Icons.Default.Settings),
    SETTINGS_2("Settings_2", Icons.Default.Settings)
}

次に、画面下に表示させるボトムナビゲーションを作成。

@Composable
fun BottomNavigation(navController: NavHostController) {
    val items = listOf(
        NavItem.HOME,
        NavItem.EMAIL,
        NavItem.SETTINGS
    )
    NavigationBar {
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry?.destination?.route
        items.forEach { item ->
            NavigationBarItem(
                icon = { Icon(item.icon, contentDescription = item.route) },
                label = { Text(item.route) },
                selected = currentRoute?.startsWith(item.route) == true,
                onClick = {
                    navController.navigate(item.route) {
                        popUpTo(navController.graph.findStartDestination().id) {
                            saveState = true
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                }
            )
        }
    }
}

表示画面を作成。

//表示画面
@Composable
fun ShowScreen(navController: NavHostController, navItem: NavItem, nextRoute: String?) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(top = 360.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Row {
            //アイコン
            Icon(
                modifier = Modifier.size(32.dp),
                imageVector = navItem.icon,
                contentDescription = navItem.route
            )

            //テキスト
            Text(
                modifier = Modifier.padding(start = 12.dp),
                text = navItem.route,
                fontSize = 28.sp
            )
        }

        if (nextRoute != null) {
            //進むボタン
            Button(
                modifier = Modifier.padding(top = 20.dp),
                onClick = { navController.navigate(nextRoute) }
            ) {
                Text(
                    text = "Next",
                    fontSize = 24.sp
                )
            }
        } else {
            //戻るボタン
            Button(
                modifier = Modifier.padding(top = 20.dp),
                onClick = { navController.popBackStack() }
            ) {
                Text(
                    text = "Back",
                    fontSize = 24.sp
                )
            }
        }
    }
}

最後に、画面遷移を作成。

@Composable
fun MainApp() {
    val navController = rememberNavController()
    Scaffold(
        //ボトムナビゲーション
        bottomBar = { BottomNavigation(navController) }
    ) { innerPadding ->
        Box(modifier = Modifier.padding(innerPadding)) {
            NavHost(
                navController = navController,
                startDestination = NavItem.HOME.route
            ) {
                //ホーム
                navigation(startDestination = NavItem.HOME_1.route, route = NavItem.HOME.route) {
                    composable(NavItem.HOME_1.route) { ShowScreen(navController, NavItem.HOME_1, NavItem.HOME_2.route) }
                    composable(NavItem.HOME_2.route) { ShowScreen(navController, NavItem.HOME_2, null) }
                }

                //Eメール
                navigation(startDestination = NavItem.EMAIL_1.route, route = NavItem.EMAIL.route) {
                    composable(NavItem.EMAIL_1.route) { ShowScreen(navController, NavItem.EMAIL_1, NavItem.EMAIL_2.route) }
                    composable(NavItem.EMAIL_2.route) { ShowScreen(navController, NavItem.EMAIL_2, null) }
                }

                //設定
                navigation(startDestination = NavItem.SETTINGS_1.route, route = NavItem.SETTINGS.route) {
                    composable(NavItem.SETTINGS_1.route) { ShowScreen(navController, NavItem.SETTINGS_1, NavItem.SETTINGS_2.route) }
                    composable(NavItem.SETTINGS_2.route) { ShowScreen(navController, NavItem.SETTINGS_2, null) }
                }
            }
        }
    }
}

【Android Jetpack Compose】横画面でのカメラカットアウト(切り欠き)とビューが被らないようにする

はじめに

Android Jetpack Composeで、横画面でのカメラカットアウト(切り欠き)とビューが被らないようにする方法について解説。

方法

windowInsetsPadding(WindowInsets.displayCutout)でpaddingを設定。

    Scaffold(
        modifier = Modifier.windowInsetsPadding(WindowInsets.displayCutout)
    ) { innerPadding ->
        //...(ç•¥)
    }

【Android Jetpack Compose】垂直Sliderの作成

はじめに

Android Jetpack Composeで、垂直Sliderを作成する方法について解説。

方法

graphicsLayerとlayoutを下記のように変更することにより実装できる。

@Composable
fun MainView(modifier: Modifier = Modifier) {
    var value by remember { mutableFloatStateOf(0f) }
    Slider(
        modifier = modifier
            .graphicsLayer {
                rotationZ = 90f
                transformOrigin = TransformOrigin(0f, 0f)
            }
            .layout { measurable, constraints ->
                val placeable = measurable.measure(
                    Constraints(
                        minWidth = constraints.minHeight,
                        maxWidth = constraints.maxHeight,
                        minHeight = constraints.minWidth,
                        maxHeight = constraints.maxHeight,
                    )
                )
                layout(placeable.height, placeable.width) {
                    placeable.place(0, -placeable.height)
                }
            },
        value = value,
        onValueChange = { value = it }
    )
}