Tworzenie interfejsu użytkownika za pomocą Glance

Na tej stronie opisaliśmy, jak obsługiwać rozmiary i tworzyć elastyczne układy responsywne w Glance przy użyciu dotychczasowych komponentów Glance.

Używanie Box, Column i Row

Glance ma 3 główne układy, które można tworzyć:

  • Box: umieszcza elementy jeden na drugim. Odpowiada elementowi RelativeLayout.

  • Column: umieszcza elementy jeden po drugim w osi pionowej. Odpowiada elementowi LinearLayout z orientacją pionową.

  • Row: umieszcza elementy jeden po drugim w osi poziomej. Odpowiada elementowi LinearLayout z orientacją poziomą.

Glance obsługuje Scaffold obiekty. Umieść elementy Column, Row i Box w danym obiekcie Scaffold.

Układ kolumnowy, wierszowy i pudełkowy.
Rysunek 1. Przykłady układów z elementami Column, Row i Box.

Każdy z tych elementów umożliwia zdefiniowanie wyrównania pionowego i poziomego treści oraz ograniczeń szerokości, wysokości, wagi lub dopełnienia za pomocą modyfikatorów. Ponadto każdy element podrzędny może zdefiniować swój modyfikator, aby zmienić odstępy i położenie w elemencie nadrzędnym.

Z tego przykładu dowiesz się, jak utworzyć element Row, który równomiernie rozkłada elementy podrzędne w poziomie, jak widać na rysunku 1:

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

Element Row wypełnia maksymalną dostępną szerokość, a ponieważ każdy element podrzędny ma tę samą wagę, równomiernie dzieli dostępną przestrzeń. 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

Innym sposobem na zapewnienie responsywności treści jest umożliwienie jej przewijania. Jest to możliwe dzięki elementowi LazyColumn. Ten element umożliwia zdefiniowanie zestawu elementów, które mają być wyświetlane w kontenerze z możliwością przewijania w widżecie aplikacji.

Poniższe fragmenty kodu pokazują różne sposoby definiowania elementów 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 poprzednich 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")
    }
}

Zwróć uwagę, że poprzedni fragment kodu nie określa itemId. Określenie itemId pomaga zwiększyć wydajność i zachować pozycję przewijania podczas aktualizacji listy i appWidget w Androidzie 12 i nowszych wersjach (np. podczas dodawania lub usuwania elementów z listy). Z tego przykładu dowiesz się, jak określić itemId:

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

Przewijanie z przyciąganiem

Przewijanie z przyciąganiem to animacja, która umożliwia przyciąganie treści z możliwością przewijania do górnej części kontenera widżetu.

Film 1. Po lewej stronie widać element listy, który nie przyciąga się podczas przewijania , a po prawej – element, który się przyciąga.


Aby zaimplementować przewijanie z przyciąganiem, musisz spełnić te warunki:

  • Zaktualizuj zależność Glance do wersji 1.3.0-alpha02 lub nowszej.
  • Ustaw compileSdk na 37 lub nowszy, ponieważ przewijanie z przyciąganiem jest obsługiwane na urządzeniach z Androidem 17 i nowszym.
  • Skonfiguruj LazyColumn za pomocą VerticalScrollMode. Jeśli urządzenie obsługuje przewijanie z przyciąganiem, użyj SnapScrollMatchHeight. W przeciwnym razie użyj Normal.

Jeśli używasz przewijania z przyciąganiem w przypadku obrazów, zapoznaj się z artykułem Układ kanoniczny obrazu na całą szerokość.

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

Definiowanie SizeMode

AppWidget rozmiary mogą się różnić w zależności od urządzenia, wyboru użytkownika lub programu uruchamiającego, dlatego ważne jest, aby zapewnić elastyczne układy, jak opisano na stronie Zapewnianie elastycznych układów widżetów. Glance upraszcza to dzięki definicji SizeMode i wartości LocalSize. W kolejnych sekcjach opisujemy 3 tryby.

SizeMode.Single

SizeMode.Single to tryb domyślny. Wskazuje, że podano tylko 1 typ treści. Oznacza to, że nawet jeśli zmieni się dostępny rozmiar AppWidget, rozmiar treści nie ulegnie zmianie.

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

Gdy używasz tego trybu, upewnij się, że:

Ogólnie rzecz biorąc, tego trybu należy używać, gdy:

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

SizeMode.Responsive

