Tworzenie interfejsu użytkownika za pomocą Glance

Z tego artykułu dowiesz się, jak obsługiwać rozmiary oraz tworzyć elastyczne i elastyczne układy w usłudze Glance z wykorzystaniem istniejących komponentów Glance.

Użyj usług Box, Column i Row

Funkcja Glance ma 3 główne układy kompozycyjne:

  • Box: powoduje umieszczenie elementów na drugim. To przekłada się na RelativeLayout.

  • Column: powoduje umieszczanie elementów po sobie na osi pionowej. To przekłada się na LinearLayout w orientacji pionowej.

  • Row: powoduje umieszczenie elementów po sobie na osi poziomej. W orientacji poziomej wyświetli się LinearLayout.

Funkcja W skrócie obsługuje obiekty Scaffold. Umieść kompozycje Column, Row i Box w określonym obiekcie Scaffold.

Obraz układu kolumny, wiersza i pola.
Rysunek 1. Przykłady układów z kolumnami, wierszami i ramką.

Każdy z tych elementów kompozycyjnych pozwala zdefiniować wyrównanie treści w pionie i w poziomie oraz ograniczenia szerokości, wysokości, wagi i dopełnienia za pomocą modyfikatorów. Dodatkowo każde wydawca podrzędny może zdefiniować swój modyfikator, aby zmienić przestrzeń i miejsce docelowe w elemencie nadrzędnym.

Ten przykład pokazuje, jak utworzyć obiekt Row równomiernie rozłożony 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 każde dziecko ma tę samą wagę, więc dzieli dostępne miejsce równomiernie. Możesz definiować różne wagi, rozmiary, dopełnienia i wyrównania, aby dostosować układy do swoich potrzeb.

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

Innym sposobem na dostarczanie elastycznych treści jest umożliwienie ich przewijania. Jest to możliwe dzięki funkcji kompozycyjnej LazyColumn. Ta funkcja kompozycyjna pozwala zdefiniować zestaw elementów do wyświetlania w kontenerze z możliwością przewijania w widżecie aplikacji.

Poniższe fragmenty kodu pokazują różne sposoby definiowania elementów w polu 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ż wykorzystać kombinację 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 kodu nie zawiera elementu itemId. Podanie właściwości itemId pomaga zwiększyć wydajność oraz utrzymać pozycję przewijania na liście i w aktualizacjach appWidget w Androidzie 12 i nowszych (na przykład podczas dodawania lub usuwania elementów z listy). Poniższy przykład pokazuje, jak określić itemId:

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

Zdefiniuj SizeMode

Rozmiary AppWidget mogą się różnić w zależności od urządzenia, wyboru użytkownika i programu uruchamiającego, dlatego ważne jest, aby udostępnić elastyczne układy zgodnie z opisem na stronie Udostępnianie elastycznych układów widżetów. Funkcja W skrócie upraszcza ten proces za pomocą definicji SizeMode i wartości LocalSize. W sekcjach poniżej opisujemy 3 tryby.

SizeMode.Single

SizeMode.Single to tryb domyślny. Oznacza to, że udostępniany jest tylko jeden typ treści. Oznacza to, że nawet jeśli zmieni się dostępny rozmiar AppWidget, nie zmieni się rozmiar treści.

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:

  • Wartości metadanych dotyczących minimalnego i maksymalnego rozmiaru są prawidłowo zdefiniowane na podstawie rozmiaru treści.
  • Treść jest wystarczająco elastyczna i mieści się w oczekiwanym zakresie rozmiarów.

Ogólnie należy używać tego trybu, 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 do określonych rozmiarów. W przypadku każdego zdefiniowanego rozmiaru treść jest tworzona i mapowana na konkretny rozmiar podczas tworzenia lub aktualizowania elementu AppWidget. System wybiera najlepsze dopasowanie na podstawie dostępnego rozmiaru.

Na przykład w miejscu docelowym AppWidget możesz zdefiniować 3 rozmiary i jej 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 zmapowana do zdefiniowanego rozmiaru.

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

SizeMode.Responsive to połączenie 2 pozostałych trybów i umożliwia definiowanie elastycznych treści we wcześniej zdefiniowanych 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ść 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ą podane tylko w celach demonstracyjnych.

SizeMode.Exact

SizeMode.Exact jest odpowiednikiem podawania dokładnych układów, które żądają treści GlanceAppWidget za każdym razem, gdy dostępny jest rozmiar elementu 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ść przekracza określoną 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ż pozostałe, ale ma kilka ograniczeń:

  • Przy każdej zmianie rozmiaru element AppWidget musi być całkowicie odtworzony. Może to prowadzić do problemów z wydajnością i skoków interfejsu, gdy treść jest skomplikowana.
  • 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 z systemem starszym niż Android 12 logika obliczania rozmiaru może nie działać w niektórych sytuacjach.

Z tego trybu warto korzystać, gdy nie można użyć elementu SizeMode.Responsive (czyli gdy nie ma możliwości użycia małego zestawu układów elastycznych).

Dostęp do zasobów

Użyj polecenia LocalContext.current, aby uzyskać dostęp do zasobów Androida w następujący sposób:

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

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

Komponenty kompozycyjne i metody akceptują zasoby udostępniane przez „dostawcę”, np. ImageProvider, lub przy użyciu 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",
)

Tekst uchwytu

Glance 1.1.0 zawiera interfejs API do ustawiania stylów tekstu. Ustaw style tekstu za pomocą atrybutów fontSize, fontWeight i 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"
)

Dodaj przyciski złożone

Złożone przyciski zostały wprowadzone w Androidzie 12. W skrócie działa zgodność wsteczna w przypadku tych typów przycisków złożonych:

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

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 uruchomiona jest lambda. Stan kontroli możesz zapisać, tak jak 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) }
        )
    }
}

Aby dostosować ich kolory, możesz też podać atrybut colors w atrybutach CheckBox, Switch i RadioButton:

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 obejmuje udostępnienie dodatkowych komponentów, jak opisano w tej tabeli:

Nazwa Obraz Link referencyjny Uwagi dodatkowe
Wypełniony przycisk tekst_alternatywny Komponent
Przyciski z konspektem tekst_alternatywny Komponent
Przyciski ikon tekst_alternatywny Komponent Główne / Dodatkowe / Tylko ikony
Pasek tytułu tekst_alternatywny Komponent
Ruszt Ruszt i pasek tytułu są w tej samej wersji demonstracyjnej.

Więcej informacji o projektach znajdziesz w projektach komponentów w tym zestawie do projektowania w Figmie.