Androidアプリケーションのレイアウト作成

今回はAndroidのUI開発の最新ツール「Jetpack Compose」を使用してAndroidアプリケーションのレイアウトを作成してみました。 従来のXMLを用いたUI開発とは全く異なり、簡潔で分かりやすく直感的に開発できるので個人的には非常に好印象でした。 現時点では、まだ実験的な位置づけのようですが、今後の主流になるのは間違いなさそうな感じがします。

※この記事は2023/06/17時点の情報です。

今回の開発環境は「Android Studio Flamingo | 2022.2.1 Patch 2」を使用しました。 エミュレータの仮想デバイスは「10.1 WXGA(Tablet) API 30」で基本的には横向きで使用する前提にしています。 「Jetpack Compose」を利用するのは初めてで、あれこれ調べながら作成しましたが、理解が追いつかず、いろいろおかしい箇所もあるかと思います。 もし流用される場合は、適宜修正してください。ご利用は自己責任でお願いいたします。

完成イメージ

Androidアプリケーションの完成イメージ

ソースは以下のとおりです。新規プロジェクト作成で「Empty Activity」テンプレートを選択した際に自動作成された「MainActivity.kt」を修正しました。 新規プロジェクト作成に関してはコチラの記事を参考にしてください。【はじめてのAndroidアプリケーション(Kotlin)】

package com.sample.jpcs

import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sample.jpcs.ui.theme.JpcsTheme
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material3.IconButton
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext

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

/*
 * 全体のレイアウト
 */
@Composable
fun Sample() {
    //全体
    Box(Modifier.fillMaxSize()) {
        //1行目
        Row(
            modifier = Modifier.offset(2.dp, 5.dp)    //位置
        ) {
            //タイトル
            Text(
                text = "Sampleアプリのタイトルです",  //テキスト
                color = Color(0xffffffff),            //文字の色
                fontSize = 10.sp,                     //文字サイズ
                modifier = Modifier
                    .offset(0.dp, 0.dp)                        //位置
                    .background(color = Color(0xff333333))     //背景色
                    .border(1.dp, Color(0xff000000))           //線
                    .padding(5.dp)                             //余白
            )
            //プルダウン用のエリア
            Column(
                horizontalAlignment  = Alignment.Start, //左寄せ
                modifier = Modifier
                    .padding(50.dp,0.dp,0.dp,0.dp)      //余白
            ){
                SelectBox()                             //プルダウン
            }
            //ボタン用のエリア
            Column(
                horizontalAlignment  = Alignment.End,   //右寄せ
                modifier = Modifier
                    .fillMaxWidth()                     //最大幅
                    .padding(0.dp,0.dp,10.dp,0.dp)      //余白
            ){
                //ボタン
                Button(
                    onClick = {                             //クリックされた時
                        Log.d("Button", "onClick")          //ログ
                    },
                    shape = RoundedCornerShape(10.dp),      //角を丸める
                    contentPadding = PaddingValues(top = 0.dp, start = 0.dp, end = 0.dp, bottom = 0.dp),    //余白
                    modifier = Modifier
                        .border(1.dp, Color(0xff000000), RoundedCornerShape(10.dp))           //線
                        .width(100.dp)                                                        //幅
                        .height(26.dp),                                                       //高さ
                    colors = ButtonDefaults.buttonColors(containerColor = Color(0xffcccccc))  //ボタンの色
                ) {
                    //ボタンのアイコン
                    Icon(
                        imageVector = Icons.Filled.Favorite,    //アイコンのイメージ
                        contentDescription = null,              //説明
                        modifier = Modifier.size(15.dp)         //サイズ
                    )
                    //ボタンのテキスト
                    Text(
                        text ="ボタン",                     //表示テキスト
                        color = Color(0xff333333),          //文字の色
                        fontSize = 10.sp                    //文字のサイズ
                    )
                }
            }
        }
        //2行目
        Row {
            //グリッド
            TableGrid()
        }
    }
}

/*
 * グリッドのレイアウト
 */