Ten tryb jest odpowiednikiem zapewniania układów responsywnych, które umożliwiają GlanceAppWidget zdefiniowanie zestawu układów responsywnych ograniczonych określonymi rozmiarami. W przypadku każdego zdefiniowanego rozmiaru treść jest tworzona i mapowana na określony rozmiar podczas tworzenia lub aktualizowania AppWidget. Następnie system wybiera najlepiej dopasowany rozmiar na podstawie dostępnego rozmiaru.

Na przykład w naszym docelowym AppWidget możesz zdefiniować 3 rozmiary i ich 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.

  • Przy pierwszym wywołaniu rozmiar wynosi 100x100. Treść nie zawiera dodatkowego przycisku ani tekstu u góry i u dołu.
  • Przy drugim wywołaniu rozmiar wynosi 250x100. Treść zawiera dodatkowy przycisk, ale nie zawiera tekstu u góry i u dołu.
  • Przy trzecim wywołaniu rozmiar wynosi 250x250. Treść zawiera dodatkowy przycisk i oba teksty.

SizeMode.Responsive to połączenie 2 pozostałych trybów, które umożliwia zdefiniowanie treści responsywnych w ramach wstępnie zdefiniowanych granic. Ogólnie rzecz biorąc, ten tryb działa lepiej i umożliwia płynniejsze przejścia podczas zmiany rozmiaru AppWidget.

W tej tabeli znajdziesz wartość rozmiaru w zależności od SizeMode i dostępnego rozmiaru 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ą podane tylko na potrzeby demonstracji.

SizeMode.Exact

SizeMode.Exact jest odpowiednikiem zapewniania dokładnych układów, które żądają treści GlanceAppWidget za każdym razem, gdy zmieni się dostępny rozmiar AppWidget (np. gdy użytkownik zmieni rozmiar 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 niż określona wartość.

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 wad:

  • AppWidget musi być całkowicie odtworzony za każdym razem, gdy zmieni się jego rozmiar. W przypadku złożonych treści może to powodować problemy z wydajnością i przeskoki interfejsu.
  • Dostępny rozmiar może się różnić w zależności od implementacji programu uruchamiającego. Jeśli np. program uruchamiający nie udostępnia listy rozmiarów, używany jest minimalny możliwy rozmiar.
  • Na urządzeniach z Androidem w wersji starszej niż 12 logika obliczania rozmiaru może nie działać we wszystkich sytuacjach.

Ogólnie rzecz biorąc, tego trybu należy używać, jeśli nie można użyć SizeMode.Responsive (czyli gdy mały zestaw układów responsywnych nie jest możliwy).

Dostęp do zasobów

Aby uzyskać dostęp do dowolnego zasobu Androida, użyj LocalContext.current, jak pokazano w tym przykładzie:

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

Zalecamy podawanie bezpośrednio identyfikatorów zasobów, aby zmniejszyć rozmiar końcowego RemoteViews obiektu i umożliwić korzystanie z zasobów dynamicznych, takich jak dynamiczne kolory.

Elementy i metody akceptują zasoby za pomocą „dostawcy”, np. ImageProvider, lub za pomocą metody przeciążonej, np. GlanceModifier.background(R.color.blue). Przykład:

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

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

Obsługa tekstu

Glance 1.1.0 zawiera interfejs API do ustawiania stylów tekstu. Style tekstu ustawiaj za pomocą atrybutów fontSize, fontWeight lub fontFamily klasy TextStyle.

fontFamily obsługuje wszystkie czcionki systemowe, jak pokazano w tym przykładzie, ale czcionki niestandardowe w aplikacjach nie są obsługiwane:

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

Dodawanie przycisków złożonych

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

Każdy z tych przycisków złożonych wyświetla widok, który można kliknąć i który reprezentuje stan „zaznaczony”.

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

Gdy stan się zmieni, zostanie wywołana podana lambda. Stan zaznaczenia możesz zapisać, jak pokazano w tym przykładzie:

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ż podać atrybut colors dla 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)
    ),

)

Dodatkowe komponenty

Glance 1.1.0 zawiera dodatkowe komponenty, które opisujemy w tej tabeli:

Nazwa Obraz Link odsyłający Uwagi dodatkowe
Wypełniony przycisk alt_text Komponent
Przyciski z konturem alt_text Komponent
Przyciski ikony alt_text Komponent Szkoła podstawowa / ponadpodstawowa / tylko ikona
Pasek tytułu alt_text Komponent
Scaffold Scaffold i pasek tytułu są w tej samej wersji demonstracyjnej.

Więcej informacji o szczegółach projektu znajdziesz w projektach komponentów w tym zestawie projektowym w Figma.

Więcej informacji o układach kanonicznych znajdziesz w artykule Układy kanoniczne widżetów.