Glance で UI を作成する

このページでは、既存の Glance コンポーネントを使用して、Glance でサイズを処理し、柔軟でレスポンシブなレイアウトを提供する方法について説明します。

BoxColumnRow を使用します。

Glance には、次の 3 つのメインのコンポーザブル レイアウトがあります。

  • Box: 要素を重ねて配置します。これは RelativeLayout に変換されます。

  • Column: 要素を垂直軸に沿って配置します。これは、縦向きの LinearLayout に変換されます。

  • Row: 要素を水平軸に沿って配置します。これは、横向きの LinearLayout に変換されます。

Glance は Scaffold オブジェクトをサポートしています。ColumnRowBox のコンポーザブルを、指定された Scaffold オブジェクト内に配置します。

Column、Row、Box のレイアウト。
図 1. Column、Row、Box を使用したレイアウトの例。

これらの各コンポーザブルでは、修飾子を使用して、コンテンツの垂直方向と水平方向の配置、幅、高さ、重み、パディングの制約を定義できます。また、各子要素は、親要素内のスペースと配置を変更する修飾子を定義できます。

次の例は、図 1 に示すように、子を水平方向に均等に配置する Row を作成する方法を示しています。

Row(modifier = GlanceModifier.fillMaxWidth().padding(16.dp)) {
    val modifier = GlanceModifier.defaultWeight()
    Text("first", modifier)
    Text("second", modifier)
    Text("third", modifier)
}

Row は使用可能な最大幅を埋め、各子要素の重みが同じであるため、使用可能なスペースを均等に共有します。さまざまな重み、サイズ、パディング、配置を定義して、ニーズに合わせてレイアウトを調整できます。

スクロール可能なレイアウトを使用する

レスポンシブ コンテンツを提供する別の方法として、スクロール可能にする方法があります。これは、LazyColumn コンポーザブルで実現できます。このコンポーザブルを使用すると、アプリ ウィジェットのスクロール可能なコンテナ内に表示するアイテムのセットを定義できます。

次のスニペットは、LazyColumn 内のアイテムを定義するさまざまな方法を示しています。

アイテムの数を指定できます。

// Remember to import Glance Composables
// import androidx.glance.appwidget.layout.LazyColumn

LazyColumn {
    items(10) { index: Int ->
        Text(
            text = "Item $index",
            modifier = GlanceModifier.fillMaxWidth()
        )
    }
}

個々のアイテムを指定します。

LazyColumn {
    item {
        Text("First Item")
    }
    item {
        Text("Second Item")
    }
}

アイテムのリストまたは配列を指定します。

LazyColumn {
    items(peopleNameList) { name ->
        Text(name)
    }
}

上記の例を組み合わせて使用することもできます。

LazyColumn {
    item {
        Text("Names:")
    }
    items(peopleNameList) { name ->
        Text(name)
    }

    // or in case you need the index:
    itemsIndexed(peopleNameList) { index, person ->
        Text("$person at index $index")
    }
}

前のスニペットでは itemId が指定されていません。itemId を指定すると、Android 12 以降でパフォーマンスを改善し、リストと appWidget の更新を通じてスクロール位置を維持するのに役立ちます(たとえば、リストからアイテムを追加または削除する場合など)。次の例は、itemId を指定する方法を示しています。

items(items = peopleList, itemId = { person -> person.id.hashCode().toLong() }) { person ->
    Text(person.name)
}

スナップ スクロール

スナップ スクロールは、スクロール可能なコンテンツをウィジェット コンテナの上部にスナップさせるアニメーションです。

動画 1. 左はスクロール中にスナップしないリストアイテム、右はスナップするリストアイテムを示しています。


スナップ スクロールを実装するには、次の条件を満たしていることを確認してください。

  • Glance の依存関係を 1.3.0-alpha02 以降に更新します。
  • compileSdk を 37 以上に設定します。スナップ スクロールは Android 17 以降を搭載したデバイスでサポートされています。
  • VerticalScrollMode を使用して LazyColumn を構成します。デバイスがスナップ スクロールをサポートしている場合は、SnapScrollMatchHeight を使用します。それ以外の場合は、Normal を使用します。

画像でスナップ スクロールを使用している場合は、全画面表示の画像の正規レイアウトをご覧ください。

@Composable
fun SnapScrollLayout() {
    val height = LocalSize.current.height
    val items = listOf(
        ColorItem(Color.Red, "Red"),
        ColorItem(Color.Yellow, "Yellow"),
        ColorItem(Color.Blue, "Blue")
    )

    val scrollMode = if (Build.VERSION.SDK_INT >= 37) {
        VerticalScrollMode.SnapScrollMatchHeight(height)
    } else {
        VerticalScrollMode.Normal
    }

    LazyColumn(
        verticalScrollMode = scrollMode
    ) {
        items(items) { item ->
            ColorCard(item, height)
        }
    }
}