@Composable
fun TableGrid() {
    //全体
    Box(
        modifier = Modifier
            .width(1000.dp)                //幅
            .fillMaxHeight(0.85f)          //高さ85%
            .offset(0.dp, 35.dp)           //位置
    ) {
        //サンプルデータを保持するクラスの定義
        data class Sample(val id: Int,
                          val name: String,
                          val colA: String,
                          val colB: String,
                          val colC: String,
                          val colD: String,
                          val colE: String,
                          val colF: String,
                          val colG: String
        )
        //サンプルデータを格納する配列
        val sampleList:MutableList<Sample> = mutableListOf()
        //100回ループしてテストデータを作成
        for(i in 0..99) {
            //配列にサンプルデータを格納
            sampleList.add(Sample(i, "名前$i", "", "", "", "", "", "", ""))
        }
        //1列目の幅
        val col01Width = .1f // 10%
        //2列目の幅
        val col02Width = .2f // 20%
        //3列目の幅
        val col03Width = .1f // 10%
        //4列目の幅
        val col04Width = .1f // 10%
        //5列目の幅
        val col05Width = .1f // 10%
        //6列目の幅
        val col06Width = .1f // 10%
        //7列目の幅
        val col07Width = .1f // 10%
        //8列目の幅
        val col08Width = .1f // 10%
        //9列目の幅
        val col09Width = .1f // 10%

        //縦
        Column(
            modifier = Modifier
                .padding(5.dp)                          //余白
                .verticalScroll(rememberScrollState())  //縦方向にスクロールできるようにする
        ) {
            //ヘッダー行
            Row(
                modifier = Modifier
                    .offset(0.dp, 0.dp)                 //位置
            ) {
                RowComponent(text = "userid", weight = col01Width, Color(0xff36ad5c), Color.White)     //1列目
                RowComponent(text = "name", weight = col02Width, Color(0xff36ad5c), Color.White)       //2列目
                RowComponent(text = "colA", weight = col03Width, Color(0xff25caf7), Color.White)       //3列目
                RowComponent(text = "colB", weight = col04Width, Color(0xff25caf7), Color.White)       //4列目
                RowComponent(text = "colC", weight = col05Width, Color(0xff25caf7), Color.White)       //5列目
                RowComponent(text = "colD", weight = col06Width, Color(0xff25caf7), Color.White)       //6列目
                RowComponent(text = "colE", weight = col07Width, Color(0xff25caf7), Color.White)       //7列目
                RowComponent(text = "colF", weight = col08Width, Color(0xff25caf7), Color.White)       //8列目
                RowComponent(text = "colG", weight = col09Width, Color(0xff25caf7), Color.White)       //9列目
            }

            //サンプルデータを格納した配列の件数分ループする
            for(item in sampleList) {
                //明細行
                Row(
                    modifier = Modifier.offset(0.dp, 0.dp)  //位置
                ) {
                    RowComponent(text = item.id.toString(), weight = col01Width, Color.White, Color.Red)   //1列目
                    RowComponent(text = item.name, weight = col02Width, Color.White, Color.Black)          //2列目
                    RowComponent(text = item.colA, weight = col03Width, Color.White, Color.Blue)           //3列目
                    RowComponent(text = item.colB, weight = col04Width, Color.White, Color.Black)          //4列目
                    RowComponent(text = item.colC, weight = col05Width, Color.White, Color.Black)          //5列目
                    RowComponent(text = item.colD, weight = col06Width, Color.White, Color.Black)          //6列目
                    RowComponent(text = item.colE, weight = col07Width, Color.White, Color.Black)          //7列目
                    RowComponent(text = item.colF, weight = col08Width, Color.White, Color.Black)          //8列目
                    RowComponent(text = item.colG, weight = col09Width, Color.White, Color.Black)          //9列目
                }
            }
        }
    }
}

/*
 * グリッド行のレイアウト
 */
@Composable
fun RowScope.RowComponent(
    text: String,   //引数1: テキスト
    weight: Float,  //比率
    bgColor: Color, //背景色
    color: Color    //テキストの色
) {
    //テキスト
    Text(
        text = text,        //テキスト
        Modifier
            .height(50.dp)              //高さ
            .background(bgColor)        //背景色
            .border(1.dp, Color.Black)  //線
            .weight(weight)             //比率
            .padding(3.dp),             //余白
        fontSize = 30.sp,               //文字のサイズ
        color = color                   //テキストの色
    )
}

/*
 * プルダウンのレイアウト
 */
@Composable
fun SelectBox() {
    //context
    val context = LocalContext.current
    //状態(rememberで保存された値は、他のコンポーザブルのパラメータとして使用できる)
    var expanded by remember { mutableStateOf(false) }
    //プルダウンに表示するデータのリスト
    val optionList = arrayOf("Test1", "Test2", "Test3", "Test4", "Test5")
    //選択値
    var selectedText by remember { mutableStateOf(optionList[0]) }
    //全体
    Box(
        modifier = Modifier.wrapContentSize(Alignment.TopStart) //左寄せ
    ) {
        //テキスト
        Text(
            text = selectedText,             //テキストには選択値をセット
            color = Color(0xff333333),       //文字の色
            fontSize = 10.sp,                //文字のサイズ
            modifier = Modifier
                .width(100.dp)               //幅
                .height(25.dp)               //高さ
                .offset(0.dp, 0.dp)          //位置
                .background(color = Color(0xffffffff),shape = RoundedCornerShape(5.dp)) //背景色と角を丸める
                .border(1.dp, Color(0xff333333),shape = RoundedCornerShape(5.dp))       //線の色と角を丸める
                .padding(5.dp)               //余白
        )
        //アイコンボタン
        IconButton(
            onClick = { expanded = !expanded }, //クリックした時(状態を切り替える)
            modifier = Modifier
                .padding(0.dp,0.dp)             //余白
                .height(25.dp)                  //高さ
                .absoluteOffset(90.dp, 0.dp)    //位置
        ) {
            //アイコン
            Icon(
                imageVector = Icons.Default.ArrowDropDown,   //アイコンイメージ
                contentDescription = "選択"                  //説明
            )
        }
        //プルダウン
        DropdownMenu(
            expanded = expanded,                    //状態
            onDismissRequest = { expanded = false } //範囲外をタップした時のコールバック
        ) {
            //データの件数分ループ
            optionList.forEach { item ->
                //プルダウンの中身
                DropdownMenuItem(
                    text = { Text(text = item) },   //テキスト
                    onClick = {                     //クリックした時
                        selectedText = item         //選択したデータを保持
                        expanded = false            //状態
                        Toast.makeText(context, item, Toast.LENGTH_SHORT).show() //メッセージを表示
                    }
                )
            }
        }
    }
}

/*
 * プレビュー用
 */
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    JpcsTheme {
        //全体のレイアウト
        Sample()
    }
}

初めての「Jetpack Compose」でしたが、Kotlinのシンプルさが活かされていて今風な感じだったので、XMLでのUI開発に比べると とっつきやすく抵抗はありませんでした。バージョンアップと共にまだまだ変更が加わっていく可能性が高いので、今後も勉強に励みます!

管理人情報