Tworzenie interfejsu użytkownika za pomocą Glance

Na tej stronie opisujemy, jak obsługiwać rozmiary oraz tworzyć elastyczne i elastyczne układy za pomocą Glance.

Użyj aplikacji Box, Column i Row

W Glance dostępne są 3 główne układy kompozycyjne:

  • Box: umieszcza elementy nad drugim. Tłumaczy na RelativeLayout.

  • Column: umieszcza elementy jeden po drugim na osi pionowej. Wyświetla się w polu LinearLayout w orientacji pionowej.

  • Row: umieszcza elementy jeden po drugim na osi poziomej. Wyświetla się w polu LinearLayout w orientacji poziomej.

Obraz układu kolumny, wiersza i pola.
Rysunek 1. Przykłady układów z kolumnami, wierszem i polem.

Każdy z tych elementów kompozycyjnych umożliwia definiowanie wyrównania w pionie i poziomie treści oraz ograniczeń szerokości, wysokości, wagi i dopełnienia za pomocą modyfikatorów. Dodatkowo każdy element podrzędny może zdefiniować swój modyfikator zmieniający spację i położenie w obrębie elementu nadrzędnego.

Poniższy przykład pokazuje, jak utworzyć element Row, który równomiernie rozkłada elementy podrzędne w poziomie, tak jak to widać na rysunku 1:

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

Row wypełnia maksymalną dostępną szerokość, a ponieważ każdy element podrzędny ma taką samą wagę, dzieli je równomiernie na dostępne miejsce. Możesz zdefiniować różne wagi, rozmiary, dopełnienia lub wyrównania, aby dostosować układy do swoich potrzeb.

Używanie układów z możliwością przewijania

Kolejnym sposobem zapewniania elastycznej zawartości jest umożliwienie jej przewijania. Jest to możliwe dzięki elementowi kompozycyjnemu LazyColumn. Ta funkcja kompozycyjna pozwala zdefiniować zbiór elementów wyświetlanych w kontenerze z możliwością przewijania w widżecie aplikacji.

Poniższe fragmenty kodu pokazują różne sposoby definiowania elementów w obrębie znaczników LazyColumn.

Możesz podać liczbę elementów:

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

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

Podaj poszczególne elementy:

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

Podaj listę lub tablicę elementów:

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

Możesz też użyć kombinacji powyższych przykładów:

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")
    }
}

Pamiętaj, że poprzedni fragment nie zawiera tych danych: itemId. Określenie itemId pomaga zwiększyć wydajność i utrzymać pozycję przewijania na liście oraz aktualizacje appWidget od Androida 12 i nowszych (np. przy dodawaniu elementów do listy lub ich usuwaniu). Ten przykład pokazuje, jak określić itemId:

items(items = peopleList, key = { person -> person.id }) { person ->
    Text(person.name)
}

Zdefiniuj pole SizeMode

Rozmiary AppWidget mogą się różnić w zależności od urządzenia, wyboru użytkownika lub programu uruchamiającego, dlatego ważne jest zapewnienie elastycznych układów opisanych na stronie Udostępnianie elastycznych układów widżetów. Funkcja Glance upraszcza tę sprawę, korzystając z definicji SizeMode i wartości LocalSize. W sekcjach poniżej opisujemy 3 tryby.

SizeMode.Single

Trybem domyślnym jest SizeMode.Single. Oznacza to, że dostarczany jest tylko 1 typ treści. Oznacza to, że nawet jeśli zmieni się rozmiar dostępnego elementu AppWidget, rozmiar treści się nie zmieni.

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
        // ...
    }
}

Podczas korzystania z tego trybu upewnij się, że:

  • Minimalny i maksymalny wartości metadanych rozmiaru są prawidłowo zdefiniowane na podstawie rozmiaru treści.
  • Treść jest wystarczająco elastyczna w oczekiwanym zakresie rozmiarów.

Ogólnie tego trybu należy używać, gdy:

a) element AppWidget ma stały rozmiar lub b) nie zmienia swojej zawartości po zmianie rozmiaru.

SizeMode.Responsive

Ten tryb jest odpowiednikiem udostępniania układów elastycznych, które umożliwiają GlanceAppWidget definiowanie zestawu układów elastycznych ograniczonych przez określone rozmiary. W przypadku każdego zdefiniowanego rozmiaru treść jest tworzona i mapowana na konkretny rozmiar podczas tworzenia lub aktualizacji elementu AppWidget. a następnie system wybierze najlepiej dopasowane na podstawie dostępnego rozmiaru.