@Composable
private fun ColorCard(item: ColorItem, height: Dp) {
    Box(
        modifier = GlanceModifier
            .background(item.color)
            .fillMaxWidth()
            .height(height),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = item.name,
            modifier = GlanceModifier.background(Color.White)
        )
    }
}

SizeMode を定義する

AppWidget のサイズは、デバイス、ユーザーの選択、ランチャーによって異なる可能性があるため、柔軟なウィジェット レイアウトを提供するページで説明されているように、柔軟なレイアウトを提供することが重要です。Glance では、SizeMode 定義と LocalSize 値を使用して、この処理を簡素化しています。以降のセクションでは、3 つのモードについて説明します。

SizeMode.Single

SizeMode.Single モード(デフォルト モード): これは、1 種類のコンテンツのみが提供されることを示します。つまり、AppWidget の利用可能なサイズが変更されても、コンテンツのサイズは変更されません。

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Single

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the minimum size or resizable
        // size defined in the App Widget metadata
        val size = LocalSize.current
        // ...
    }
}

このモードを使用する場合は、次の点を確認してください。

  • コンテンツのサイズに基づいて、最小サイズと最大サイズのメタデータ値が適切に定義されている。
  • コンテンツは想定されるサイズ範囲内で十分に柔軟です。

一般に、このモードは次のいずれかの条件を満たす場合に使用します。

a) AppWidget のサイズが固定されている、または b) サイズ変更時にコンテンツが変更されない。

SizeMode.Responsive

このモードは、レスポンシブ レイアウトを提供するのと同等です。これにより、GlanceAppWidget は特定のサイズで区切られたレスポンシブ レイアウトのセットを定義できます。定義されたサイズごとに、AppWidget の作成時または更新時にコンテンツが作成され、特定のサイズにマッピングされます。次に、使用可能なサイズに基づいて最適なサイズが選択されます。

たとえば、宛先 AppWidget で、3 つのサイズとそのコンテンツを定義できます。

class MyAppWidget : GlanceAppWidget() {

    companion object {
        private val SMALL_SQUARE = DpSize(100.dp, 100.dp)
        private val HORIZONTAL_RECTANGLE = DpSize(250.dp, 100.dp)
        private val BIG_SQUARE = DpSize(250.dp, 250.dp)
    }

    override val sizeMode = SizeMode.Responsive(
        setOf(
            SMALL_SQUARE,
            HORIZONTAL_RECTANGLE,
            BIG_SQUARE
        )
    )

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be one of the sizes defined above.
        val size = LocalSize.current
        Column {
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            }
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width >= HORIZONTAL_RECTANGLE.width) {
                    Button("School")
                }
            }
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "provided by X")
            }
        }
    }
}

前の例では、provideContent メソッドが 3 回呼び出され、定義されたサイズにマッピングされます。

  • 最初の呼び出しでは、サイズは 100x100 と評価されます。コンテンツには、追加のボタンや上部と下部のテキストは含まれません。
  • 2 回目の呼び出しでは、サイズは 250x100 と評価されます。コンテンツには追加ボタンが含まれますが、上部と下部のテキストは含まれません。
  • 3 回目の呼び出しでは、サイズは 250x250 と評価されます。コンテンツには、追加のボタンと両方のテキストが含まれます。

SizeMode.Responsive は他の 2 つのモードを組み合わせたもので、事前定義された境界内でレスポンシブ コンテンツを定義できます。一般的に、このモードはパフォーマンスが高く、AppWidget のサイズ変更時にスムーズなトランジションを実現します。

次の表に、SizeModeAppWidget の使用可能なサイズに応じたサイズの値を示します。

利用可能なサイズ 105 x 110 203 x 112 72 x 72 203×150
SizeMode.Single 110 x 110 110 x 110 110 x 110 110 x 110
SizeMode.Exact 105 x 110 203 x 112 72 x 72 203×150
SizeMode.Responsive 80 x 100 80 x 100 80 x 100 150×120
* 正確な値はデモ専用です。

SizeMode.Exact

SizeMode.Exact は、正確なレイアウトを提供するのと同等です。これは、利用可能な AppWidget サイズが変更されるたびに(たとえば、ユーザーがホーム画面で AppWidget のサイズを変更したとき)、GlanceAppWidget コンテンツをリクエストします。

たとえば、利用可能な幅が特定の値より大きい場合、宛先ウィジェットに追加のボタンを追加できます。

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Exact

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the size of the AppWidget
        val size = LocalSize.current
        Column {
            Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width > 250.dp) {
                    Button("School")
                }
            }
        }
    }
}

このモードは他のモードよりも柔軟性が高いですが、いくつかの注意点があります。

  • サイズが変更されるたびに、AppWidget を完全に再作成する必要があります。コンテンツが複雑な場合、パフォーマンスの問題や UI のジャンプが発生する可能性があります。
  • 利用可能なサイズは、ランチャーの実装によって異なる場合があります。たとえば、ランチャーがサイズのリストを提供しない場合、可能な最小サイズが使用されます。
  • Android 12 より前のデバイスでは、サイズ計算ロジックがすべての状況で機能しない可能性があります。

