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 elementowiRelativeLayout.Column: umieszcza elementy jeden po drugim w osi pionowej. Odpowiada elementowiLinearLayoutz orientacją pionową.Row: umieszcza elementy jeden po drugim w osi poziomej. Odpowiada elementowiLinearLayoutz orientacją poziomą.
Glance obsługuje Scaffold obiekty. Umieść elementy Column, Row i Box w danym obiekcie Scaffold.
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.
Aby zaimplementować przewijanie z przyciąganiem, musisz spełnić te warunki:
- Zaktualizuj zależność Glance do wersji 1.3.0-alpha02 lub nowszej.
- Ustaw
compileSdkna 37 lub nowszy, ponieważ przewijanie z przyciąganiem jest obsługiwane na urządzeniach z Androidem 17 i nowszym. - Skonfiguruj
LazyColumnza pomocąVerticalScrollMode. Jeśli urządzenie obsługuje przewijanie z przyciąganiem, użyjSnapScrollMatchHeight. W przeciwnym razie użyjNormal.
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:
- wartości metadanych minimalnego i maksymalnego rozmiaru są prawidłowo zdefiniowane na podstawie rozmiaru treści,
- treść jest wystarczająco elastyczna w oczekiwanym zakresie rozmiarów.
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:
AppWidgetmusi 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 |
|
Komponent | |
| Przyciski z konturem |
|
Komponent | |
| Przyciski ikony |
|
Komponent | Szkoła podstawowa / ponadpodstawowa / tylko ikona |
| Pasek tytułu |
|
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.