Tworzenie interfejsu użytkownika za pomocą Glance

Na tej stronie dowiesz się, jak obsługiwać rozmiary i tworzyć elastyczne i responsywne układy za pomocą Glance, korzystając z istniejących komponentów Glance.

Korzystaj z usług Box, Column i Row

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

  • Box: umieszcza elementy jeden na drugim. To oznacza RelativeLayout.

  • Column: umieszcza elementy jeden po drugim na osi pionowej. Odpowiada to LinearLayout w orientacji pionowej.

  • Row: umieszcza elementy jeden po drugim na osi poziomej. Odpowiada to LinearLayout w orientacji poziomej.

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

Układ kolumn, wierszy i bloków.
Rysunek 1. Przykłady układów z kolumną, wierszem i polem.

Każda z tych funkcji kompozycyjnych umożliwia określenie wyrównania w pionie i poziomie zawartości oraz ograniczeń szerokości, wysokości, wagi lub dopełnienia za pomocą modyfikatorów. Dodatkowo każdy element podrzędny może zdefiniować swój modyfikator, aby zmienić przestrzeń i umiejscowienie wewnątrz elementu nadrzędnego.

Z poniższego przykładu dowiesz się, jak utworzyć element Row, który równomiernie rozmieszcza elementy podrzędne w poziomie, jak pokazano 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ę, równomiernie dzielą dostępną przestrzeń. Możesz definiować różne grubości, rozmiary, dopełnienia i wyrównania, aby dostosować układy do swoich potrzeb.

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

Innym sposobem na zapewnienie elastyczności treści jest umożliwienie ich przewijania. Można to zrobić za pomocą funkcji kompozycyjnej LazyColumn. Ten element kompozycyjny umożliwia zdefiniowanie zestawu elementów, które mają być wyświetlane w przewijanym kontenerze w widżecie aplikacji.

Poniższe fragmenty kodu pokazują różne sposoby definiowania elementów w pliku 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 kodu nie określa itemId. Określenie itemId pomaga zwiększyć wydajność i utrzymać pozycję przewijania podczas aktualizacji listy i appWidget od Androida 12 (np. podczas dodawania lub usuwania elementów z listy). Ten przykład pokazuje, 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 przewijanych treści do góry kontenera widżetu.

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


Aby wdrożyć przewijanie z przyciąganiem, musisz spełniać te warunki:

  • Zaktualizuj zależność Glance do wersji 1.3.0-alpha02 lub nowszej.
  • Ustaw compileSdk na 37 lub wyższą, ponieważ przewijanie z przyciąganiem jest obsługiwane na urządzeniach z Androidem 17 lub 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 z obrazami, zapoznaj się z układem kanonicznym obrazu bez obramowania.

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

Zdefiniuj 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, aby udostępniać elastyczne układy, jak opisano na stronie Udostępnianie elastycznych układów widżetów. Glance upraszcza to dzięki definicji SizeMode i wartości LocalSize. W sekcjach poniżej opisujemy te 3 tryby.

SizeMode.Single

SizeMode.Single to tryb domyślny. Oznacza to, że dostępny jest tylko 1 typ treści, czyli nawet jeśli rozmiar AppWidget się zmieni, rozmiar treści pozostanie taki sam.

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

Jeśli używasz tego trybu, upewnij się, że:

  • Minimalne i maksymalne wartości metadanych rozmiaru są prawidłowo 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 swojej zawartości po zmianie rozmiaru.

SizeMode.Responsive

Ten tryb jest odpowiednikiem udostępniania układów elastycznych, co pozwala GlanceAppWidget na zdefiniowanie zestawu układów elastycznych ograniczonych określonymi rozmiarami. W przypadku każdego zdefiniowanego rozmiaru treść jest tworzona i mapowana na konkretny rozmiar podczas tworzenia lub aktualizowania elementu AppWidget. Następnie system wybiera najlepiej dopasowany rozmiar na podstawie dostępnego miejsca.

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 mapowana na zdefiniowany rozmiar.

  • W pierwszym wywołaniu rozmiar jest oceniany jako 100x100. Treść nie zawiera dodatkowego przycisku ani tekstów u góry i u dołu.
  • W drugim wywołaniu rozmiar jest oceniany jako 250x100. Treść zawiera dodatkowy przycisk, ale nie zawiera tekstu u góry i u dołu.
  • W trzecim wywołaniu rozmiar jest określany jako 250x250. Treść obejmuje dodatkowy przycisk i oba teksty.

SizeMode.Responsive to połączenie 2 pozostałych trybów, które umożliwia definiowanie treści elastycznych w ramach wstępnie określonych granic. Ogólnie rzecz biorąc, ten tryb działa lepiej i umożliwia płynniejsze przejścia, gdy zmienia się rozmiar elementu AppWidget.

W tabeli poniżej znajdziesz wartości 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łużą tylko do celów demonstracyjnych.

SizeMode.Exact

SizeMode.Exact jest odpowiednikiem podania dokładnych układów, co powoduje żądanie treści GlanceAppWidget za każdym razem, gdy zmienia się dostępny rozmiar 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ż pozostałe, ale wiąże się z kilkoma zastrzeżeniami:

  • Za każdym razem, gdy zmienia się rozmiar, AppWidget musi zostać utworzony od nowa. Jeśli treść jest złożona, może to powodować problemy z wydajnością i przeskakiwanie interfejsu.
  • 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 Androidem w wersji starszej niż 12 logika obliczania rozmiaru może nie działać we wszystkich sytuacjach.

Ogólnie rzecz biorąc, należy używać tego trybu, jeśli nie można użyć SizeMode.Responsive (czyli nie można zastosować małego zestawu układów elastycznych).

Zasoby dotyczące dostępu

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

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

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

Komponenty i metody akceptują zasoby za pomocą „dostawcy”, takiego jak ImageProvider, lub 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",
)

Tekst uchwytu

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 pokazano w tym przykładzie, ale nie obsługuje czcionek niestandardowych w aplikacjach:

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 wielofunkcyjnych wyświetla klikalny widok, 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 funkcja lambda. Możesz zapisać stan zaznaczenia, 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) }
        )
    }
}

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

    )

Dodatkowe komponenty

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

Nazwa Obraz Link odsyłający Uwagi dodatkowe
Wypełniony przycisk alt_text Komponent
Przyciski konturowe alt_text Komponent
Przyciski ikon alt_text Komponent Główny / Dodatkowy / Tylko ikona
Pasek tytułu alt_text Komponent
Rusztowanie Rusztowanie i pasek tytułu znajdują się w tym samym demo.

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 Kanoniczne układy widżetów.