一般に、SizeMode.Responsive を使用できない場合(つまり、レスポンシブ レイアウトの小さなセットが実現できない場合)は、このモードを使用する必要があります。

リソースにアクセスする

次の例に示すように、LocalContext.current を使用して Android リソースにアクセスします。

LocalContext.current.getString(R.string.glance_title)

最終的な RemoteViews オブジェクトのサイズを縮小し、動的な色などの動的なリソースを有効にするため、リソース ID を直接指定することをおすすめします。

コンポーザブルとメソッドは、ImageProvider などの「プロバイダ」を使用するか、GlanceModifier.background(R.color.blue) などのオーバーロード メソッドを使用してリソースを受け取ります。次に例を示します。

Column(
    modifier = GlanceModifier.background(R.color.default_widget_background)
) { /**...*/ }

Image(
    provider = ImageProvider(R.drawable.ic_logo),
    contentDescription = "My image",
)

テキストを処理する

Glance 1.1.0 には、テキスト スタイルを設定するための API が含まれています。TextStyle クラスの fontSizefontWeightfontFamily 属性を使用してテキスト スタイルを設定します。

fontFamily は、次の例に示すように、すべてのシステム フォントをサポートしていますが、アプリ内のカスタム フォントはサポートしていません。

Text(
    style = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 18.sp,
        fontFamily = FontFamily.Monospace
    ),
    text = "Example Text"
)

複合ボタンを追加する

複合ボタンは Android 12 で導入されました。Glance は、次のタイプの複合ボタンの下位互換性をサポートしています。

これらの複合ボタンはそれぞれ、チェックされた状態を表すクリック可能なビューを表示します。

var isApplesChecked by remember { mutableStateOf(false) }
var isEnabledSwitched by remember { mutableStateOf(false) }
var isRadioChecked by remember { mutableIntStateOf(0) }

CheckBox(
    checked = isApplesChecked,
    onCheckedChange = { isApplesChecked = !isApplesChecked },
    text = "Apples"
)

Switch(
    checked = isEnabledSwitched,
    onCheckedChange = { isEnabledSwitched = !isEnabledSwitched },
    text = "Enabled"
)

RadioButton(
    checked = isRadioChecked == 1,
    onClick = { isRadioChecked = 1 },
    text = "Checked"
)

状態が変化すると、指定されたラムダがトリガーされます。次の例に示すように、チェック状態を保存できます。

class MyAppWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        val myRepository = MyRepository.getInstance()

        provideContent {
            val scope = rememberCoroutineScope()

            val saveApple: (Boolean) -> Unit =
                { scope.launch { myRepository.saveApple(it) } }
            MyContent(saveApple)
        }
    }

    @Composable
    private fun MyContent(saveApple: (Boolean) -> Unit) {

        var isAppleChecked by remember { mutableStateOf(false) }

        Button(
            text = "Save",
            onClick = { saveApple(isAppleChecked) }
        )
    }
}

CheckBoxSwitchRadioButtoncolors 属性を指定して、色をカスタマイズすることもできます。

CheckBox(
    // ...
    colors = CheckboxDefaults.colors(
        checkedColor = ColorProvider(day = colorAccentDay, night = colorAccentNight),
        uncheckedColor = ColorProvider(day = Color.DarkGray, night = Color.LightGray)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked }
)

Switch(
    // ...
    colors = SwitchDefaults.colors(
        checkedThumbColor = ColorProvider(day = Color.Red, night = Color.Cyan),
        uncheckedThumbColor = ColorProvider(day = Color.Green, night = Color.Magenta),
        checkedTrackColor = ColorProvider(day = Color.Blue, night = Color.Yellow),
        uncheckedTrackColor = ColorProvider(day = Color.Magenta, night = Color.Green)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked },
    text = "Enabled"
)

RadioButton(
    // ...
    colors = RadioButtonDefaults.colors(
        checkedColor = ColorProvider(day = Color.Cyan, night = Color.Yellow),
        uncheckedColor = ColorProvider(day = Color.Red, night = Color.Blue)
    ),

)

追加コンポーネント

Glance 1.1.0 には、次の表に示すように、追加コンポーネントのリリースが含まれています。

名前 画像 参照リンク その他の注意事項
塗りつぶしボタン alt_text コンポーネント
アウトライン ボタン alt_text コンポーネント
アイコンボタン alt_text コンポーネント プライマリ / セカンダリ / アイコンのみ
タイトルバー alt_text コンポーネント
Scaffold Scaffold と Title bar は同じデモにあります。

デザインの詳細については、Figma のデザインキットにあるコンポーネント デザインをご覧ください。

正規レイアウトについて詳しくは、正規ウィジェット レイアウトをご覧ください。