Na przykład w elemencie docelowym AppWidget możesz zdefiniować 3 rozmiary i jego zawartość:

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")
            }
        }
    }
}

W poprzednim przykładzie metoda provideContent jest wywoływana 3 razy i mapowana na zdefiniowany rozmiar.

  • W pierwszym wywołaniu rozmiar przyjmuje wartość 100x100. Nie zawiera dodatkowego przycisku ani tekstu na górze i na dole.
  • W drugim wywołaniu rozmiar przyjmuje wartość 250x100. Treść zawiera dodatkowy przycisk, ale nie zawiera tekstu na górze ani na dole.
  • W trzecim wywołaniu rozmiar przyjmuje wartość 250x250. Zawiera ona dodatkowy przycisk i oba teksty.

SizeMode.Responsive to połączenie dwóch pozostałych trybów i umożliwia definiowanie treści elastycznych w zdefiniowanych wstępnie granicach. Ogólnie ten tryb działa lepiej i umożliwia płynniejsze przejścia po zmianie rozmiaru elementu AppWidget.

W tabeli poniżej znajdziesz wartości rozmiaru w zależności od dostępnego rozmiaru SizeMode i AppWidget:

Dostępny rozmiar 105 x 110 203 x 112 72 x 72 203 x 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 x 150
SizeMode.Responsive 80 x 100 80 x 100 80 x 100 150 x 120
* Dokładne wartości służą tylko do celów demonstracyjnych.

SizeMode.Exact

SizeMode.Exact jest odpowiednikiem udostępniania dokładnych układów, które powoduje żądanie treści GlanceAppWidget za każdym razem, gdy zmieni się dostępny rozmiar AppWidget (np. gdy użytkownik zmieni rozmiar elementu AppWidget na ekranie głównym).

Na przykład w widżecie docelowym można dodać dodatkowy przycisk, jeśli dostępna szerokość jest większa od określonej wartości.

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")
                }
            }
        }
    }
}

Ten tryb zapewnia większą elastyczność niż inne, ale ma kilka ograniczeń:

  • Przy każdej zmianie rozmiaru trzeba ponownie utworzyć atrybut AppWidget. Może to powodować problemy z wydajnością i przeskakiwanie interfejsu, gdy treść jest złożona.
  • Dostępny rozmiar może się różnić w zależności od implementacji programu uruchamiającego. Jeśli na przykład program uruchamiający nie podaje listy rozmiarów, używany jest minimalny możliwy rozmiar.
  • Na urządzeniach starszych niż Android 12 logika obliczania rozmiaru może nie działać w niektórych sytuacjach.

Ogólnie używaj tego trybu, jeśli nie można użyć SizeMode.Responsive (czyli nie da się użyć małego zestawu układów elastycznych).

Dostęp do zasobów

Użyj metody LocalContext.current, aby uzyskać dostęp do dowolnych zasobów Androida, jak w tym przykładzie:

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

Zalecamy bezpośrednie podawanie identyfikatorów zasobów. Pozwoli to zmniejszyć rozmiar końcowego obiektu RemoteViews i włączyć zasoby dynamiczne, takie jak kolory dynamiczne.

Obiekty kompozycyjne i metody akceptują zasoby, korzystając z funkcji dostawcy, np. ImageProvider, lub metody przeciążenia, takiej jak GlanceModifier.background(R.color.blue). Na przykład:

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

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

Dodaj przyciski złożone

Przyciski złożone zostały wprowadzone w Androidzie 12. Funkcja Glance obsługuje wsteczną zgodność tych typów przycisków złożonych:

Każdy z tych złożonych przycisków wyświetla klikalny widok, który przedstawia stan „zaznaczony”.

var isApplesChecked by remember { mutableStateOf(false) }
var isEnabledSwitched by remember { mutableStateOf(false) }
var isRadioChecked by remember { mutableStateOf(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"
)

Po zmianie stanu wywoływana jest podana funkcja lambda. Stan kontroli możesz zapisać w następujący sposób:

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) }
        )
    }
}

Możesz też dodać atrybut colors do elementów CheckBox, Switch i RadioButton, aby dostosować ich kolory:

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)
    ),

)