Tworzenie interfejsu użytkownika za pomocą Glance

Na tej stronie opisaliśmy, jak obsługiwać rozmiary i zapewniać elastyczne i responsywne układy za pomocą Glance, korzystając z dotychczasowych komponentów Glance.

Użyj właściwości Box, ColumnRow.

Glance ma 3 główne układy:

  • Box: umieszcza elementy jeden na drugim. Jest to RelativeLayout.

  • Column: elementy są umieszczane jeden po drugim na osi pionowej. Przekształca się w LinearLayout o orientacji pionowej.

  • Row: elementy są umieszczane jeden po drugim na osi poziomej. Przekształca się on w LinearLayout w orientacji poziomej.

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

Obraz układu kolumn, wierszy i pól.
Rysunek 1. Przykłady układów z kolumnami, wierszami i polem.

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

Z tego przykładu dowiesz się, jak utworzyć element Row, który równomiernie rozmieści swoje elementy potomne w poziomie (patrz rysunek 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żde dziecko ma taką samą wagę, mają równy dostęp do dostępnej przestrzeni. Aby dostosować układy do swoich potrzeb, możesz zdefiniować różne wagi, rozmiary, odstępy lub wyrównania.

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

Innym sposobem na wyświetlanie responsywnych treści jest umożliwienie ich przewijania. Jest to możliwe dzięki komponentowi LazyColumn. Ta kompozycja umożliwia zdefiniowanie zestawu elementów, które mają być wyświetlane w kontenerze, którego zawartość można przewijać, w widżecie aplikacji.

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

Dostarczaj 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 kodu nie określa wartości itemId. Określanie pozycji itemId pomaga poprawić wydajność i utrzymać pozycję przewijania podczas aktualizacji listy i appWidget od Androida 12 (np. 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 lub programu uruchamiania, dlatego ważne jest, aby udostępniać elastyczne układy zgodnie z opisem na stronie Umieszczanie elastycznych układów widżetów. Skróty upraszczają to dzięki definicji SizeMode i wartości LocalSize. W sekcjach poniżej opisujemy 3 tryby.

SizeMode.Single

SizeMode.Single to domyślny tryb. Wskazuje, że jest dostępny tylko jeden typ treści. Oznacza to, że nawet jeśli rozmiar AppWidget ulegnie zmianie, rozmiar treści pozostanie bez zmian.

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:

  • Minimalne i maksymalne rozmiary wartości metadanych są odpowiednio zdefiniowane na podstawie rozmiaru treści.
  • Treści są wystarczająco elastyczne w oczekiwanym zakresie rozmiarów.

Ogólnie rzecz biorąc, należy używać tego trybu, 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, co pozwala GlanceAppWidget zdefiniować zestaw układów responsywnych ograniczonych określonymi rozmiarami. W przypadku każdego zdefiniowanego rozmiaru treści są tworzone i mapowane na konkretny rozmiar podczas tworzenia lub aktualizowania AppWidget. Następnie system wybierze najbardziej pasujący na podstawie dostępnego rozmiaru.

Na przykład w miejscu 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 przypisana do zdefiniowanego rozmiaru.

  • W pierwszym wywołaniu rozmiar ma wartość 100x100. Treść nie zawiera dodatkowego przycisku ani tekstów u góry i dołu.
  • W drugim wywołaniu rozmiar ma wartość 250x100. Treść obejmuje dodatkowy przycisk, ale nie teksty u góry i na dole.
  • W 3 wywołaniu rozmiar jest określany jako 250x250. Treści obejmują dodatkowy przycisk i oba teksty.

SizeMode.Responsive to połączenie 2 innych trybów, które umożliwia definiowanie treści responsywnych w określonych granicach. Ogólnie ten tryb zapewnia lepszą wydajność i płynniejsze przejścia przy zmianie rozmiaru AppWidget.

W tabeli poniżej znajdziesz wartość rozmiaru w zależności od dostępnego rozmiaru SizeModeAppWidget:

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 celu demonstracyjnym.

SizeMode.Exact

SizeMode.Exact jest równoważne dostarczaniu dokładnych układów, które powoduje żądanie treści GlanceAppWidget za każdym razem, gdy zmienia się dostępna wielkość AppWidget (np. gdy użytkownik zmienia rozmiar AppWidget na ekranie głównym).

Na przykład w widżecie miejsca docelowego 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 ograniczeń:

  • Za każdym razem, gdy zmienia się rozmiar, AppWidget musi zostać całkowicie odtworzona. Może to powodować problemy z wydajnością i przeskakiwanie interfejsu, gdy treści są złożone.
  • Dostępny rozmiar może się różnić w zależności od implementacji programu uruchamiającego. Jeśli na przykład w launcherze nie ma listy rozmiarów, używany jest najmniejszy możliwy rozmiar.
  • Na urządzeniach z Androidem 11 i starszym logika obliczania rozmiaru może nie działać we wszystkich sytuacjach.

Z tego trybu należy korzystać, jeśli nie można użyć SizeMode.Responsive (czyli nie można utworzyć małego zestawu elastycznych układów).

Dostęp do zasobów

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

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

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

Elementy składane i metody przyjmują zasoby za pomocą „dostawcy”, takiego jak ImageProvider, lub za pomocą metody przeciążenia, takiej jak 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. Ustaw style tekstu za pomocą atrybutów fontSize, fontWeight lub fontFamily klasy TextStyle.

fontFamily obsługuje wszystkie czcionki systemowe, jak 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 złożonych przycisków

W Androidzie 12 wprowadzono złożone przyciski. Glance obsługuje zgodność wsteczną w przypadku tych typów złożonych przycisków:

Te złożone przyciski wyświetlają 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 { 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"
)

Gdy stan się zmieni, zostanie uruchomiony podany zasób Lambda. Stan sprawdzania możesz zapisać w ten 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) }
        )
    }
}

Aby dostosować kolory, możesz też ustawić atrybut colors na CheckBox, Switch lub 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

Wersja Glance 1.1.0 zawiera dodatkowe komponenty opisane w tabeli poniżej:

Nazwa Obraz Link referencyjny Uwagi dodatkowe
Wypełniony przycisk alt_text Składnik
Przyciski z konturem alt_text Składnik
Ikony przycisków alt_text Składnik Główna / dodatkowa / tylko ikona
Pasek tytułu alt_text Składnik
Rusztowanie Szablon i pasek tytułu znajdują się w tym samym pokazie.

Więcej informacji o szczegółach projektu znajdziesz w tym składzie projektowym w programie Figma.

Więcej informacji o kanonicznej wersji układu znajdziesz w artykule Kanoniczna wersja układu widżetu.