diff --git a/.github/workflows/apply_spotless.yml b/.github/workflows/apply_spotless.yml index db01dad52..5ba1f6fe5 100644 --- a/.github/workflows/apply_spotless.yml +++ b/.github/workflows/apply_spotless.yml @@ -44,10 +44,13 @@ jobs: - name: Run spotlessApply run: ./gradlew :compose:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace + - name: Run spotlessApply for Wear + run: ./gradlew :wear:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace + + - name: Run spotlessApply for Misc + run: ./gradlew :misc:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace + - name: Auto-commit if spotlessApply has changes uses: stefanzweifel/git-auto-commit-action@v5 with: - commit_message: Apply Spotless - - - name: Run spotlessApply for Wear - run: ./gradlew :wear:spotlessApply --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace + commit_message: Apply Spotless \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8337b4237..97b36f468 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,3 +45,5 @@ jobs: run: ./gradlew :kotlin:build - name: Build Wear snippets run: ./gradlew :wear:build + - name: Build misc snippets + run: ./gradlew :misc:build diff --git a/compose/snippets/src/main/AndroidManifest.xml b/compose/snippets/src/main/AndroidManifest.xml index c9b15e6c5..4ec4d9b70 100644 --- a/compose/snippets/src/main/AndroidManifest.xml +++ b/compose/snippets/src/main/AndroidManifest.xml @@ -27,6 +27,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" + android:enableOnBackInvokedCallback="true" android:theme="@style/Theme.Snippets"> ApplyPolygonAsClipImage() Destination.SharedElementExamples -> PlaceholderSizeAnimated_Demo() + Destination.PagerExamples -> PagerExamples() } } } @@ -109,6 +118,13 @@ class SnippetsActivity : ComponentActivity() { TopComponentsDestination.PartialBottomSheet -> PartialBottomSheet() TopComponentsDestination.TimePickerExamples -> TimePickerExamples() TopComponentsDestination.DatePickerExamples -> DatePickerExamples() + TopComponentsDestination.CarouselExamples -> CarouselExamples() + TopComponentsDestination.MenusExample -> MenusExamples() + TopComponentsDestination.TooltipExamples -> TooltipExamples() + TopComponentsDestination.NavigationDrawerExamples -> NavigationDrawerExamples() + TopComponentsDestination.SegmentedButtonExamples -> SegmentedButtonExamples() + TopComponentsDestination.SwipeToDismissBoxExamples -> SwipeToDismissBoxExamples() + TopComponentsDestination.SearchBarExamples -> SearchBarExamples() } } } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt index 38a1eb44d..2fe06cf06 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt @@ -26,9 +26,11 @@ import androidx.compose.animation.EnterExitState import androidx.compose.animation.SizeTransform import androidx.compose.animation.animateColor import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.AnimationVector2D import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.ExperimentalAnimationSpecApi import androidx.compose.animation.core.ExperimentalTransitionApi import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutSlowInEasing @@ -43,11 +45,13 @@ import androidx.compose.animation.core.TwoWayConverter import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.animateOffsetAsState import androidx.compose.animation.core.animateRect import androidx.compose.animation.core.animateValueAsState import androidx.compose.animation.core.createChildTransition import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.keyframesWithSpline import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.rememberTransition import androidx.compose.animation.core.repeatable @@ -71,11 +75,13 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn @@ -93,25 +99,34 @@ import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.PointMode import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.boundsInParent +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.round import com.example.compose.snippets.R import java.text.BreakIterator import java.text.StringCharacterIterator import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive /* * Copyright 2023 The Android Open Source Project @@ -709,6 +724,101 @@ private fun AnimationSpecKeyframe() { // [END android_compose_animations_spec_keyframe] } +@OptIn(ExperimentalAnimationSpecApi::class) +@Composable +private fun AnimationSpecKeyframeWithSpline() { + // [START android_compose_animation_spec_keyframes_with_spline] + val offset by animateOffsetAsState( + targetValue = Offset(300f, 300f), + animationSpec = keyframesWithSpline { + durationMillis = 6000 + Offset(0f, 0f) at 0 + Offset(150f, 200f) atFraction 0.5f + Offset(0f, 100f) atFraction 0.7f + } + ) + // [END android_compose_animation_spec_keyframes_with_spline] +} + +@OptIn(ExperimentalAnimationSpecApi::class) +@Preview +@Composable +private fun OffsetKeyframeWithSplineDemo() { + val points = remember { mutableStateListOf() } + val offsetAnim = remember { + Animatable( + Offset.Zero, + Offset.VectorConverter + ) + } + val density = LocalDensity.current + + BoxWithConstraints( + Modifier.fillMaxSize().drawBehind { + drawPoints( + points = points, + pointMode = PointMode.Lines, + color = Color.LightGray, + strokeWidth = 4f, + pathEffect = PathEffect.dashPathEffect(floatArrayOf(30f, 20f)) + ) + } + ) { + val minDimension = minOf(maxWidth, maxHeight) + val size = minDimension / 4 + + val sizePx = with(density) { size.toPx() } + val widthPx = with(density) { maxWidth.toPx() } + val heightPx = with(density) { maxHeight.toPx() } + + val maxXOff = (widthPx - sizePx) / 2f + val maxYOff = heightPx - (sizePx / 2f) + + Box( + Modifier.align(Alignment.TopCenter) + .offset { offsetAnim.value.round() } + .size(size) + .background(Color.Red, RoundedCornerShape(50)) + .onPlaced { points.add(it.boundsInParent().center) } + ) + + LaunchedEffect(Unit) { + delay(1000) + while (isActive) { + offsetAnim.animateTo( + targetValue = Offset.Zero, + animationSpec = + keyframesWithSpline { + durationMillis = 4400 + + // Increasingly approach the halfway point moving from side to side + for (i in 0..4) { + val sign = if (i % 2 == 0) 1 else -1 + Offset( + x = maxXOff * (i.toFloat() / 5f) * sign, + y = (maxYOff) * (i.toFloat() / 5f) + ) atFraction (0.1f * i) + } + + // Halfway point (at bottom of the screen) + Offset(0f, maxYOff) atFraction 0.5f + + // Return with mirrored movement + for (i in 0..4) { + val sign = if (i % 2 == 0) 1 else -1 + Offset( + x = maxXOff * (1f - i.toFloat() / 5f) * sign, + y = (maxYOff) * (1f - i.toFloat() / 5f) + ) atFraction ((0.1f * i) + 0.5f) + } + } + ) + points.clear() + } + } + } +} + @Composable private fun AnimationSpecRepeatable() { // [START android_compose_animations_spec_repeatable] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/Cheese.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/Cheese.kt new file mode 100644 index 000000000..2468bce9f --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/Cheese.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo + +import androidx.annotation.DrawableRes +import com.example.compose.snippets.R + +val CheeseImages = listOf( + R.drawable.cheese_1, + R.drawable.cheese_2, + R.drawable.cheese_3, + R.drawable.cheese_4, + R.drawable.cheese_5 +) + +val CheeseNames = listOf( + "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi", + "Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale", + "Aisy Cendre", "Allgauer Emmentaler", "Alverca", "Ambert", "American Cheese", + "Ami du Chambertin", "Anejo Enchilado", "Anneau du Vic-Bilh", "Anthoriro", "Appenzell", + "Aragon", "Ardi Gasna", "Ardrahan", "Armenian String", "Aromes au Gene de Marc", + "Asadero", "Asiago", "Aubisque Pyrenees", "Autun", "Avaxtskyr", "Baby Swiss", + "Babybel", "Baguette Laonnaise", "Bakers", "Baladi", "Balaton", "Bandal", "Banon", + "Barry's Bay Cheddar", "Basing", "Basket Cheese", "Bath Cheese", "Bavarian Bergkase", + "Baylough", "Beaufort", "Beauvoorde", "Beenleigh Blue", "Beer Cheese", "Bel Paese", + "Bergader", "Bergere Bleue", "Berkswell", "Beyaz Peynir", "Bierkase", "Bishop Kennedy", + "Blarney", "Bleu d'Auvergne", "Bleu de Gex", "Bleu de Laqueuille", + "Bleu de Septmoncel", "Bleu Des Causses", "Blue", "Blue Castello", "Blue Rathgore", + "Blue Vein (Australian)", "Blue Vein Cheeses", "Bocconcini", "Bocconcini (Australian)", + "Boeren Leidenkaas", "Bonchester", "Bosworth", "Bougon", "Boule Du Roves", + "Boulette d'Avesnes", "Boursault", "Boursin", "Bouyssou", "Bra", "Braudostur", + "Breakfast Cheese", "Brebis du Lavort", "Brebis du Lochois", "Brebis du Puyfaucon", + "Bresse Bleu", "Brick", "Brie", "Brie de Meaux", "Brie de Melun", "Brillat-Savarin", + "Brin", "Brin d' Amour", "Brin d'Amour", "Brinza (Burduf Brinza)", + "Briquette de Brebis", "Briquette du Forez", "Broccio", "Broccio Demi-Affine", + "Brousse du Rove", "Bruder Basil", "Brusselae Kaas (Fromage de Bruxelles)", "Bryndza", + "Buchette d'Anjou", "Buffalo", "Burgos", "Butte", "Butterkase", "Button (Innes)", + "Buxton Blue", "Cabecou", "Caboc", "Cabrales", "Cachaille", "Caciocavallo", "Caciotta", + "Caerphilly", "Cairnsmore", "Calenzana", "Cambazola", "Camembert de Normandie", + "Canadian Cheddar", "Canestrato", "Cantal", "Caprice des Dieux", "Capricorn Goat", + "Capriole Banon", "Carre de l'Est", "Casciotta di Urbino", "Cashel Blue", "Castellano", + "Castelleno", "Castelmagno", "Castelo Branco", "Castigliano", "Cathelain", + "Celtic Promise", "Cendre d'Olivet", "Cerney", "Chabichou", "Chabichou du Poitou", + "Chabis de Gatine", "Chaource", "Charolais", "Chaumes", "Cheddar", + "Cheddar Clothbound", "Cheshire", "Chevres", "Chevrotin des Aravis", "Chontaleno", + "Civray", "Coeur de Camembert au Calvados", "Coeur de Chevre", "Colby", "Cold Pack", + "Comte", "Coolea", "Cooleney", "Coquetdale", "Corleggy", "Cornish Pepper", + "Cotherstone", "Cotija", "Cottage Cheese", "Cottage Cheese (Australian)", + "Cougar Gold", "Coulommiers", "Coverdale", "Crayeux de Roncq", "Cream Cheese", + "Cream Havarti", "Crema Agria", "Crema Mexicana", "Creme Fraiche", "Crescenza", + "Croghan", "Crottin de Chavignol", "Crottin du Chavignol", "Crowdie", "Crowley", + "Cuajada", "Curd", "Cure Nantais", "Curworthy", "Cwmtawe Pecorino", + "Cypress Grove Chevre", "Danablu (Danish Blue)", "Danbo", "Danish Fontina", + "Daralagjazsky", "Dauphin", "Delice des Fiouves", "Denhany Dorset Drum", "Derby", + "Dessertnyj Belyj", "Devon Blue", "Devon Garland", "Dolcelatte", "Doolin", + "Doppelrhamstufel", "Dorset Blue Vinney", "Double Gloucester", "Double Worcester", + "Dreux a la Feuille", "Dry Jack", "Duddleswell", "Dunbarra", "Dunlop", "Dunsyre Blue", + "Duroblando", "Durrus", "Dutch Mimolette (Commissiekaas)", "Edam", "Edelpilz", + "Emental Grand Cru", "Emlett", "Emmental", "Epoisses de Bourgogne", "Esbareich", + "Esrom", "Etorki", "Evansdale Farmhouse Brie", "Evora De L'Alentejo", "Exmoor Blue", + "Explorateur", "Feta", "Feta (Australian)", "Figue", "Filetta", "Fin-de-Siecle", + "Finlandia Swiss", "Finn", "Fiore Sardo", "Fleur du Maquis", "Flor de Guia", + "Flower Marie", "Folded", "Folded cheese with mint", "Fondant de Brebis", + "Fontainebleau", "Fontal", "Fontina Val d'Aosta", "Formaggio di capra", "Fougerus", + "Four Herb Gouda", "Fourme d' Ambert", "Fourme de Haute Loire", "Fourme de Montbrison", + "Fresh Jack", "Fresh Mozzarella", "Fresh Ricotta", "Fresh Truffles", "Fribourgeois", + "Friesekaas", "Friesian", "Friesla", "Frinault", "Fromage a Raclette", "Fromage Corse", + "Fromage de Montagne de Savoie", "Fromage Frais", "Fruit Cream Cheese", + "Frying Cheese", "Fynbo", "Gabriel", "Galette du Paludier", "Galette Lyonnaise", + "Galloway Goat's Milk Gems", "Gammelost", "Gaperon a l'Ail", "Garrotxa", "Gastanberra", + "Geitost", "Gippsland Blue", "Gjetost", "Gloucester", "Golden Cross", "Gorgonzola", + "Gornyaltajski", "Gospel Green", "Gouda", "Goutu", "Gowrie", "Grabetto", "Graddost", + "Grafton Village Cheddar", "Grana", "Grana Padano", "Grand Vatel", + "Grataron d' Areches", "Gratte-Paille", "Graviera", "Greuilh", "Greve", + "Gris de Lille", "Gruyere", "Gubbeen", "Guerbigny", "Halloumi", + "Halloumy (Australian)", "Haloumi-Style Cheese", "Harbourne Blue", "Havarti", + "Heidi Gruyere", "Hereford Hop", "Herrgardsost", "Herriot Farmhouse", "Herve", + "Hipi Iti", "Hubbardston Blue Cow", "Hushallsost", "Iberico", "Idaho Goatster", + "Idiazabal", "Il Boschetto al Tartufo", "Ile d'Yeu", "Isle of Mull", "Jarlsberg", + "Jermi Tortes", "Jibneh Arabieh", "Jindi Brie", "Jubilee Blue", "Juustoleipa", + "Kadchgall", "Kaseri", "Kashta", "Kefalotyri", "Kenafa", "Kernhem", "Kervella Affine", + "Kikorangi", "King Island Cape Wickham Brie", "King River Gold", "Klosterkaese", + "Knockalara", "Kugelkase", "L'Aveyronnais", "L'Ecir de l'Aubrac", "La Taupiniere", + "La Vache Qui Rit", "Laguiole", "Lairobell", "Lajta", "Lanark Blue", "Lancashire", + "Langres", "Lappi", "Laruns", "Lavistown", "Le Brin", "Le Fium Orbo", "Le Lacandou", + "Le Roule", "Leafield", "Lebbene", "Leerdammer", "Leicester", "Leyden", "Limburger", + "Lincolnshire Poacher", "Lingot Saint Bousquet d'Orb", "Liptauer", "Little Rydings", + "Livarot", "Llanboidy", "Llanglofan Farmhouse", "Loch Arthur Farmhouse", + "Loddiswell Avondale", "Longhorn", "Lou Palou", "Lou Pevre", "Lyonnais", "Maasdam", + "Macconais", "Mahoe Aged Gouda", "Mahon", "Malvern", "Mamirolle", "Manchego", + "Manouri", "Manur", "Marble Cheddar", "Marbled Cheeses", "Maredsous", "Margotin", + "Maribo", "Maroilles", "Mascares", "Mascarpone", "Mascarpone (Australian)", + "Mascarpone Torta", "Matocq", "Maytag Blue", "Meira", "Menallack Farmhouse", + "Menonita", "Meredith Blue", "Mesost", "Metton (Cancoillotte)", "Meyer Vintage Gouda", + "Mihalic Peynir", "Milleens", "Mimolette", "Mine-Gabhar", "Mini Baby Bells", "Mixte", + "Molbo", "Monastery Cheeses", "Mondseer", "Mont D'or Lyonnais", "Montasio", + "Monterey Jack", "Monterey Jack Dry", "Morbier", "Morbier Cru de Montagne", + "Mothais a la Feuille", "Mozzarella", "Mozzarella (Australian)", + "Mozzarella di Bufala", "Mozzarella Fresh, in water", "Mozzarella Rolls", "Munster", + "Murol", "Mycella", "Myzithra", "Naboulsi", "Nantais", "Neufchatel", + "Neufchatel (Australian)", "Niolo", "Nokkelost", "Northumberland", "Oaxaca", + "Olde York", "Olivet au Foin", "Olivet Bleu", "Olivet Cendre", + "Orkney Extra Mature Cheddar", "Orla", "Oschtjepka", "Ossau Fermier", "Ossau-Iraty", + "Oszczypek", "Oxford Blue", "P'tit Berrichon", "Palet de Babligny", "Paneer", "Panela", + "Pannerone", "Pant ys Gawn", "Parmesan (Parmigiano)", "Parmigiano Reggiano", + "Pas de l'Escalette", "Passendale", "Pasteurized Processed", "Pate de Fromage", + "Patefine Fort", "Pave d'Affinois", "Pave d'Auge", "Pave de Chirac", "Pave du Berry", + "Pecorino", "Pecorino in Walnut Leaves", "Pecorino Romano", "Peekskill Pyramid", + "Pelardon des Cevennes", "Pelardon des Corbieres", "Penamellera", "Penbryn", + "Pencarreg", "Perail de Brebis", "Petit Morin", "Petit Pardou", "Petit-Suisse", + "Picodon de Chevre", "Picos de Europa", "Piora", "Pithtviers au Foin", + "Plateau de Herve", "Plymouth Cheese", "Podhalanski", "Poivre d'Ane", "Polkolbin", + "Pont l'Eveque", "Port Nicholson", "Port-Salut", "Postel", "Pouligny-Saint-Pierre", + "Pourly", "Prastost", "Pressato", "Prince-Jean", "Processed Cheddar", "Provolone", + "Provolone (Australian)", "Pyengana Cheddar", "Pyramide", "Quark", + "Quark (Australian)", "Quartirolo Lombardo", "Quatre-Vents", "Quercy Petit", + "Queso Blanco", "Queso Blanco con Frutas --Pina y Mango", "Queso de Murcia", + "Queso del Montsec", "Queso del Tietar", "Queso Fresco", "Queso Fresco (Adobera)", + "Queso Iberico", "Queso Jalapeno", "Queso Majorero", "Queso Media Luna", + "Queso Para Frier", "Queso Quesadilla", "Rabacal", "Raclette", "Ragusano", "Raschera", + "Reblochon", "Red Leicester", "Regal de la Dombes", "Reggianito", "Remedou", + "Requeson", "Richelieu", "Ricotta", "Ricotta (Australian)", "Ricotta Salata", "Ridder", + "Rigotte", "Rocamadour", "Rollot", "Romano", "Romans Part Dieu", "Roncal", "Roquefort", + "Roule", "Rouleau De Beaulieu", "Royalp Tilsit", "Rubens", "Rustinu", "Saaland Pfarr", + "Saanenkaese", "Saga", "Sage Derby", "Sainte Maure", "Saint-Marcellin", + "Saint-Nectaire", "Saint-Paulin", "Salers", "Samso", "San Simon", "Sancerre", + "Sap Sago", "Sardo", "Sardo Egyptian", "Sbrinz", "Scamorza", "Schabzieger", "Schloss", + "Selles sur Cher", "Selva", "Serat", "Seriously Strong Cheddar", "Serra da Estrela", + "Sharpam", "Shelburne Cheddar", "Shropshire Blue", "Siraz", "Sirene", "Smoked Gouda", + "Somerset Brie", "Sonoma Jack", "Sottocenare al Tartufo", "Soumaintrain", + "Sourire Lozerien", "Spenwood", "Sraffordshire Organic", "St. Agur Blue Cheese", + "Stilton", "Stinking Bishop", "String", "Sussex Slipcote", "Sveciaost", "Swaledale", + "Sweet Style Swiss", "Swiss", "Syrian (Armenian String)", "Tala", "Taleggio", "Tamie", + "Tasmania Highland Chevre Log", "Taupiniere", "Teifi", "Telemea", "Testouri", + "Tete de Moine", "Tetilla", "Texas Goat Cheese", "Tibet", "Tillamook Cheddar", + "Tilsit", "Timboon Brie", "Toma", "Tomme Brulee", "Tomme d'Abondance", + "Tomme de Chevre", "Tomme de Romans", "Tomme de Savoie", "Tomme des Chouans", "Tommes", + "Torta del Casar", "Toscanello", "Touree de L'Aubier", "Tourmalet", + "Trappe (Veritable)", "Trois Cornes De Vendee", "Tronchon", "Trou du Cru", "Truffe", + "Tupi", "Turunmaa", "Tymsboro", "Tyn Grug", "Tyning", "Ubriaco", "Ulloa", + "Vacherin-Fribourgeois", "Valencay", "Vasterbottenost", "Venaco", "Vendomois", + "Vieux Corse", "Vignotte", "Vulscombe", "Waimata Farmhouse Blue", + "Washed Rind Cheese (Australian)", "Waterloo", "Weichkaese", "Wellington", + "Wensleydale", "White Stilton", "Whitestone Farmhouse", "Wigmore", "Woodside Cabecou", + "Xanadu", "Xynotyro", "Yarg Cornish", "Yarra Valley Pyramid", "Yorkshire Blue", + "Zamorano", "Zanetti Grana Padano", "Zanetti Parmigiano Reggiano" +) + +data class Cheese( + val id: Long, + val name: String, + @DrawableRes val image: Int +) { + companion object { + fun all(): List { + return CheeseNames.mapIndexed { i, name -> + Cheese( + id = (i + 1).toLong(), + name = name, + image = CheeseImages[ + ((name.hashCode() % CheeseImages.size) + CheeseImages.size) % + CheeseImages.size + ] + ) + } + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/SimpleScaffold.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/SimpleScaffold.kt new file mode 100644 index 000000000..987d545a3 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/SimpleScaffold.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SimpleScaffold( + title: String, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + title = { + Text(text = title) + }, + modifier = Modifier + .statusBarsPadding() + ) + }, + modifier = modifier + ) { padding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + content() + } + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/custom/CustomCanvasAnimation.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/custom/CustomCanvasAnimation.kt new file mode 100644 index 000000000..e82794346 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/custom/CustomCanvasAnimation.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo.custom + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.core.spring +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay + +/** + * A custom loading animation example using Canvas and draw APIs, combined with + * Animatable to show the use of the animateTo() function used sequentially. + */ +@Composable +@Preview +fun CustomCanvasBouncyLoader() { + val yOffset = remember { + Animatable(0f) + } + val scale = remember { + Animatable(1f) + } + LaunchedEffect("bouncyLoader") { + delay(400) + // We use the Animatable.animateTo() API here to demonstrate the coroutine usage - each + // item is animating one after the other, as the animateTo function is sequential. + // Animate y to half the height + yOffset.animateTo(0.5f, bouncyAnimationSpec) + scale.animateTo(3f, bouncyAnimationSpec) + delay(500) + scale.animateTo(10f, bouncyAnimationSpec) + delay(500) + scale.animateTo(50f, bouncyAnimationSpec) + } + val size = remember { + mutableStateOf(IntSize.Zero) + } + Box( + Modifier + .fillMaxSize() + .onSizeChanged { + // We get the size change of the whole composable, and use this to determine how + // big the ball should be. + size.value = it + } + ) { + GradientCircle( + Modifier + .align(Alignment.TopCenter) + .size(26.dp) + .graphicsLayer { + // We use .graphicsLayer here to perform the animation as we are animating + // multiple properties of our Gradient circle at once, and this is more + // efficient than using multiple modifiers. + // .graphicsLayer also defers these changes to the Draw phase of Compose, + // therefore minimizing recompositions required to do this. + scaleX = scale.value + scaleY = scale.value + translationY = yOffset.value * size.value.height + } + ) + } +} + +@Composable +private fun GradientCircle(modifier: Modifier = Modifier) { + val brush = remember { + Brush.verticalGradient(listOf(Color(0xFFF56E34), Color(0xFF234EDA))) + } + Canvas(modifier = modifier) { + drawCircle(brush = brush) + } +} + +private val bouncyAnimationSpec: SpringSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow +) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/fade/FadeDemo.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/fade/FadeDemo.kt new file mode 100644 index 000000000..c46735f25 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/fade/FadeDemo.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo.fade + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.animations.demo.CheeseImages +import com.example.compose.snippets.animations.demo.SimpleScaffold + +/** + * A fade creates a smooth sequence between elements that fully overlap each other, such as + * photos inside of a card or another container. When a new element enters, it fades in + * over the current element. + */ +@Preview +@Composable +fun FadeDemo() { + SimpleScaffold(title = "Fade") { + + val painters = CheeseImages.map { painterResource(it) } + var index by remember { mutableIntStateOf(0) } + + AnimatedContent( + targetState = index, + modifier = Modifier.align(Alignment.Center), + transitionSpec = fade() + ) { targetIndex -> + Image( + painter = painters[targetIndex], + contentDescription = "Cheese", + modifier = Modifier + .size(256.dp, 192.dp) + .clip(shape = RoundedCornerShape(16.dp)), + contentScale = ContentScale.Crop + ) + } + + Button( + onClick = { index = (index + 1) % painters.size }, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(64.dp) + ) { + Text(text = "NEXT") + } + } +} + +/** + * Creates a transitionSpec for configuring [AnimatedContent] to the fade pattern. + */ +private fun fade( + durationMillis: Int = 300 +): AnimatedContentTransitionScope.() -> ContentTransform { + return { + ContentTransform( + // The initial content should stay until the target content is completely opaque. + initialContentExit = fadeOut(animationSpec = snap(delayMillis = durationMillis)), + // The target content fades in. This is shown on top of the initial content. + targetContentEnter = fadeIn( + animationSpec = tween( + durationMillis = durationMillis, + // LinearOutSlowInEasing is suitable for incoming elements. + easing = LinearOutSlowInEasing + ) + ) + ) + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/fadethrough/FadeThroughDemo.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/fadethrough/FadeThroughDemo.kt new file mode 100644 index 000000000..efdd35745 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/fadethrough/FadeThroughDemo.kt @@ -0,0 +1,256 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo.fadethrough + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R +import com.example.compose.snippets.animations.demo.SimpleScaffold + +/** + * Fade through involves one element fading out completely before a new one fades in. These + * transitions can be applied to text, icons, and other elements that don't perfectly + * overlap. This technique lets the background show through during a transition, and it can + * provide continuity between screens when paired with a shared transformation. + */ +@Preview +@Composable +fun FadeThroughDemo() { + SimpleScaffold(title = "Fade through") { + var expanded by rememberSaveable { mutableStateOf(false) } + DemoCard( + expanded = expanded, + modifier = Modifier.align(Alignment.Center) + ) + + Button( + onClick = { expanded = !expanded }, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(64.dp) + ) { + Text(text = "TOGGLE") + } + } +} + +/** + * Shows the card. The card can be either expanded or collapsed, and the transition between the two + * states is animated with the fade-through effect. + * + * @param expanded Whether the card is expanded or collapsed. + */ +@Composable +private fun DemoCard( + expanded: Boolean, + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier, + tonalElevation = 2.dp, + shape = RoundedCornerShape(16.dp) + ) { + // Use `AnimatedContent` to switch between different content. + AnimatedContent( + // `targetState` specifies the input state. + targetState = expanded, + // `transitionSpec` defines the behavior of the transition animation. + transitionSpec = fadeThrough() + ) { targetExpanded -> + if (targetExpanded) { + ExpandedContent() + } else { + CollapsedContent() + } + } + } +} + +/** + * Creates a transitionSpec for configuring [AnimatedContent] to the fade through pattern. + * See [Fade through](https://material.io/design/motion/the-motion-system.html#fade-through) for + * the motion spec. + */ +fun fadeThrough( + durationMillis: Int = 300 +): AnimatedContentTransitionScope.() -> ContentTransform { + return { + ContentTransform( + // The initial content fades out. + initialContentExit = fadeOut( + animationSpec = tween( + // The duration is 3/8 of the overall duration. + durationMillis = durationMillis * 3 / 8, + // FastOutLinearInEasing is suitable for elements exiting the screen. + easing = FastOutLinearInEasing + ) + ), + // The target content fades in after the current content fades out. + targetContentEnter = fadeIn( + animationSpec = tween( + // The duration is 5/8 of the overall duration. + durationMillis = durationMillis * 5 / 8, + // Delays the EnterTransition by the duration of the ExitTransition. + delayMillis = durationMillis * 3 / 8, + // LinearOutSlowInEasing is suitable for incoming elements. + easing = LinearOutSlowInEasing + ) + ), + // The size changes along with the content transition. + sizeTransform = SizeTransform( + sizeAnimationSpec = { _, _ -> + tween(durationMillis = durationMillis) + } + ) + ) + } +} + +@Composable +private fun CollapsedContent() { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Image( + painter = painterResource(R.drawable.cheese_1), + contentDescription = null, + modifier = Modifier + .size(64.dp) + .clip(CircleShape) + ) + Text( + text = "Cheese", + style = MaterialTheme.typography.headlineSmall + ) + } +} + +@Composable +private fun ExpandedContent() { + Column( + modifier = Modifier.width(IntrinsicSize.Min) + ) { + Image( + painter = painterResource(R.drawable.cheese_1), + contentDescription = null, + modifier = Modifier.size(320.dp, 128.dp), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Cheese", + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Text( + text = "Hello, world", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.lorem_ipsum), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(horizontal = 16.dp), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + TextButton(onClick = { /* Do nothing */ }) { + Text(text = "DETAIL") + } + TextButton(onClick = { /* Do nothing */ }) { + Text(text = "ORDER") + } + Spacer(modifier = Modifier.weight(1f)) + IconButton(onClick = { /* Do nothing */ }) { + Icon( + imageVector = Icons.Default.Favorite, + contentDescription = "Favorite" + ) + } + IconButton(onClick = { /* Do nothing */ }) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = "Share" + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + } +} + +@Preview +@Composable +private fun PreviewDemoCardCollapsed() { + DemoCard(expanded = false) +} + +@Preview +@Composable +private fun PreviewDemoCardExpanded() { + DemoCard(expanded = true) +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/sharedaxis/SharedAxisDemo.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/sharedaxis/SharedAxisDemo.kt new file mode 100644 index 000000000..ceb7b4a61 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/sharedaxis/SharedAxisDemo.kt @@ -0,0 +1,282 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo.sharedaxis + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.compose.snippets.animations.demo.CheeseImages +import com.example.compose.snippets.animations.demo.CheeseNames +import com.example.compose.snippets.animations.demo.SimpleScaffold + +/** + * The shared axis pattern is used for transitions between UI elements that have a spatial + * or navigational relationship. This demo uses a shared transformation on the Y-axis to + * reinforce the sequential order of elements. + */ +@Composable +fun SharedAxisDemo() { + SimpleScaffold(title = "Layout > Shared axis (Y-axis)") { + val pages = remember { createPages() } + // Indicator column + var id by rememberSaveable { mutableIntStateOf(1) } + Row(modifier = Modifier.padding(end = 16.dp)) { + PageIndicatorsColumn( + pages = pages, + selectedId = id, + onIndicatorClick = { id = it } + ) + + // SharedYAxis animates the content change. + SharedYAxis(targetState = pages.first { it.id == id }) { page -> + PageContent(page = page) + } + } + } +} + +/** + * Animates content change with the vertical shared axis pattern. + * + * See [Shared axis](https://material.io/design/motion/the-motion-system.html#shared-axis) for the + * detail about this motion pattern. + */ +@Composable +private fun > SharedYAxis( + targetState: T, + modifier: Modifier = Modifier, + content: @Composable AnimatedVisibilityScope.(T) -> Unit +) { + val exitDurationMillis = 80 + val enterDurationMillis = 220 + + // This local function creates the AnimationSpec for outgoing elements. + fun exitSpec(): FiniteAnimationSpec = + tween( + durationMillis = exitDurationMillis, + easing = FastOutLinearInEasing + ) + + // This local function creates the AnimationSpec for incoming elements. + fun enterSpec(): FiniteAnimationSpec = + tween( + // The enter animation runs right after the exit animation. + delayMillis = exitDurationMillis, + durationMillis = enterDurationMillis, + easing = LinearOutSlowInEasing + ) + + val slideDistance = with(LocalDensity.current) { 30.dp.roundToPx() } + + AnimatedContent( + targetState = targetState, + transitionSpec = { + // The state type () is Comparable. + // We compare the initial state and the target state to determine whether we are moving + // down or up. + if (initialState < targetState) { // Move down + ContentTransform( + // Outgoing elements fade out and slide up to the top. + initialContentExit = fadeOut(exitSpec()) + + slideOutVertically(exitSpec()) { -slideDistance }, + // Incoming elements fade in and slide up from the bottom. + targetContentEnter = fadeIn(enterSpec()) + + slideInVertically(enterSpec()) { slideDistance } + ) + } else { // Move up + ContentTransform( + // Outgoing elements fade out and slide down to the bottom. + initialContentExit = fadeOut(exitSpec()) + + slideOutVertically(exitSpec()) { slideDistance }, + // Outgoing elements fade in and slide down from the top. + targetContentEnter = fadeIn(enterSpec()) + + slideInVertically(enterSpec()) { -slideDistance } + ) + } + }, + modifier = modifier, + content = content + ) +} + +private class Page( + val id: Int, + @DrawableRes + val image: Int, + val title: String, + val body: String +) : Comparable { + + override fun compareTo(other: Page): Int { + return id.compareTo(other.id) + } +} + +private fun createPages(): List { + val body = LoremIpsum().values.joinToString(separator = " ").replace('\n', ' ') + return (0..4).map { i -> + Page( + id = i + 1, + image = CheeseImages[i % CheeseImages.size], + title = CheeseNames[i * 128], + body = body + ) + } +} + +@Composable +private fun PageIndicatorsColumn( + pages: List, + selectedId: Int, + onIndicatorClick: (index: Int) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp), + ) { + for (page in pages) { + PageIndicator( + index = page.id, + selected = selectedId == page.id, + onClick = { onIndicatorClick(page.id) } + ) + } + } +} + +@Composable +private fun PageIndicator( + index: Int, + selected: Boolean, + onClick: () -> Unit +) { + val transition = updateTransition(targetState = selected, label = "indicator") + val backgroundColor by transition.animateColor(label = "background color") { targetSelected -> + if (targetSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.surfaceVariant + } + } + val textColor by transition.animateColor(label = "text color") { targetSelected -> + if (targetSelected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + } + IconButton(onClick = onClick) { + Text( + text = index.toString(), + modifier = Modifier + .size(32.dp) + .background(backgroundColor, CircleShape) + .wrapContentSize(), + color = textColor, + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + } +} + +@Composable +private fun PageContent( + page: Page, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .navigationBarsPadding(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Image( + painter = painterResource(page.image), + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(16f / 9f) + .clip(RoundedCornerShape(16.dp)), + contentScale = ContentScale.Crop + ) + Text( + text = page.title, + style = MaterialTheme.typography.titleLarge + ) + Text( + text = page.body, + textAlign = TextAlign.Justify + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewSharedAxisDemo() { + SharedAxisDemo() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/sharedtransform/SharedTransformDemo.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/sharedtransform/SharedTransformDemo.kt new file mode 100644 index 000000000..3e913c782 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/demo/sharedtransform/SharedTransformDemo.kt @@ -0,0 +1,261 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.animations.demo.sharedtransform + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import com.example.compose.snippets.R +import com.example.compose.snippets.animations.demo.SimpleScaffold +import com.example.compose.snippets.animations.demo.fadethrough.fadeThrough +import com.example.compose.snippets.ui.theme.SnippetsTheme + +/** + * Complex layout changes use a shared transformation to create smooth transitions from + * one layout to the next. Elements are grouped together and transform as a single unit, + * rather than animating independently. This avoids multiple transformations overlapping + * and competing for attention. + */ +@Composable +fun SharedTransformDemo() { + SimpleScaffold(title = "Shared transform") { + DemoCard( + modifier = Modifier + .align(Alignment.Center) + .padding(horizontal = 32.dp) + .widthIn(max = 384.dp) + .fillMaxWidth() + ) + } +} + +@Composable +private fun DemoCard( + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier, + tonalElevation = 2.dp, + shape = RoundedCornerShape(16.dp), + ) { + // The content of this card is laid out by this ConstraintLayout. + ConstraintLayout { + // The card is either expanded or collapsed. + var expanded by rememberSaveable { mutableStateOf(false) } + + // The ConstraintLayout has 4 constrained elements. They animate separately during the + // animation, except for the icon that is shared in both the expanded and the + // collapsed states. + val (content, icon, divider, button) = createRefs() + + // This transition object coordinates different kinds of animations. + val transition = updateTransition(targetState = expanded, label = "card") + + // This is the main content of the card. + // By using the AnimatedContent composable as an extension function of the transition + // object, the animation runs in sync with other animations of the transition. + // The height of this element animates on the state change (SizeTransform), and the + // ConstraintLayout can lay out its children based on the constraints continuously + // during the animation. + transition.AnimatedContent( + // We use the fade-through effect for elements that change between the states. + transitionSpec = fadeThrough(), + modifier = Modifier.constrainAs(content) { + top.linkTo(parent.top, margin = 16.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + ) { targetExpanded -> + CardContent(expanded = targetExpanded) + } + + // The icon is shared between the expanded and collapsed states. + CardIcon( + modifier = Modifier.constrainAs(icon) { + top.linkTo(parent.top, margin = 16.dp) + end.linkTo(parent.end, margin = 16.dp) + } + ) + + // The divider becomes transparent in the collapsed state. + val dividerColor by transition.animateColor(label = "divider color") { targetExpanded -> + if (targetExpanded) { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) + } else { + Color.Transparent + } + } + HorizontalDivider( + modifier = Modifier.constrainAs(divider) { + top.linkTo(content.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + color = dividerColor + ) + + // The expand/collapse button is shared between the expanded and collapsed states. + TextButton( + onClick = { expanded = !expanded }, + modifier = Modifier.constrainAs(button) { + top.linkTo(divider.bottom, margin = 8.dp) + start.linkTo(parent.start, margin = 8.dp) + // The button is constrained to the bottom of the parent so that it remains + // visible during the animations. + bottom.linkTo(parent.bottom, margin = 8.dp) + } + ) { + // The AnimatedContent extension function can be used for any descendant elements, + // not just direct children. + transition.AnimatedContent(transitionSpec = fadeThrough()) { targetExpanded -> + Text(text = if (targetExpanded) "COLLAPSE" else "EXPAND") + } + } + } + } +} + +private val CheeseImages = listOf( + R.drawable.cheese_1 to "Cheese 1", + R.drawable.cheese_2 to "Cheese 2", + R.drawable.cheese_3 to "Cheese 3", + R.drawable.cheese_4 to "Cheese 4", + R.drawable.cheese_5 to "Cheese 5" +) + +@Composable +private fun CardContent(expanded: Boolean, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + if (expanded) { + ContentTitle(modifier = Modifier.padding(horizontal = 16.dp)) + ContentMaker( + modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 16.dp) + ) + ContentImagesRow(images = CheeseImages.subList(0, 2)) + Spacer(modifier = Modifier.height(1.dp)) + ContentImagesRow(images = CheeseImages.subList(2, 5)) + ContentBody(maxLines = 2, modifier = Modifier.padding(16.dp)) + } else { + ContentMaker(modifier = Modifier.padding(horizontal = 16.dp)) + ContentTitle(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) + ContentBody(maxLines = 1, Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)) + ContentImagesRow(images = CheeseImages) + } + } +} + +@Composable +private fun ContentTitle(modifier: Modifier = Modifier) { + Text( + text = "Cheeses", + modifier = modifier, + style = MaterialTheme.typography.titleLarge + ) +} + +@Composable +private fun ContentMaker(modifier: Modifier = Modifier) { + Text( + text = "Maker: Android Cheese", + modifier = modifier, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) + ) +} + +@Composable +private fun CardIcon(modifier: Modifier = Modifier) { + Image( + painter = painterResource(R.drawable.cheese_1), + contentDescription = null, + modifier = modifier + .size(48.dp) + .clip(CircleShape) + ) +} + +@Composable +private fun ContentImagesRow(images: List>, modifier: Modifier = Modifier) { + Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(1.dp)) { + for ((resourceId, contentDescription) in images) { + Image( + painter = painterResource(resourceId), + contentDescription = contentDescription, + contentScale = ContentScale.Crop, + modifier = Modifier + .weight(1f) + .aspectRatio(1f) + ) + } + } +} + +@Composable +private fun ContentBody(maxLines: Int, modifier: Modifier = Modifier) { + Text( + text = LoremIpsum(32).values.joinToString(" ").replace('\n', ' '), + modifier = modifier, + style = MaterialTheme.typography.bodyMedium, + maxLines = maxLines, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Justify, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) + ) +} + +@Preview(showBackground = true) +@Composable +private fun PreviewExpandedContent() { + SnippetsTheme { + SharedTransformDemo() + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt index 3e60c10e8..d599e1586 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt @@ -18,6 +18,7 @@ package com.example.compose.snippets.animations.sharedelement +import androidx.activity.compose.PredictiveBackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope @@ -30,7 +31,9 @@ import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.core.ArcMode import androidx.compose.animation.core.ExperimentalAnimationSpecApi import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.SeekableTransitionState import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.rememberTransition import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -60,12 +63,15 @@ import androidx.compose.material.icons.outlined.Create import androidx.compose.material.icons.outlined.Favorite import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.Icon +import androidx.compose.material3.Slider import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -85,6 +91,8 @@ import androidx.navigation.navArgument import com.example.compose.snippets.R import com.example.compose.snippets.ui.theme.LavenderLight import com.example.compose.snippets.ui.theme.RoseLight +import kotlin.coroutines.cancellation.CancellationException +import kotlinx.coroutines.launch @Preview @Composable @@ -628,3 +636,96 @@ fun PlaceholderSizeAnimated_Demo() { } // [END android_compose_shared_element_placeholder_size] } + +private sealed class Screen { + data object Home : Screen() + data class Details(val id: Int) : Screen() +} + +@Preview +@Composable +fun CustomPredictiveBackHandle() { + // [START android_compose_shared_element_custom_seeking] + val seekableTransitionState = remember { + SeekableTransitionState(Screen.Home) + } + val transition = rememberTransition(transitionState = seekableTransitionState) + + PredictiveBackHandler(seekableTransitionState.currentState is Screen.Details) { progress -> + try { + // Whilst a back gesture is in progress, backEvents will be fired for each progress + // update. + progress.collect { backEvent -> + // For each backEvent that comes in, we manually seekTo the reported back progress + try { + seekableTransitionState.seekTo(backEvent.progress, targetState = Screen.Home) + } catch (e: CancellationException) { + // seekTo may be cancelled as expected, if animateTo or subsequent seekTo calls + // before the current seekTo finishes, in this case, we ignore the cancellation. + } + } + // Once collection has completed, we are either fully in the target state, or need + // to progress towards the end. + seekableTransitionState.animateTo(seekableTransitionState.targetState) + } catch (e: CancellationException) { + // When the predictive back gesture is cancelled, we snap to the end state to ensure + // it completes its seeking animation back to the currentState + seekableTransitionState.snapTo(seekableTransitionState.currentState) + } + } + val coroutineScope = rememberCoroutineScope() + var lastNavigatedIndex by remember { + mutableIntStateOf(0) + } + Column { + Slider( + modifier = Modifier.height(48.dp), + value = seekableTransitionState.fraction, + onValueChange = { + coroutineScope.launch { + if (seekableTransitionState.currentState is Screen.Details) { + seekableTransitionState.seekTo(it, Screen.Home) + } else { + // seek to the previously navigated index + seekableTransitionState.seekTo(it, Screen.Details(lastNavigatedIndex)) + } + } + } + ) + SharedTransitionLayout(modifier = Modifier.weight(1f)) { + transition.AnimatedContent { targetState -> + when (targetState) { + Screen.Home -> { + HomeScreen( + this@SharedTransitionLayout, + this@AnimatedContent, + onItemClick = { + coroutineScope.launch { + lastNavigatedIndex = it + seekableTransitionState.animateTo(Screen.Details(it)) + } + } + ) + } + + is Screen.Details -> { + val snack = listSnacks[targetState.id] + DetailsScreen( + targetState.id, + snack, + this@SharedTransitionLayout, + this@AnimatedContent, + onBackPressed = { + coroutineScope.launch { + seekableTransitionState.animateTo(Screen.Home) + } + } + ) + } + } + } + } + } + + // [END android_compose_shared_element_custom_seeking] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt index da1d178fb..cac68a586 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt @@ -46,7 +46,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -75,9 +74,9 @@ fun SharedElement_PredictiveBack() { ) { composable("home") { HomeScreen( - navController, this@SharedTransitionLayout, - this@composable + this@composable, + { navController.navigate("details/$it") } ) } composable( @@ -87,11 +86,13 @@ fun SharedElement_PredictiveBack() { val id = backStackEntry.arguments?.getInt("item") val snack = listSnacks[id!!] DetailsScreen( - navController, id, snack, this@SharedTransitionLayout, - this@composable + this@composable, + { + navController.navigate("home") + } ) } } @@ -99,19 +100,19 @@ fun SharedElement_PredictiveBack() { } @Composable -private fun DetailsScreen( - navController: NavHostController, +fun DetailsScreen( id: Int, snack: Snack, sharedTransitionScope: SharedTransitionScope, - animatedContentScope: AnimatedContentScope + animatedContentScope: AnimatedContentScope, + onBackPressed: () -> Unit ) { with(sharedTransitionScope) { Column( Modifier .fillMaxSize() .clickable { - navController.navigate("home") + onBackPressed() } ) { Image( @@ -141,10 +142,10 @@ private fun DetailsScreen( } @Composable -private fun HomeScreen( - navController: NavHostController, +fun HomeScreen( sharedTransitionScope: SharedTransitionScope, - animatedContentScope: AnimatedContentScope + animatedContentScope: AnimatedContentScope, + onItemClick: (Int) -> Unit, ) { LazyColumn( modifier = Modifier @@ -155,7 +156,7 @@ private fun HomeScreen( itemsIndexed(listSnacks) { index, item -> Row( Modifier.clickable { - navController.navigate("details/$index") + onItemClick(index) } ) { Spacer(modifier = Modifier.width(8.dp)) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/AppBar.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/AppBar.kt index 8199b00cd..354efe635 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/AppBar.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/AppBar.kt @@ -16,12 +16,17 @@ package com.example.compose.snippets.components +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add @@ -30,6 +35,7 @@ import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.Share import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBarDefaults import androidx.compose.material3.Button @@ -40,6 +46,7 @@ import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.Scaffold @@ -52,6 +59,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -76,6 +84,7 @@ fun AppBarExamples( "topBarMedium" -> MediumTopAppBarExample() "topBarLarge" -> LargeTopAppBarExample() "topBarNavigation" -> TopBarNavigationExample { navigateBack() } + "multiSelection" -> AppBarMultiSelectionExample() else -> AppBarOptions( toBottom = { selection = "bottomBar" }, toTopBarSmall = { selection = "topBar" }, @@ -83,6 +92,7 @@ fun AppBarExamples( toTopBarMedium = { selection = "topBarMedium" }, toTopBarLarge = { selection = "topBarLarge" }, toTopBarNavigation = { selection = "topBarNavigation" }, + toMultiSelection = { selection = "multiSelection" }, ) } } @@ -96,6 +106,7 @@ fun AppBarOptions( toTopBarMedium: () -> Unit, toTopBarLarge: () -> Unit, toTopBarNavigation: () -> Unit, + toMultiSelection: () -> Unit, ) { Column() { Button({ toBottom() }) { @@ -116,6 +127,9 @@ fun AppBarOptions( Button({ toTopBarNavigation() }) { Text("Top bar navigation example") } + Button({ toMultiSelection() }) { + Text("Top bar with multi selection list") + } } } @@ -382,3 +396,156 @@ fun ScrollContent(innerPadding: PaddingValues) { } } } + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_appbarselectionactions] +@Composable +fun AppBarSelectionActions( + selectedItems: Set, + modifier: Modifier = Modifier, +) { + val hasSelection = selectedItems.isNotEmpty() + val topBarText = if (hasSelection) { + "Selected ${selectedItems.size} items" + } else { + "List of items" + } + + TopAppBar( + title = { + Text(topBarText) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + actions = { + if (hasSelection) { + IconButton(onClick = { + /* click action */ + }) { + Icon( + imageVector = Icons.Filled.Share, + contentDescription = "Share items" + ) + } + } + }, + ) +} +// [END android_compose_components_appbarselectionactions] + +@Preview +@Composable +private fun AppBarSelectionActionsPreview() { + val selectedItems = setOf(1, 2, 3) + + AppBarSelectionActions(selectedItems) +} + +@OptIn(ExperimentalFoundationApi::class) +@Preview +// [START android_compose_components_appbarmultiselectionexample] +@Composable +private fun AppBarMultiSelectionExample( + modifier: Modifier = Modifier, +) { + val listItems by remember { mutableStateOf(listOf(1, 2, 3, 4, 5, 6)) } + var selectedItems by rememberSaveable { mutableStateOf(setOf()) } + + Scaffold( + topBar = { AppBarSelectionActions(selectedItems) } + ) { innerPadding -> + LazyColumn(contentPadding = innerPadding) { + itemsIndexed(listItems) { _, index -> + val isItemSelected = selectedItems.contains(index) + ListItemSelectable( + selected = isItemSelected, + Modifier + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + /* click action */ + }, + onLongClick = { + if (isItemSelected) selectedItems -= index else selectedItems += index + } + ) + ) + } + } + } +} +// [END android_compose_components_appbarmultiselectionexample] + +// [START android_compose_components_listitemselectable] +@Composable +fun ListItemSelectable( + selected: Boolean, + modifier: Modifier = Modifier +) { + Box(modifier = modifier) { + ListItem( + headlineContent = { Text("Long press to select or deselect item") }, + leadingContent = { + if (selected) { + Icon( + Icons.Filled.Check, + contentDescription = "Localized description", + ) + } + } + ) + } +} +// [END android_compose_components_listitemselectable] + +@Preview +@Composable +private fun ListItemSelectablePreview() { + ListItemSelectable(true) +} + +@OptIn(ExperimentalFoundationApi::class) +// [START android_compose_components_lazylistmultiselection +@Composable +fun LazyListMultiSelection( + listItems: List, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp), +) { + var selectedItems by rememberSaveable { mutableStateOf(setOf()) } + + LazyColumn(contentPadding = contentPadding) { + itemsIndexed(listItems) { _, index -> + val selected = selectedItems.contains(index) + ListItemSelectable( + selected = selected, + Modifier + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + /* click action */ + }, + onLongClick = { + if (selected) selectedItems -= index else selectedItems += index + } + ) + ) + } + } +} +// [END android_compose_components_lazylistmultiselection + +@Preview +@Composable +private fun LazyListMultiSelectionPreview() { + val listItems = listOf(1, 2, 3) + + LazyListMultiSelection( + listItems, + modifier = Modifier + ) +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Carousel.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Carousel.kt new file mode 100644 index 000000000..fb8a31775 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Carousel.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel +import androidx.compose.material3.carousel.HorizontalUncontainedCarousel +import androidx.compose.material3.carousel.rememberCarouselState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +// [START android_compose_carousel_multi_browse_basic] +@Composable +fun CarouselExample_MultiBrowse() { + data class CarouselItem( + val id: Int, + @DrawableRes val imageResId: Int, + val contentDescription: String + ) + + val items = remember { + listOf( + CarouselItem(0, R.drawable.cupcake, "cupcake"), + CarouselItem(1, R.drawable.donut, "donut"), + CarouselItem(2, R.drawable.eclair, "eclair"), + CarouselItem(3, R.drawable.froyo, "froyo"), + CarouselItem(4, R.drawable.gingerbread, "gingerbread"), + ) + } + + HorizontalMultiBrowseCarousel( + state = rememberCarouselState { items.count() }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 16.dp, bottom = 16.dp), + preferredItemWidth = 186.dp, + itemSpacing = 8.dp, + contentPadding = PaddingValues(horizontal = 16.dp) + ) { i -> + val item = items[i] + Image( + modifier = Modifier + .height(205.dp) + .maskClip(MaterialTheme.shapes.extraLarge), + painter = painterResource(id = item.imageResId), + contentDescription = item.contentDescription, + contentScale = ContentScale.Crop + ) + } +} +// [END android_compose_carousel_multi_browse_basic] + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +// [START android_compose_carousel_uncontained_basic] +@Composable +fun CarouselExample() { + data class CarouselItem( + val id: Int, + @DrawableRes val imageResId: Int, + val contentDescription: String + ) + + val carouselItems = remember { + listOf( + CarouselItem(0, R.drawable.cupcake, "cupcake"), + CarouselItem(1, R.drawable.donut, "donut"), + CarouselItem(2, R.drawable.eclair, "eclair"), + CarouselItem(3, R.drawable.froyo, "froyo"), + CarouselItem(4, R.drawable.gingerbread, "gingerbread"), + ) + } + + HorizontalUncontainedCarousel( + state = rememberCarouselState { carouselItems.count() }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 16.dp, bottom = 16.dp), + itemWidth = 186.dp, + itemSpacing = 8.dp, + contentPadding = PaddingValues(horizontal = 16.dp) + ) { i -> + val item = carouselItems[i] + Image( + modifier = Modifier + .height(205.dp) + .maskClip(MaterialTheme.shapes.extraLarge), + painter = painterResource(id = item.imageResId), + contentDescription = item.contentDescription, + contentScale = ContentScale.Crop + ) + } +} +// [END android_compose_carousel_uncontained_basic] + +@Preview +@Composable +fun CarouselExamples() { + Column { + CarouselExample() + CarouselExample_MultiBrowse() + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/ComponentsScreen.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/ComponentsScreen.kt index ce1bbae40..7f87a5c95 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/ComponentsScreen.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/ComponentsScreen.kt @@ -16,45 +16,59 @@ package com.example.compose.snippets.components +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.example.compose.snippets.navigation.TopComponentsDestination +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ComponentsScreen( navigate: (TopComponentsDestination) -> Unit ) { - LazyColumn( - modifier = Modifier - .padding(16.dp) - .fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - items(TopComponentsDestination.entries) { destination -> - NavigationItem(destination) { - navigate( - destination - ) + Scaffold(topBar = { + TopAppBar(title = { + Text("Common Components") + }) + }, content = { padding -> + LazyColumn( + modifier = Modifier + .padding(padding) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + items(TopComponentsDestination.entries) { destination -> + NavigationItem(destination) { + navigate( + destination + ) + } } } - } + }) } @Composable fun NavigationItem(destination: TopComponentsDestination, onClick: () -> Unit) { - Button( - onClick = { onClick() } - ) { - Text(destination.title) - } + ListItem( + headlineContent = { + Text(destination.title) + }, + modifier = Modifier.clickable { + onClick() + } + ) } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt index 650b969a0..692199123 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt @@ -17,6 +17,9 @@ package com.example.compose.snippets.components import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -49,12 +52,24 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup +import com.example.compose.snippets.touchinput.userinteractions.MyAppTheme import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +@Preview +@Composable +private fun DatePickerPreview() { + MyAppTheme { + DatePickerExamples() + } +} + // [START android_compose_components_datepicker_examples] // [START_EXCLUDE] @Composable @@ -77,6 +92,9 @@ fun DatePickerExamples() { Text("Docked date picker:") DatePickerDocked() + Text("Open modal picker on click") + DatePickerFieldToModal() + Text("Modal date pickers:") Button(onClick = { showModal = true }) { Text("Show Modal Date Picker") @@ -259,6 +277,43 @@ fun DatePickerDocked() { } } +@Composable +fun DatePickerFieldToModal(modifier: Modifier = Modifier) { + var selectedDate by remember { mutableStateOf(null) } + var showModal by remember { mutableStateOf(false) } + + OutlinedTextField( + value = selectedDate?.let { convertMillisToDate(it) } ?: "", + onValueChange = { }, + label = { Text("DOB") }, + placeholder = { Text("MM/DD/YYYY") }, + trailingIcon = { + Icon(Icons.Default.DateRange, contentDescription = "Select date") + }, + modifier = modifier + .fillMaxWidth() + .pointerInput(selectedDate) { + awaitEachGesture { + // Modifier.clickable doesn't work for text fields, so we use Modifier.pointerInput + // in the Initial pass to observe events before the text field consumes them + // in the Main pass. + awaitFirstDown(pass = PointerEventPass.Initial) + val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Initial) + if (upEvent != null) { + showModal = true + } + } + } + ) + + if (showModal) { + DatePickerModal( + onDateSelected = { selectedDate = it }, + onDismiss = { showModal = false } + ) + } +} + fun convertMillisToDate(millis: Long): String { val formatter = SimpleDateFormat("MM/dd/yyyy", Locale.getDefault()) return formatter.format(Date(millis)) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.kt new file mode 100644 index 000000000..a3468a7c2 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.kt @@ -0,0 +1,304 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.DirectionsBike +import androidx.compose.material.icons.automirrored.filled.DirectionsRun +import androidx.compose.material.icons.automirrored.filled.DirectionsWalk +import androidx.compose.material.icons.automirrored.outlined.Help +import androidx.compose.material.icons.automirrored.outlined.OpenInNew +import androidx.compose.material.icons.automirrored.outlined.Send +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Hiking +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Tune +import androidx.compose.material.icons.outlined.Feedback +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun MenusExamples() { + var currentExample by remember { mutableStateOf<(@Composable () -> Unit)?>(null) } + + Box(modifier = Modifier.fillMaxSize()) { + currentExample?.let { + it() + FloatingActionButton( + onClick = { currentExample = null }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp) + ) { + Text(text = "Close example", modifier = Modifier.padding(16.dp)) + } + return + } + + Column(modifier = Modifier.padding(16.dp)) { + Button(onClick = { currentExample = { MinimalDropdownMenu() } }) { + Text("Minimal dropdown menu") + } + Button(onClick = { currentExample = { LongBasicDropdownMenu() } }) { + Text("Dropdown menu with many items") + } + Button(onClick = { currentExample = { DropdownMenuWithDetails() } }) { + Text("Dropdown menu with sections and icons") + } + Button(onClick = { currentExample = { DropdownFilter() } }) { + Text("Menu for applying a filter, attached to a filter chip") + } + } + } +} + +// [START android_compose_components_minimaldropdownmenu] +@Composable +fun MinimalDropdownMenu() { + var expanded by remember { mutableStateOf(false) } + Box( + modifier = Modifier + .padding(16.dp) + ) { + IconButton(onClick = { expanded = !expanded }) { + Icon(Icons.Default.MoreVert, contentDescription = "More options") + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + DropdownMenuItem( + text = { Text("Option 1") }, + onClick = { /* Do something... */ } + ) + DropdownMenuItem( + text = { Text("Option 2") }, + onClick = { /* Do something... */ } + ) + } + } +} +// [END android_compose_components_minimaldropdownmenu] + +@Preview +@Composable +fun MinimalDropdownMenuPreview() { + MinimalDropdownMenu() +} + +// [START android_compose_components_longbasicdropdownmenu] +@Composable +fun LongBasicDropdownMenu() { + var expanded by remember { mutableStateOf(false) } + // Placeholder list of 100 strings for demonstration + val menuItemData = List(100) { "Option ${it + 1}" } + + Box( + modifier = Modifier + .padding(16.dp) + ) { + IconButton(onClick = { expanded = !expanded }) { + Icon(Icons.Default.MoreVert, contentDescription = "More options") + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + menuItemData.forEach { option -> + DropdownMenuItem( + text = { Text(option) }, + onClick = { /* Do something... */ } + ) + } + } + } +} +// [END android_compose_components_longbasicdropdownmenu] + +@Preview +@Composable +fun LongBasicDropdownMenuPreview() { + LongBasicDropdownMenu() +} + +// [START android_compose_components_dropdownmenuwithdetails] +@Composable +fun DropdownMenuWithDetails() { + var expanded by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + IconButton(onClick = { expanded = !expanded }) { + Icon(Icons.Default.MoreVert, contentDescription = "More options") + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + // First section + DropdownMenuItem( + text = { Text("Profile") }, + leadingIcon = { Icon(Icons.Outlined.Person, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + DropdownMenuItem( + text = { Text("Settings") }, + leadingIcon = { Icon(Icons.Outlined.Settings, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + + HorizontalDivider() + + // Second section + DropdownMenuItem( + text = { Text("Send Feedback") }, + leadingIcon = { Icon(Icons.Outlined.Feedback, contentDescription = null) }, + trailingIcon = { Icon(Icons.AutoMirrored.Outlined.Send, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + + HorizontalDivider() + + // Third section + DropdownMenuItem( + text = { Text("About") }, + leadingIcon = { Icon(Icons.Outlined.Info, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + DropdownMenuItem( + text = { Text("Help") }, + leadingIcon = { Icon(Icons.AutoMirrored.Outlined.Help, contentDescription = null) }, + trailingIcon = { Icon(Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + } + } +} +// [END android_compose_components_dropdownmenuwithdetails] + +@Preview +@Composable +fun DropdownMenuWithDetailsPreview() { + DropdownMenuWithDetails() +} + +@Composable +fun DropdownFilter(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .padding(16.dp) + .wrapContentSize(unbounded = true), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Default.Tune, "Filters") + FilterChip(selected = false, onClick = { /*TODO*/ }, label = { Text("Time") }) + DropdownFilterChip() + FilterChip(selected = false, onClick = { /*TODO*/ }, label = { Text("Wheelchair accessible") }) + } +} + +// [START android_compose_components_dropdownfilterchip] +@Composable +fun DropdownFilterChip(modifier: Modifier = Modifier) { + var isDropdownExpanded by remember { mutableStateOf(false) } + var selectedChipText by remember { mutableStateOf(null) } + Box(modifier) { + FilterChip( + selected = selectedChipText != null, + onClick = { isDropdownExpanded = !isDropdownExpanded }, + label = { Text(if (selectedChipText == null) "Type" else "$selectedChipText") }, + leadingIcon = { if (selectedChipText != null) Icon(Icons.Default.Check, null) }, + trailingIcon = { Icon(Icons.Default.ArrowDropDown, null) }, + ) + DropdownMenu( + expanded = isDropdownExpanded, + onDismissRequest = { isDropdownExpanded = !isDropdownExpanded } + ) { + DropdownMenuItem( + text = { Text("Running") }, + leadingIcon = { Icon(Icons.AutoMirrored.Default.DirectionsRun, null) }, + onClick = { + selectedChipText = + if (selectedChipText == "Running") null else "Running" + } + ) + DropdownMenuItem( + text = { Text("Walking") }, + leadingIcon = { Icon(Icons.AutoMirrored.Default.DirectionsWalk, null) }, + onClick = { + selectedChipText = + if (selectedChipText == "Walking") null else "Walking" + } + ) + DropdownMenuItem( + text = { Text("Hiking") }, + leadingIcon = { Icon(Icons.Default.Hiking, null) }, + onClick = { + selectedChipText = + if (selectedChipText == "Hiking") null else "Hiking" + } + ) + DropdownMenuItem( + text = { Text("Cycling") }, + leadingIcon = { Icon(Icons.AutoMirrored.Default.DirectionsBike, null) }, + onClick = { + selectedChipText = + if (selectedChipText == "Cycling") null else "Cycling" + } + ) + } + } +} +// [END android_compose_components_dropdownfilterchip] + +@Preview +@Composable +private fun DropdownFilterPreview() { + DropdownFilter() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/NavigationDrawer.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/NavigationDrawer.kt new file mode 100644 index 000000000..993f1c99a --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/NavigationDrawer.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Help +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + +@Composable +fun NavigationDrawerExamples() { + // Add more examples here in future if necessary. + + DetailedDrawerExample { innerPadding -> + Box(modifier = Modifier.padding(innerPadding)) { + Text( + "Swipe from left edge or use menu icon to open the dismissible drawer", + modifier = Modifier.padding(16.dp) + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_detaileddrawerexample] +@Composable +fun DetailedDrawerExample( + content: @Composable (PaddingValues) -> Unit +) { + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val scope = rememberCoroutineScope() + + ModalNavigationDrawer( + drawerContent = { + ModalDrawerSheet { + Column( + modifier = Modifier.padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()) + ) { + Spacer(Modifier.height(12.dp)) + Text("Drawer Title", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleLarge) + HorizontalDivider() + + Text("Section 1", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium) + NavigationDrawerItem( + label = { Text("Item 1") }, + selected = false, + onClick = { /* Handle click */ } + ) + NavigationDrawerItem( + label = { Text("Item 2") }, + selected = false, + onClick = { /* Handle click */ } + ) + + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + + Text("Section 2", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium) + NavigationDrawerItem( + label = { Text("Settings") }, + selected = false, + icon = { Icon(Icons.Outlined.Settings, contentDescription = null) }, + badge = { Text("20") }, // Placeholder + onClick = { /* Handle click */ } + ) + NavigationDrawerItem( + label = { Text("Help and feedback") }, + selected = false, + icon = { Icon(Icons.AutoMirrored.Outlined.Help, contentDescription = null) }, + onClick = { /* Handle click */ }, + ) + Spacer(Modifier.height(12.dp)) + } + } + }, + drawerState = drawerState + ) { + Scaffold( + topBar = { + TopAppBar( + title = { Text("Navigation Drawer Example") }, + navigationIcon = { + IconButton(onClick = { + scope.launch { + if (drawerState.isClosed) { + drawerState.open() + } else { + drawerState.close() + } + } + }) { + Icon(Icons.Default.Menu, contentDescription = "Menu") + } + } + ) + } + ) { innerPadding -> + content(innerPadding) + } + } +} +// [END android_compose_components_detaileddrawerexample] + +@Preview +@Composable +fun DetailedDrawerExamplePreview() { + NavigationDrawerExamples() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/PullToRefreshBox.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/PullToRefreshBox.kt new file mode 100644 index 000000000..81280151a --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/PullToRefreshBox.kt @@ -0,0 +1,241 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CloudDownload +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.PositionalThreshold +import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.material3.pulltorefresh.pullToRefreshIndicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.components.PullToRefreshIndicatorConstants.CROSSFADE_DURATION_MILLIS +import com.example.compose.snippets.components.PullToRefreshIndicatorConstants.SPINNER_SIZE +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private object PullToRefreshIndicatorConstants { + const val CROSSFADE_DURATION_MILLIS = 100 + val SPINNER_SIZE = 16.dp +} + +@Preview +@Composable +fun PullToRefreshBasicPreview() { + PullToRefreshStatefulWrapper { itemCount, isRefreshing, onRefresh -> + val items = List(itemCount) { "Item ${itemCount - it}" } + PullToRefreshBasicSample(items, isRefreshing, onRefresh) + } +} + +@Preview +@Composable +fun PullToRefreshCustomStylePreview() { + PullToRefreshStatefulWrapper { itemCount, isRefreshing, onRefresh -> + val items = List(itemCount) { "Item ${itemCount - it}" } + PullToRefreshCustomStyleSample(items, isRefreshing, onRefresh) + } +} + +@Preview +@Composable +fun PullToRefreshCustomIndicatorPreview() { + PullToRefreshStatefulWrapper { itemCount, isRefreshing, onRefresh -> + val items = List(itemCount) { "Item ${itemCount - it}" } + PullToRefreshCustomIndicatorSample(items, isRefreshing, onRefresh) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_pull_to_refresh_basic] +@Composable +fun PullToRefreshBasicSample( + items: List, + isRefreshing: Boolean, + onRefresh: () -> Unit, + modifier: Modifier = Modifier +) { + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = onRefresh, + modifier = modifier + ) { + LazyColumn(Modifier.fillMaxSize()) { + items(items) { + ListItem({ Text(text = it) }) + } + } + } +} +// [END android_compose_components_pull_to_refresh_basic] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_pull_to_refresh_custom_style] +@Composable +fun PullToRefreshCustomStyleSample( + items: List, + isRefreshing: Boolean, + onRefresh: () -> Unit, + modifier: Modifier = Modifier +) { + val state = rememberPullToRefreshState() + + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = onRefresh, + modifier = modifier, + state = state, + indicator = { + Indicator( + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = isRefreshing, + containerColor = MaterialTheme.colorScheme.primaryContainer, + color = MaterialTheme.colorScheme.onPrimaryContainer, + state = state + ) + }, + ) { + LazyColumn(Modifier.fillMaxSize()) { + items(items) { + ListItem({ Text(text = it) }) + } + } + } +} +// [END android_compose_components_pull_to_refresh_custom_style] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_pull_to_refresh_custom_indicator] +@Composable +fun PullToRefreshCustomIndicatorSample( + items: List, + isRefreshing: Boolean, + onRefresh: () -> Unit, + modifier: Modifier = Modifier +) { + val state = rememberPullToRefreshState() + + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = onRefresh, + modifier = modifier, + state = state, + indicator = { + MyCustomIndicator( + state = state, + isRefreshing = isRefreshing, + modifier = Modifier.align(Alignment.TopCenter) + ) + } + ) { + LazyColumn(Modifier.fillMaxSize()) { + items(items) { + ListItem({ Text(text = it) }) + } + } + } +} + +// [START_EXCLUDE] +@OptIn(ExperimentalMaterial3Api::class) +// [END_EXCLUDE] +@Composable +fun MyCustomIndicator( + state: PullToRefreshState, + isRefreshing: Boolean, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.pullToRefreshIndicator( + state = state, + isRefreshing = isRefreshing, + containerColor = PullToRefreshDefaults.containerColor, + threshold = PositionalThreshold + ), + contentAlignment = Alignment.Center + ) { + Crossfade( + targetState = isRefreshing, + animationSpec = tween(durationMillis = CROSSFADE_DURATION_MILLIS), + modifier = Modifier.align(Alignment.Center) + ) { refreshing -> + if (refreshing) { + CircularProgressIndicator(Modifier.size(SPINNER_SIZE)) + } else { + val distanceFraction = { state.distanceFraction.coerceIn(0f, 1f) } + Icon( + imageVector = Icons.Filled.CloudDownload, + contentDescription = "Refresh", + modifier = Modifier + .size(18.dp) + .graphicsLayer { + val progress = distanceFraction() + this.alpha = progress + this.scaleX = progress + this.scaleY = progress + } + ) + } + } + } +} +// [END android_compose_components_pull_to_refresh_custom_indicator] + +@Composable +fun PullToRefreshStatefulWrapper( + content: @Composable (itemCount: Int, isRefreshing: Boolean, onRefresh: () -> Unit) -> Unit +) { + var itemCount by remember { mutableIntStateOf(15) } + var isRefreshing by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + val onRefresh: () -> Unit = { + isRefreshing = true + coroutineScope.launch { + delay(1500) + itemCount += 5 + isRefreshing = false + } + } + content(itemCount, isRefreshing, onRefresh) +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/RadioButton.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/RadioButton.kt new file mode 100644 index 000000000..93961f245 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/RadioButton.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +// [START android_compose_components_radiobuttonsingleselection] +@Composable +fun RadioButtonSingleSelection(modifier: Modifier = Modifier) { + val radioOptions = listOf("Calls", "Missed", "Friends") + val (selectedOption, onOptionSelected) = remember { mutableStateOf(radioOptions[0]) } + // Note that Modifier.selectableGroup() is essential to ensure correct accessibility behavior + Column(modifier.selectableGroup()) { + radioOptions.forEach { text -> + Row( + Modifier + .fillMaxWidth() + .height(56.dp) + .selectable( + selected = (text == selectedOption), + onClick = { onOptionSelected(text) }, + role = Role.RadioButton + ) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = (text == selectedOption), + onClick = null // null recommended for accessibility with screen readers + ) + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(start = 16.dp) + ) + } + } + } +} +// [END android_compose_components_radiobuttonsingleselection] + +@Preview +@Composable +private fun RadioButtonSingleSelectionPreview() { + RadioButtonSingleSelection() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.kt new file mode 100644 index 000000000..78c37b8e5 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.kt @@ -0,0 +1,244 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun SearchBarExamples() { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + var currentExample by remember { mutableStateOf(null) } + + when (currentExample) { + "basic" -> SearchBarBasicFilterList() + "advanced" -> AppSearchBar() + else -> { + Button(onClick = { currentExample = "basic" }) { + Text("Basic search bar with filter") + } + Button(onClick = { currentExample = "advanced" }) { + Text("Advanced search bar with filter") + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_searchbarbasicfilterlist] +@Composable +fun SearchBarBasicFilterList(modifier: Modifier = Modifier) { + var text by rememberSaveable { mutableStateOf("") } + var expanded by rememberSaveable { mutableStateOf(false) } + Box( + modifier + .fillMaxSize() + .semantics { isTraversalGroup = true } + ) { + SearchBar( + modifier = Modifier + .align(Alignment.TopCenter) + .semantics { traversalIndex = 0f }, + inputField = { + SearchBarDefaults.InputField( + query = text, + onQueryChange = { text = it }, + onSearch = { expanded = false }, + expanded = expanded, + onExpandedChange = { expanded = it }, + placeholder = { Text("Hinted search text") } + ) + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + Column(Modifier.verticalScroll(rememberScrollState())) { + repeat(4) { index -> + val resultText = "Suggestion $index" + ListItem( + headlineContent = { Text(resultText) }, + supportingContent = { Text("Additional info") }, + modifier = Modifier + .clickable { + text = resultText + expanded = false + } + .fillMaxWidth() + ) + } + } + } + } +} +// [END android_compose_components_searchbarbasicfilterlist] + +@Preview(showBackground = true) +@Composable +private fun SearchBarBasicFilterListPreview() { + SearchBarBasicFilterList() +} + +// [START android_compose_components_searchbarfilterlist] +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchBarFilterList( + list: List, + modifier: Modifier = Modifier +) { + var text by rememberSaveable { mutableStateOf("") } + val filteredList by remember { + derivedStateOf { + list.filter { it.lowercase().contains(text.lowercase()) } + } + } + var expanded by rememberSaveable { mutableStateOf(false) } + + Box( + modifier + .fillMaxSize() + .semantics { isTraversalGroup = true } + ) { + SearchBar( + modifier = Modifier + .align(Alignment.TopCenter) + .semantics { traversalIndex = 0f }, + inputField = { + SearchBarDefaults.InputField( + query = text, + onQueryChange = { text = it }, + onSearch = { expanded = false }, + expanded = expanded, + onExpandedChange = { expanded = it }, + placeholder = { Text("Hinted search text") }, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = "Search") }, + trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = "More options") }, + ) + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + LazyColumn { + items(count = filteredList.size) { index -> + val resultText = filteredList[index] + ListItem( + headlineContent = { Text(resultText) }, + supportingContent = { Text("Additional info") }, + leadingContent = { + Icon( + Icons.Filled.Star, + contentDescription = "Starred item" + ) + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + modifier = Modifier + .clickable { + text = resultText + expanded = false + } + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + ) + } + } + } + LazyColumn( + contentPadding = PaddingValues( + start = 16.dp, + top = 72.dp, + end = 16.dp, + bottom = 16.dp + ), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.semantics { + traversalIndex = 1f + }, + ) { + items(count = filteredList.size) { + Text(text = filteredList[it]) + } + } + } +} +// [END android_compose_components_searchbarfilterlist] + +@Preview(showBackground = true) +@Composable +fun AppSearchBar(modifier: Modifier = Modifier) { + SearchBarFilterList( + list = listOf( + "Cupcake", + "Donut", + "Eclair", + "Froyo", + "Gingerbread", + "Honeycomb", + "Ice Cream Sandwich", + "Jelly Bean", + "KitKat", + "Lollipop", + "Marshmallow", + "Nougat", + "Oreo", + "Pie" + ), + modifier + ) +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/SegmentedButton.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/SegmentedButton.kt new file mode 100644 index 000000000..21fbd9d65 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/SegmentedButton.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.DirectionsWalk +import androidx.compose.material.icons.filled.DirectionsBus +import androidx.compose.material.icons.filled.DirectionsCar +import androidx.compose.material3.Icon +import androidx.compose.material3.MultiChoiceSegmentedButtonRow +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun SegmentedButtonExamples() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + + ) { + SingleChoiceSegmentedButton() + Spacer(modifier = Modifier.height(16.dp)) + MultiChoiceSegmentedButton() + } +} + +// [START android_compose_components_singlechoicesegmentedbutton] +@Composable +fun SingleChoiceSegmentedButton(modifier: Modifier = Modifier) { + var selectedIndex by remember { mutableIntStateOf(0) } + val options = listOf("Day", "Month", "Week") + + SingleChoiceSegmentedButtonRow { + options.forEachIndexed { index, label -> + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = options.size + ), + onClick = { selectedIndex = index }, + selected = index == selectedIndex, + label = { Text(label) } + ) + } + } +} +// [END android_compose_components_singlechoicesegmentedbutton] + +@Preview +@Composable +private fun SingleChoiceSegmentedButtonPreview() { + SingleChoiceSegmentedButton() +} + +// [START android_compose_components_multichoicesegmentedbutton] +@Composable +fun MultiChoiceSegmentedButton(modifier: Modifier = Modifier) { + val selectedOptions = remember { + mutableStateListOf(false, false, false) + } + val options = listOf("Walk", "Ride", "Drive") + + MultiChoiceSegmentedButtonRow { + options.forEachIndexed { index, label -> + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = options.size + ), + checked = selectedOptions[index], + onCheckedChange = { + selectedOptions[index] = !selectedOptions[index] + }, + icon = { SegmentedButtonDefaults.Icon(selectedOptions[index]) }, + label = { + when (label) { + "Walk" -> Icon( + imageVector = + Icons.AutoMirrored.Filled.DirectionsWalk, + contentDescription = "Directions Walk" + ) + "Ride" -> Icon( + imageVector = + Icons.Default.DirectionsBus, + contentDescription = "Directions Bus" + ) + "Drive" -> Icon( + imageVector = + Icons.Default.DirectionsCar, + contentDescription = "Directions Car" + ) + } + } + ) + } + } +} +// [END android_compose_components_multichoicesegmentedbutton] + +@Preview +@Composable +private fun MultiChoiceSegmentedButtonPreview() { + MultiChoiceSegmentedButton() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.kt new file mode 100644 index 000000000..34a6aaebe --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.kt @@ -0,0 +1,339 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckBox +import androidx.compose.material.icons.filled.CheckBoxOutlineBlank +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.material3.Text +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun SwipeToDismissBoxExamples() { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Swipe to dismiss with change of background", fontWeight = FontWeight.Bold) + SwipeItemExample() + Text("Swipe to dismiss with a cross-fade animation", fontWeight = FontWeight.Bold) + SwipeCardItemExample() + } +} + +// [START android_compose_components_todoitem] +data class TodoItem( + var isItemDone: Boolean, + var itemDescription: String +) +// [END android_compose_components_todoitem] + +// [START android_compose_components_swipeitem] +@Composable +fun SwipeItem( + value: TodoItem, + startToEndAction: (TodoItem) -> Unit, + endToStartAction: (TodoItem) -> Unit, + modifier: Modifier = Modifier, + content: @Composable (TodoItem) -> Unit +) { + val swipeToDismissBoxState = rememberSwipeToDismissBoxState( + confirmValueChange = { + when (it) { + SwipeToDismissBoxValue.StartToEnd -> { + startToEndAction(value) + // Do not dismiss this item. + false + } + SwipeToDismissBoxValue.EndToStart -> { + endToStartAction(value) + true + } + SwipeToDismissBoxValue.Settled -> { + false + } + } + } + ) + + SwipeToDismissBox( + state = swipeToDismissBoxState, + modifier = modifier + .fillMaxSize(), + backgroundContent = { + Row( + modifier = Modifier + .background( + when (swipeToDismissBoxState.dismissDirection) { + SwipeToDismissBoxValue.StartToEnd -> { + Color.Blue + } + SwipeToDismissBoxValue.EndToStart -> { + Color.Red + } + SwipeToDismissBoxValue.Settled -> { + Color.LightGray + } + } + ) + .fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + when (swipeToDismissBoxState.dismissDirection) { + SwipeToDismissBoxValue.StartToEnd -> { + if (value.isItemDone) { + Icon( + imageVector = Icons.Default.CheckBox, + contentDescription = "Item done", + tint = Color.White, + modifier = Modifier + .padding(12.dp) + ) + } else { + Icon( + imageVector = Icons.Default.CheckBoxOutlineBlank, + contentDescription = "Item not done", + tint = Color.White, + modifier = Modifier + .padding(12.dp) + ) + } + } + + SwipeToDismissBoxValue.EndToStart -> { + Spacer(modifier = Modifier) + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Remove item", + tint = Color.White, + modifier = Modifier + .padding(12.dp) + ) + } + + SwipeToDismissBoxValue.Settled -> {} + } + } + } + ) { + content(value) + } +} +// [END android_compose_components_swipeitem] + +@Preview(showBackground = true) +// [START android_compose_components_swipeitemexample] +@Composable +private fun SwipeItemExample() { + val todoItems = remember { + mutableStateListOf( + TodoItem(isItemDone = false, itemDescription = "Pay bills"), + TodoItem(isItemDone = false, itemDescription = "Buy groceries"), + TodoItem(isItemDone = false, itemDescription = "Go to gym"), + TodoItem(isItemDone = false, itemDescription = "Get dinner") + ) + } + + LazyColumn { + items( + items = todoItems, + key = { it.itemDescription } + ) { todoItem -> + SwipeItem( + value = todoItem, + startToEndAction = { + todoItem.isItemDone = !todoItem.isItemDone + }, + endToStartAction = { + todoItems -= todoItem + } + ) { + ListItem( + headlineContent = { Text(text = todoItem.itemDescription) }, + supportingContent = { Text(text = "swipe me to update or remove.") } + ) + } + } + } +} +// [END android_compose_components_swipeitemexample] + +// [START android_compose_components_swipecarditem] +@Composable +fun SwipeCardItem( + value: TodoItem, + startToEndAction: (TodoItem) -> Unit, + endToStartAction: (TodoItem) -> Unit, + modifier: Modifier = Modifier, + content: @Composable (TodoItem) -> Unit +) { + val swipeToDismissState = rememberSwipeToDismissBoxState( + positionalThreshold = { totalDistance -> totalDistance * 0.25f }, + confirmValueChange = { + when (it) { + SwipeToDismissBoxValue.StartToEnd -> { + startToEndAction(value) + // Do not dismiss this item. + false + } + SwipeToDismissBoxValue.EndToStart -> { + endToStartAction(value) + true + } + SwipeToDismissBoxValue.Settled -> { + false + } + } + } + ) + + SwipeToDismissBox( + modifier = Modifier, + state = swipeToDismissState, + backgroundContent = { + // Cross-fade the background color as the drag gesture progresses. + val color by animateColorAsState( + when (swipeToDismissState.targetValue) { + SwipeToDismissBoxValue.Settled -> Color.LightGray + SwipeToDismissBoxValue.StartToEnd -> + lerp(Color.LightGray, Color.Blue, swipeToDismissState.progress) + + SwipeToDismissBoxValue.EndToStart -> + lerp(Color.LightGray, Color.Red, swipeToDismissState.progress) + }, + label = "swipeable card item background color" + ) + Row( + modifier = Modifier + .background(color) + .fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + when (swipeToDismissState.dismissDirection) { + SwipeToDismissBoxValue.StartToEnd -> { + if (value.isItemDone) { + Icon( + imageVector = Icons.Default.CheckBox, + contentDescription = "Item done", + tint = Color.White, + modifier = Modifier + .padding(12.dp) + ) + } else { + Icon( + imageVector = Icons.Default.CheckBoxOutlineBlank, + contentDescription = "Item not done", + tint = Color.White, + modifier = Modifier + .padding(12.dp) + ) + } + } + + SwipeToDismissBoxValue.EndToStart -> { + Spacer(modifier = Modifier) + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Remove item", + tint = Color.White, + modifier = Modifier + .padding(12.dp) + ) + } + + SwipeToDismissBoxValue.Settled -> {} + } + } + } + ) { + content(value) + } +} +// [END android_compose_components_swipecarditem] + +// [START android_compose_components_swipecarditemexample] +@Preview +@Composable +private fun SwipeCardItemExample() { + val todoItems = remember { + mutableStateListOf( + TodoItem(isItemDone = false, itemDescription = "Pay bills"), + TodoItem(isItemDone = false, itemDescription = "Buy groceries"), + TodoItem(isItemDone = false, itemDescription = "Go to gym"), + TodoItem(isItemDone = false, itemDescription = "Get dinner") + ) + } + + LazyColumn { + items( + items = todoItems, + key = { it.itemDescription } + ) { todoItem -> + SwipeCardItem( + value = todoItem, + startToEndAction = { + todoItem.isItemDone = !todoItem.isItemDone + }, + endToStartAction = { + todoItems -= todoItem + } + ) { + OutlinedCard(shape = RectangleShape) { + ListItem( + headlineContent = { Text(todoItem.itemDescription) }, + supportingContent = { Text("swipe me to update or remove.") } + ) + } + } + } + } +} +// [END android_compose_components_swipecarditemexample] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.kt new file mode 100644 index 000000000..1b5d9ea04 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.kt @@ -0,0 +1,185 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Camera +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.RichTooltip +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.rememberTooltipState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp + +@Composable +fun TooltipExamples() { + Text( + "Long press an icon to see the tooltip.", + modifier = Modifier.fillMaxWidth().padding(16.dp), + textAlign = TextAlign.Center + ) + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize() + ) { + PlainTooltipExample() + RichTooltipExample() + AdvancedRichTooltipExample() + } +} + +@Preview +@Composable +private fun TooltipExamplesPreview() { + TooltipExamples() +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_plaintooltipexample] +@Composable +fun PlainTooltipExample( + modifier: Modifier = Modifier, + plainTooltipText: String = "Add to favorites" +) { + TooltipBox( + modifier = modifier, + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip { Text(plainTooltipText) } + }, + state = rememberTooltipState() + ) { + IconButton(onClick = { /* Do something... */ }) { + Icon( + imageVector = Icons.Filled.Favorite, + contentDescription = "Add to favorites" + ) + } + } +} + +// [END android_compose_components_plaintooltipexample] + +@Preview +@Composable +private fun PlainTooltipSamplePreview() { + PlainTooltipExample() +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_richtooltipexample] +@Composable +fun RichTooltipExample( + modifier: Modifier = Modifier, + richTooltipSubheadText: String = "Rich Tooltip", + richTooltipText: String = "Rich tooltips support multiple lines of informational text." +) { + TooltipBox( + modifier = modifier, + positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(), + tooltip = { + RichTooltip( + title = { Text(richTooltipSubheadText) } + ) { + Text(richTooltipText) + } + }, + state = rememberTooltipState() + ) { + IconButton(onClick = { /* Icon button's click event */ }) { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = "Show more information" + ) + } + } +} +// [END android_compose_components_richtooltipexample] + +@Preview +@Composable +private fun RichTooltipSamplePreview() { + RichTooltipExample() +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_advancedrichtooltipexample] +@Composable +fun AdvancedRichTooltipExample( + modifier: Modifier = Modifier, + richTooltipSubheadText: String = "Custom Rich Tooltip", + richTooltipText: String = "Rich tooltips support multiple lines of informational text.", + richTooltipActionText: String = "Dismiss" +) { + val tooltipState = rememberTooltipState() + + TooltipBox( + modifier = modifier, + positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(), + tooltip = { + RichTooltip( + title = { Text(richTooltipSubheadText) }, + action = { + Row { + TextButton(onClick = { tooltipState.dismiss() }) { + Text(richTooltipActionText) + } + } + }, + caretSize = DpSize(32.dp, 16.dp) + ) { + Text(richTooltipText) + } + }, + state = tooltipState + ) { + IconButton(onClick = { tooltipState.dismiss() }) { + Icon( + imageVector = Icons.Filled.Camera, + contentDescription = "Open camera" + ) + } + } +} +// [END android_compose_components_advancedrichtooltipexample] + +@Preview +@Composable +private fun RichTooltipWithCustomCaretSamplePreview() { + AdvancedRichTooltipExample() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/GraphicsModifiersSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/GraphicsModifiersSnippets.kt index ef00faecc..0493012ed 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/GraphicsModifiersSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/GraphicsModifiersSnippets.kt @@ -16,15 +16,26 @@ package com.example.compose.snippets.graphics +import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text @@ -42,21 +53,33 @@ import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.compose.snippets.R +import kotlin.random.Random /* * Copyright 2022 The Android Open Source Project @@ -296,171 +319,170 @@ fun ModifierGraphicsLayerAlpha() { @Preview @Composable fun ModifierGraphicsLayerCompositingStrategy() { - /* Commented out until compositing Strategy is rolled out to production - // [START android_compose_graphics_modifiers_graphicsLayer_compositing_strategy] - - Image(painter = painterResource(id = R.drawable.dog), - contentDescription = "Dog", - contentScale = ContentScale.Crop, - modifier = Modifier - .size(120.dp) - .aspectRatio(1f) - .background( - Brush.linearGradient( - listOf( - Color(0xFFC5E1A5), - Color(0xFF80DEEA) - ) - ) - ) - .padding(8.dp) - .graphicsLayer { - compositingStrategy = CompositingStrategy.Offscreen - } - .drawWithCache { - val path = Path() - path.addOval( - Rect( - topLeft = Offset.Zero, - bottomRight = Offset(size.width, size.height) - ) - ) - onDrawWithContent { - clipPath(path) { - // this draws the actual image - if you don't call drawContent, it wont - // render anything - this@onDrawWithContent.drawContent() - } - val dotSize = size.width / 8f - // Clip a white border for the content - drawCircle( - Color.Black, - radius = dotSize, - center = Offset( - x = size.width - dotSize, - y = size.height - dotSize - ), - blendMode = BlendMode.Clear - ) - // draw the red circle indication - drawCircle( - Color(0xFFEF5350), radius = dotSize * 0.8f, - center = Offset( - x = size.width - dotSize, - y = size.height - dotSize - ) - ) - } - - } - ) - // [END android_compose_graphics_modifiers_graphicsLayer_compositing_strategy] - */ + // [START android_compose_graphics_modifiers_graphicsLayer_compositing_strategy] + + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = "Dog", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(120.dp) + .aspectRatio(1f) + .background( + Brush.linearGradient( + listOf( + Color(0xFFC5E1A5), + Color(0xFF80DEEA) + ) + ) + ) + .padding(8.dp) + .graphicsLayer { + compositingStrategy = CompositingStrategy.Offscreen + } + .drawWithCache { + val path = Path() + path.addOval( + Rect( + topLeft = Offset.Zero, + bottomRight = Offset(size.width, size.height) + ) + ) + onDrawWithContent { + clipPath(path) { + // this draws the actual image - if you don't call drawContent, it wont + // render anything + this@onDrawWithContent.drawContent() + } + val dotSize = size.width / 8f + // Clip a white border for the content + drawCircle( + Color.Black, + radius = dotSize, + center = Offset( + x = size.width - dotSize, + y = size.height - dotSize + ), + blendMode = BlendMode.Clear + ) + // draw the red circle indication + drawCircle( + Color(0xFFEF5350), radius = dotSize * 0.8f, + center = Offset( + x = size.width - dotSize, + y = size.height - dotSize + ) + ) + } + } + ) + // [END android_compose_graphics_modifiers_graphicsLayer_compositing_strategy] } -/* Commented out until compositing Strategy is rolled out to production + @Preview // [START android_compose_graphics_modifier_compositing_strategy_differences] @Composable fun CompositingStrategyExamples() { - Column( - modifier = Modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center) - ) { - /** Does not clip content even with a graphics layer usage here. By default, graphicsLayer - does not allocate + rasterize content into a separate layer but instead is used - for isolation. That is draw invalidations made outside of this graphicsLayer will not - re-record the drawing instructions in this composable as they have not changed **/ - Canvas( - modifier = Modifier - .graphicsLayer() - .size(100.dp) // Note size of 100 dp here - .border(2.dp, color = Color.Blue) - ) { - // ... and drawing a size of 200 dp here outside the bounds - drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx())) - } - - Spacer(modifier = Modifier.size(300.dp)) - - /** Clips content as alpha usage here creates an offscreen buffer to rasterize content - into first then draws to the original destination **/ - Canvas( - modifier = Modifier - // force to an offscreen buffer - .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) - .size(100.dp) // Note size of 100 dp here - .border(2.dp, color = Color.Blue) - ) { - /** ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the - content gets clipped **/ - drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx())) - } - } + Column( + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center) + ) { + // Does not clip content even with a graphics layer usage here. By default, graphicsLayer + // does not allocate + rasterize content into a separate layer but instead is used + // for isolation. That is draw invalidations made outside of this graphicsLayer will not + // re-record the drawing instructions in this composable as they have not changed + Canvas( + modifier = Modifier + .graphicsLayer() + .size(100.dp) // Note size of 100 dp here + .border(2.dp, color = Color.Blue) + ) { + // ... and drawing a size of 200 dp here outside the bounds + drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx())) + } + + Spacer(modifier = Modifier.size(300.dp)) + + /* Clips content as alpha usage here creates an offscreen buffer to rasterize content + into first then draws to the original destination */ + Canvas( + modifier = Modifier + // force to an offscreen buffer + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + .size(100.dp) // Note size of 100 dp here + .border(2.dp, color = Color.Blue) + ) { + /* ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the + content gets clipped */ + drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx())) + } + } } // [END android_compose_graphics_modifier_compositing_strategy_differences] - */ -/* Commented out until compositing Strategy is rolled out to production // [START android_compose_graphics_modifier_compositing_strategy_modulate_alpha] @Preview @Composable -fun CompositingStratgey_ModulateAlpha() { - Column( - modifier = Modifier - .fillMaxSize() - .padding(32.dp) - ) { - // Base drawing, no alpha applied - Canvas( - modifier = Modifier.size(200.dp) - ) { - drawSquares() - } - - Spacer(modifier = Modifier.size(36.dp)) - - // Alpha 0.5f applied to whole composable - Canvas(modifier = Modifier - .size(200.dp) - .graphicsLayer { - alpha = 0.5f - }) { - drawSquares() - } - Spacer(modifier = Modifier.size(36.dp)) - - // 0.75f alpha applied to each draw call when using ModulateAlpha - Canvas(modifier = Modifier - .size(200.dp) - .graphicsLayer { - compositingStrategy = CompositingStrategy.ModulateAlpha - alpha = 0.75f - }) { - drawSquares() - } - } +fun CompositingStrategy_ModulateAlpha() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(32.dp) + ) { + // Base drawing, no alpha applied + Canvas( + modifier = Modifier.size(200.dp) + ) { + drawSquares() + } + + Spacer(modifier = Modifier.size(36.dp)) + + // Alpha 0.5f applied to whole composable + Canvas( + modifier = Modifier + .size(200.dp) + .graphicsLayer { + alpha = 0.5f + } + ) { + drawSquares() + } + Spacer(modifier = Modifier.size(36.dp)) + + // 0.75f alpha applied to each draw call when using ModulateAlpha + Canvas( + modifier = Modifier + .size(200.dp) + .graphicsLayer { + compositingStrategy = CompositingStrategy.ModulateAlpha + alpha = 0.75f + } + ) { + drawSquares() + } + } } private fun DrawScope.drawSquares() { - val size = Size(100.dp.toPx(), 100.dp.toPx()) - drawRect(color = Red, size = size) - drawRect( - color = Purple, size = size, - topLeft = Offset(size.width / 4f, size.height / 4f) - ) - drawRect( - color = Yellow, size = size, - topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f) - ) + val size = Size(100.dp.toPx(), 100.dp.toPx()) + drawRect(color = Red, size = size) + drawRect( + color = Purple, size = size, + topLeft = Offset(size.width / 4f, size.height / 4f) + ) + drawRect( + color = Yellow, size = size, + topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f) + ) } val Purple = Color(0xFF7E57C2) val Yellow = Color(0xFFFFCA28) val Red = Color(0xFFEF5350) // [END android_compose_graphics_modifier_compositing_strategy_modulate_alpha] -*/ // [START android_compose_graphics_modifier_flipped] class FlippedModifier : DrawModifier { @@ -485,3 +507,168 @@ fun ModifierGraphicsFlippedUsage() { ) // [END android_compose_graphics_modifier_flipped_usage] } + +// [START android_compose_graphics_faded_edge_example] +@Composable +fun FadedEdgeBox(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + Box( + modifier = modifier + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + .drawWithContent { + drawContent() + drawRect( + brush = Brush.verticalGradient( + listOf(Color.Black, Color.Transparent) + ), + blendMode = BlendMode.DstIn + ) + } + ) { + content() + } +} +// [END android_compose_graphics_faded_edge_example] +@Preview +@Composable +private fun FadingLazyComments() { + FadedEdgeBox( + modifier = Modifier + .padding(32.dp) + .height(300.dp) + .fillMaxWidth() + ) { + LazyColumn { + items(listComments, key = { it.key }) { + ListCommentItem(it) + } + item { + Spacer(Modifier.height(100.dp)) + } + } + } +} + +@Composable +private fun ListCommentItem(it: Comment) { + Row(modifier = Modifier.padding(bottom = 8.dp)) { + val strokeWidthPx = with(LocalDensity.current) { + 2.dp.toPx() + } + Avatar(strokeWidth = strokeWidthPx, modifier = Modifier.size(48.dp)) { + Image( + painter = painterResource(id = it.avatar), + contentScale = ContentScale.Crop, + contentDescription = null + ) + } + Spacer(Modifier.width(6.dp)) + Text( + it.text, + fontSize = 20.sp, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } +} + +data class Comment( + val avatar: Int, + val text: String, + val key: Int = Random.nextInt() +) + +val listComments = listOf( + Comment(R.drawable.dog, "Woof 🐶"), + Comment(R.drawable.froyo, "I love ice cream..."), + Comment(R.drawable.donut, "Mmmm delicious"), + Comment(R.drawable.cupcake, "I love cupcakes"), + Comment(R.drawable.gingerbread, "🍪🍪❤️"), + Comment(R.drawable.eclair, "Where do I get the recipe?"), + Comment(R.drawable.froyo, "🍦The ice cream is BEST"), +) + +// [START android_compose_graphics_stacked_clipped_avatars] +@Composable +fun Avatar( + strokeWidth: Float, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + val stroke = remember(strokeWidth) { + Stroke(width = strokeWidth) + } + Box( + modifier = modifier + .drawWithContent { + drawContent() + drawCircle( + Color.Black, + size.minDimension / 2, + size.center, + style = stroke, + blendMode = BlendMode.Clear + ) + } + .clip(CircleShape) + ) { + content() + } +} + +@Preview +@Composable +private fun StackedAvatars() { + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.linearGradient( + colors = listOf( + Color.Magenta.copy(alpha = 0.5f), + Color.Blue.copy(alpha = 0.5f) + ) + ) + ) + ) { + val size = 80.dp + val strokeWidth = 2.dp + val strokeWidthPx = with(LocalDensity.current) { + strokeWidth.toPx() + } + val sizeModifier = Modifier.size(size) + val avatars = listOf( + R.drawable.cupcake, + R.drawable.donut, + R.drawable.eclair, + R.drawable.froyo, + R.drawable.gingerbread, + R.drawable.dog + ) + val width = ((size / 2) + strokeWidth * 2) * (avatars.size + 1) + Box( + modifier = Modifier + .size(width, size) + .graphicsLayer { + // Use an offscreen buffer as underdraw protection when + // using blendmodes that clear destination pixels + compositingStrategy = CompositingStrategy.Offscreen + } + .align(Alignment.Center), + ) { + var offset = 0.dp + for (avatar in avatars) { + Avatar( + strokeWidth = strokeWidthPx, + modifier = sizeModifier.offset(offset) + ) { + Image( + painter = painterResource(id = avatar), + contentScale = ContentScale.Crop, + contentDescription = null + ) + } + offset += size / 2 + } + } + } +} +// [END android_compose_graphics_stacked_clipped_avatars] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/images/AnimateImageSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/images/AnimateImageSnippets.kt new file mode 100644 index 000000000..2eab0220d --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/images/AnimateImageSnippets.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.images + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp + +// [START android_compose_images_imageresizeonscrollexample] +@Composable +fun ImageResizeOnScrollExample( + modifier: Modifier = Modifier, + maxImageSize: Dp = 300.dp, + minImageSize: Dp = 100.dp +) { + var currentImageSize by remember { mutableStateOf(maxImageSize) } + var imageScale by remember { mutableFloatStateOf(1f) } + + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + // Calculate the change in image size based on scroll delta + val delta = available.y + val newImageSize = currentImageSize + delta.dp + val previousImageSize = currentImageSize + + // Constrain the image size within the allowed bounds + currentImageSize = newImageSize.coerceIn(minImageSize, maxImageSize) + val consumed = currentImageSize - previousImageSize + + // Calculate the scale for the image + imageScale = currentImageSize / maxImageSize + + // Return the consumed scroll amount + return Offset(0f, consumed.value) + } + } + } + + Box(Modifier.nestedScroll(nestedScrollConnection)) { + LazyColumn( + Modifier + .fillMaxWidth() + .padding(15.dp) + .offset { + IntOffset(0, currentImageSize.roundToPx()) + } + ) { + // Placeholder list items + items(100, key = { it }) { + Text( + text = "Item: $it", + style = MaterialTheme.typography.bodyLarge + ) + } + } + + Image( + painter = ColorPainter(Color.Red), + contentDescription = "Red color image", + Modifier + .size(maxImageSize) + .align(Alignment.TopCenter) + .graphicsLayer { + scaleX = imageScale + scaleY = imageScale + // Center the image vertically as it scales + translationY = -(maxImageSize.toPx() - currentImageSize.toPx()) / 2f + } + ) + } +} +// [END android_compose_images_imageresizeonscrollexample] + +@Preview(showBackground = true) +@Composable +private fun ImageSizeOnScrollScreenPreview() { + ImageResizeOnScrollExample() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt b/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt index 79083bd3b..6e6c84019 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt @@ -18,56 +18,45 @@ package com.example.compose.snippets.landing import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.example.compose.snippets.navigation.Destination +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LandingScreen( navigate: (Destination) -> Unit ) { - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(24.dp), - ) { - Text( - modifier = Modifier.fillMaxWidth(), - style = TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - ), - text = "Android snippets", - ) - Text( - text = "Use the following buttons to view a selection of the snippets used in the Android documentation." - ) - NavigationItems { navigate(it) } + Scaffold( + topBar = { + TopAppBar(title = { + Text(text = "Android snippets",) + }) + } + ) { padding -> + NavigationItems(modifier = Modifier.padding(padding)) { navigate(it) } } } @Composable -fun NavigationItems(navigate: (Destination) -> Unit) { +fun NavigationItems( + modifier: Modifier = Modifier, + navigate: (Destination) -> Unit +) { LazyColumn( - modifier = Modifier + modifier = modifier .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -84,14 +73,14 @@ fun NavigationItems(navigate: (Destination) -> Unit) { @Composable fun NavigationItem(destination: Destination, onClick: () -> Unit) { - Box( + ListItem( + headlineContent = { + Text(destination.title) + }, modifier = Modifier .heightIn(min = 48.dp) .clickable { onClick() } - ) { - Text(destination.title) - HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) - } + ) } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AdaptiveLayoutSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AdaptiveLayoutSnippets.kt index 7980eae05..9447582cf 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AdaptiveLayoutSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/AdaptiveLayoutSnippets.kt @@ -28,7 +28,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.unit.dp -import androidx.window.core.layout.WindowHeightSizeClass import androidx.window.core.layout.WindowSizeClass /* @@ -51,10 +50,10 @@ import androidx.window.core.layout.WindowSizeClass fun MyApp( windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass ) { - // Perform logic on the size class to decide whether to show the top app bar. - val showTopAppBar = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.COMPACT + // Decide whether to show the top app bar based on window size class. + val showTopAppBar = windowSizeClass.isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND) - // MyScreen knows nothing about window sizes, and performs logic based on a Boolean flag. + // MyScreen logic is based on the showTopAppBar boolean flag. MyScreen( showTopAppBar = showTopAppBar, /* ... */ diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt index 4599fd392..77568686d 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/InsetsSnippets.kt @@ -22,7 +22,6 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -30,8 +29,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.only @@ -49,7 +46,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -124,7 +120,6 @@ fun ConsumedFromPaddingSnippet() { @Preview @Composable fun M3SupportScaffoldSnippet() { - val colors = listOf(Color.Red, Color.Blue, Color.Yellow) // [START android_compose_insets_m3_scaffold] Scaffold { innerPadding -> // innerPadding contains inset information for you to use and apply @@ -133,14 +128,7 @@ fun M3SupportScaffoldSnippet() { modifier = Modifier.consumeWindowInsets(innerPadding), contentPadding = innerPadding ) { - items(count = 100) { - Box( - Modifier - .fillMaxWidth() - .height(50.dp) - .background(colors[it % colors.size]) - ) - } + // .. } } // [END android_compose_insets_m3_scaffold] diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/IntrinsicSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/IntrinsicSnippets.kt index 9bc39b292..edca298d7 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/IntrinsicSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/IntrinsicSnippets.kt @@ -39,10 +39,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -71,7 +71,7 @@ private object IntrinsicsSnippet1 { .wrapContentWidth(Alignment.Start), text = text1 ) - HorizontalDivider( + VerticalDivider( color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp) ) @@ -111,7 +111,7 @@ private object IntrinsicsSnippet2 { .wrapContentWidth(Alignment.Start), text = text1 ) - HorizontalDivider( + VerticalDivider( color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp) ) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt index ec8e84cbf..899163367 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt @@ -20,7 +20,12 @@ package com.example.compose.snippets.layouts import android.util.Log import androidx.compose.foundation.Image +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -50,6 +55,8 @@ import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment @@ -58,6 +65,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp @@ -65,6 +73,7 @@ import androidx.compose.ui.util.lerp import coil.compose.rememberAsyncImagePainter import com.example.compose.snippets.util.rememberRandomSampleImageUrl import kotlin.math.absoluteValue +import kotlinx.coroutines.delay import kotlinx.coroutines.launch /* @@ -83,6 +92,18 @@ import kotlinx.coroutines.launch * limitations under the License. */ +@Composable +fun PagerExamples() { + AutoAdvancePager( + listOf( + Color.Red, + Color.Gray, + Color.Green, + Color.White + ) + ) +} + @Preview @Composable fun HorizontalPagerSample() { @@ -392,6 +413,98 @@ fun PagerIndicator() { } } +// [START android_compose_autoadvancepager] +@Composable +fun AutoAdvancePager(pageItems: List, modifier: Modifier = Modifier) { + Box(modifier = Modifier.fillMaxSize()) { + val pagerState = rememberPagerState(pageCount = { pageItems.size }) + val pagerIsDragged by pagerState.interactionSource.collectIsDraggedAsState() + + val pageInteractionSource = remember { MutableInteractionSource() } + val pageIsPressed by pageInteractionSource.collectIsPressedAsState() + + // Stop auto-advancing when pager is dragged or one of the pages is pressed + val autoAdvance = !pagerIsDragged && !pageIsPressed + + if (autoAdvance) { + LaunchedEffect(pagerState, pageInteractionSource) { + while (true) { + delay(2000) + val nextPage = (pagerState.currentPage + 1) % pageItems.size + pagerState.animateScrollToPage(nextPage) + } + } + } + + HorizontalPager( + state = pagerState + ) { page -> + Text( + text = "Page: $page", + textAlign = TextAlign.Center, + modifier = modifier + .fillMaxSize() + .background(pageItems[page]) + .clickable( + interactionSource = pageInteractionSource, + indication = LocalIndication.current + ) { + // Handle page click + } + .wrapContentSize(align = Alignment.Center) + ) + } + + PagerIndicator(pageItems.size, pagerState.currentPage) + } +} +// [END android_compose_autoadvancepager] + +@Preview +@Composable +private fun AutoAdvancePagerPreview() { + val pageItems: List = listOf( + Color.Red, + Color.Gray, + Color.Green, + Color.White + ) + AutoAdvancePager(pageItems = pageItems) +} + +// [START android_compose_pagerindicator] +@Composable +fun PagerIndicator(pageCount: Int, currentPageIndex: Int, modifier: Modifier = Modifier) { + Box(modifier = Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.Center + ) { + repeat(pageCount) { iteration -> + val color = if (currentPageIndex == iteration) Color.DarkGray else Color.LightGray + Box( + modifier = modifier + .padding(2.dp) + .clip(CircleShape) + .background(color) + .size(16.dp) + ) + } + } + } +} +// [END android_compose_pagerindicator] + +@Preview +@Composable +private fun PagerIndicatorPreview() { + PagerIndicator(pageCount = 4, currentPageIndex = 1) +} + // [START android_compose_pager_custom_page_size] private val threePagesPerViewport = object : PageSize { override fun Density.calculateMainAxisPageSize( diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/lists/AnimatedOrderedList.kt b/compose/snippets/src/main/java/com/example/compose/snippets/lists/AnimatedOrderedList.kt new file mode 100644 index 000000000..4d60e66b2 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/lists/AnimatedOrderedList.kt @@ -0,0 +1,217 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.lists + +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class AnimatedOrderedListViewModel : ViewModel() { + private val _data = listOf("One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten") + private val _displayedItems: MutableStateFlow> = MutableStateFlow(_data) + val displayedItems: StateFlow> = _displayedItems + + fun resetOrder() { + _displayedItems.value = _data.filter { it in _displayedItems.value } + } + + fun sortAlphabetically() { + _displayedItems.value = _displayedItems.value.sortedBy { it } + } + + fun sortByLength() { + _displayedItems.value = _displayedItems.value.sortedBy { it.length } + } + + fun addItem() { + // Avoid duplicate items + val remainingItems = _data.filter { it !in _displayedItems.value } + if (remainingItems.isNotEmpty()) _displayedItems.value += remainingItems.first() + } + + fun removeItem() { + _displayedItems.value = _displayedItems.value.dropLast(1) + } +} + +@Composable +fun AnimatedOrderedListScreen( + viewModel: AnimatedOrderedListViewModel, + modifier: Modifier = Modifier, +) { + val displayedItems by viewModel.displayedItems.collectAsStateWithLifecycle() + + ListAnimatedItemsExample( + displayedItems, + onAddItem = viewModel::addItem, + onRemoveItem = viewModel::removeItem, + resetOrder = viewModel::resetOrder, + onSortAlphabetically = viewModel::sortAlphabetically, + onSortByLength = viewModel::sortByLength, + modifier = modifier + ) +} + +// [START android_compose_layouts_list_listanimateditems] +@Composable +fun ListAnimatedItems( + items: List, + modifier: Modifier = Modifier +) { + LazyColumn(modifier) { + // Use a unique key per item, so that animations work as expected. + items(items, key = { it }) { + ListItem( + headlineContent = { Text(it) }, + modifier = Modifier + .animateItem( + // Optionally add custom animation specs + ) + .fillParentMaxWidth() + .padding(horizontal = 8.dp, vertical = 0.dp), + ) + } + } +} +// [END android_compose_layouts_list_listanimateditems] + +// [START android_compose_layouts_list_listanimateditemsexample] +@Composable +private fun ListAnimatedItemsExample( + data: List, + modifier: Modifier = Modifier, + onAddItem: () -> Unit = {}, + onRemoveItem: () -> Unit = {}, + resetOrder: () -> Unit = {}, + onSortAlphabetically: () -> Unit = {}, + onSortByLength: () -> Unit = {}, +) { + val canAddItem = data.size < 10 + val canRemoveItem = data.isNotEmpty() + + Scaffold(modifier) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + // Buttons that change the value of displayedItems. + AddRemoveButtons(canAddItem, canRemoveItem, onAddItem, onRemoveItem) + OrderButtons(resetOrder, onSortAlphabetically, onSortByLength) + + // List that displays the values of displayedItems. + ListAnimatedItems(data) + } + } +} +// [END android_compose_layouts_list_listanimateditemsexample] + +// [START android_compose_layouts_list_addremovebuttons] +@Composable +private fun AddRemoveButtons( + canAddItem: Boolean, + canRemoveItem: Boolean, + onAddItem: () -> Unit, + onRemoveItem: () -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Button(enabled = canAddItem, onClick = onAddItem) { + Text("Add Item") + } + Spacer(modifier = Modifier.padding(25.dp)) + Button(enabled = canRemoveItem, onClick = onRemoveItem) { + Text("Delete Item") + } + } +} +// [END android_compose_layouts_list_addremovebuttons] + +// [START android_compose_layouts_list_orderbuttons] +@Composable +private fun OrderButtons( + resetOrder: () -> Unit, + orderAlphabetically: () -> Unit, + orderByLength: () -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + var selectedIndex by remember { mutableIntStateOf(0) } + val options = listOf("Reset", "Alphabetical", "Length") + + SingleChoiceSegmentedButtonRow { + options.forEachIndexed { index, label -> + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = options.size + ), + onClick = { + Log.d("AnimatedOrderedList", "selectedIndex: $selectedIndex") + selectedIndex = index + when (options[selectedIndex]) { + "Reset" -> resetOrder() + "Alphabetical" -> orderAlphabetically() + "Length" -> orderByLength() + } + }, + selected = index == selectedIndex + ) { + Text(label) + } + } + } + } +} +// [END android_compose_layouts_list_orderbuttons] + +@Preview +@Composable +fun AnimatedOrderedListScreenPreview() { + val viewModel = remember { AnimatedOrderedListViewModel() } + AnimatedOrderedListScreen(viewModel) +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt index 9e7110503..8d86b28b4 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt @@ -23,7 +23,8 @@ enum class Destination(val route: String, val title: String) { ComponentsExamples("topComponents", "Top Compose Components"), ScreenshotExample("screenshotExample", "Screenshot Examples"), ShapesExamples("shapesExamples", "Shapes Examples"), - SharedElementExamples("sharedElement", "Shared elements") + SharedElementExamples("sharedElement", "Shared elements"), + PagerExamples("pagerExamples", "Pager examples") } // Enum class for compose components navigation screen. @@ -44,4 +45,11 @@ enum class TopComponentsDestination(val route: String, val title: String) { PartialBottomSheet("partialBottomSheets", "Partial Bottom Sheet"), TimePickerExamples("timePickerExamples", "Time Pickers"), DatePickerExamples("datePickerExamples", "Date Pickers"), + CarouselExamples("carouselExamples", "Carousel"), + MenusExample("menusExamples", "Menus"), + TooltipExamples("tooltipExamples", "Tooltips"), + NavigationDrawerExamples("navigationDrawerExamples", "Navigation drawer"), + SegmentedButtonExamples("segmentedButtonExamples", "Segmented button"), + SwipeToDismissBoxExamples("swipeToDismissBoxExamples", "Swipe to dismiss box examples"), + SearchBarExamples("searchBarExamples", "Search bar") } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/predictiveback/PredictiveBackSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/predictiveback/PredictiveBackSnippets.kt new file mode 100644 index 000000000..fb94e1186 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/predictiveback/PredictiveBackSnippets.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.predictiveback + +import android.os.SystemClock +import androidx.activity.BackEventCompat +import androidx.activity.compose.PredictiveBackHandler +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.input.pointer.util.VelocityTracker +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import kotlin.coroutines.cancellation.CancellationException +import kotlinx.coroutines.flow.Flow +import kotlinx.serialization.Serializable + +@Serializable data object Home +@Serializable data object Settings + +@Composable +private fun PredictiveBackOverrideExit( + modifier: Modifier, +) { + val navController = rememberNavController() + + // [START android_compose_predictiveback_navhost] + NavHost( + navController = navController, + startDestination = Home, + popExitTransition = { + scaleOut( + targetScale = 0.9f, + transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0.5f) + ) + }, + popEnterTransition = { + EnterTransition.None + }, + modifier = modifier, + ) + // [END android_compose_predictiveback_navhost] + { + composable { + HomeScreen( + modifier = modifier, + navController = navController, + ) + } + composable { + SettingsScreen( + modifier = modifier, + navController = navController, + ) + } + } +} + +@Composable +private fun HomeScreen( + modifier: Modifier = Modifier, + navController: NavHostController +) { +} + +@Composable +private fun SettingsScreen( + modifier: Modifier = Modifier, + navController: NavHostController +) { +} + +@Composable +private fun PredictiveBackHandlerBasicExample() { + + var boxScale by remember { mutableFloatStateOf(1F) } + + Box( + modifier = Modifier + .fillMaxSize(boxScale) + .background(Color.Blue) + ) + + // [START android_compose_predictivebackhandler_basic] + PredictiveBackHandler(true) { progress: Flow -> + // code for gesture back started + try { + progress.collect { backEvent -> + // code for progress + boxScale = 1F - (1F * backEvent.progress) + } + // code for completion + boxScale = 0F + } catch (e: CancellationException) { + // code for cancellation + boxScale = 1F + } + } + // [END android_compose_predictivebackhandler_basic] +} + +@Composable +private fun PredictiveBackHandlerManualProgress() { + + Surface( + modifier = Modifier.fillMaxSize() + ) { + var drawerState by remember { + mutableStateOf(DrawerState.Closed) + } + + val translationX = remember { + Animatable(0f) + } + + val drawerWidth = with(LocalDensity.current) { + DrawerWidth.toPx() + } + translationX.updateBounds(0f, drawerWidth) + + val coroutineScope = rememberCoroutineScope() + + suspend fun closeDrawer(velocity: Float = 0f) { + translationX.animateTo(targetValue = 0f, initialVelocity = velocity) + drawerState = DrawerState.Closed + } + suspend fun openDrawer(velocity: Float = 0f) { + translationX.animateTo(targetValue = drawerWidth, initialVelocity = velocity) + drawerState = DrawerState.Open + } + + val velocityTracker = remember { + VelocityTracker() + } + + // [START android_compose_predictivebackhandler_manualprogress] + PredictiveBackHandler(drawerState == DrawerState.Open) { progress -> + try { + progress.collect { backEvent -> + val targetSize = (drawerWidth - (drawerWidth * backEvent.progress)) + translationX.snapTo(targetSize) + velocityTracker.addPosition( + SystemClock.uptimeMillis(), + Offset(backEvent.touchX, backEvent.touchY) + ) + } + closeDrawer(velocityTracker.calculateVelocity().x) + } catch (e: CancellationException) { + openDrawer(velocityTracker.calculateVelocity().x) + } + velocityTracker.resetTracking() + } + // [END android_compose_predictivebackhandler_manualprogress] + } +} + +private enum class DrawerState { + Open, + Closed +} + +private val DrawerWidth = 300.dp diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/FilterText.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/FilterText.kt new file mode 100644 index 000000000..6967ea4f8 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/FilterText.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.text + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ListItem +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +// [START android_compose_text_filtertextviewmodel] +class FilterTextViewModel : ViewModel() { + private val items = listOf( + "Cupcake", + "Donut", + "Eclair", + "Froyo", + "Gingerbread", + "Honeycomb", + "Ice Cream Sandwich" + ) + + private val _filteredItems = MutableStateFlow(items) + var filteredItems: StateFlow> = _filteredItems + + fun filterText(input: String) { + // This filter returns the full items list when input is an empty string. + _filteredItems.value = items.filter { it.contains(input, ignoreCase = true) } + } +} +// [END android_compose_text_filtertextviewmodel] + +// [START android_compose_text_filtertextview] +@Composable +fun FilterTextView(modifier: Modifier = Modifier, viewModel: FilterTextViewModel = viewModel()) { + val filteredItems by viewModel.filteredItems.collectAsStateWithLifecycle() + var text by rememberSaveable { mutableStateOf("") } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(all = 10.dp) + ) { + OutlinedTextField( + value = text, + onValueChange = { + text = it + viewModel.filterText(text) + }, + label = { Text("Filter Text") }, + modifier = Modifier.fillMaxWidth() + ) + + LazyColumn { + items( + count = filteredItems.size, + key = { index -> filteredItems[index] } + ) { + ListItem( + headlineContent = { Text(filteredItems[it]) }, + modifier = Modifier + .fillParentMaxWidth() + .padding(10.dp) + ) + } + } + } +} +// [END android_compose_text_filtertextview] + +@Preview(showBackground = true) +@Composable +private fun FilterTextViewPreview() { + FilterTextView() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/text/HtmlStyling.kt b/compose/snippets/src/main/java/com/example/compose/snippets/text/HtmlStyling.kt new file mode 100644 index 000000000..607fdf462 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/text/HtmlStyling.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.text + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview + +// [START android_compose_text_annotatedhtmlstringwithlink] +@Composable +fun AnnotatedHtmlStringWithLink( + modifier: Modifier = Modifier, + htmlText: String = """ +

Jetpack Compose

+

+ Build better apps faster with Jetpack Compose +

+ """.trimIndent() +) { + Text( + AnnotatedString.fromHtml( + htmlText, + linkStyles = TextLinkStyles( + style = SpanStyle( + textDecoration = TextDecoration.Underline, + fontStyle = FontStyle.Italic, + color = Color.Blue + ) + ) + ), + modifier + ) +} +// [END android_compose_text_annotatedhtmlstringwithlink] + +@Preview(showBackground = true) +@Composable +private fun AnnotatedHtmlStringWithLinkPreview() { + AnnotatedHtmlStringWithLink() +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt index 82b5b2d1e..2fce7f0aa 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Android Open Source Project + * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,20 +14,17 @@ * limitations under the License. */ -@file:Suppress("DEPRECATION_ERROR") - package com.example.compose.snippets.touchinput.focus -import androidx.compose.foundation.Indication -import androidx.compose.foundation.IndicationInstance +import androidx.compose.foundation.IndicationNodeFactory import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.focusGroup import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.FocusInteraction import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -42,7 +39,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.runtime.Composable -import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -70,9 +66,13 @@ import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.invalidateDraw import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch @Preview @Composable @@ -436,45 +436,64 @@ private fun ReactToFocus() { } // [START android_compose_touchinput_focus_advanced_cues] -private class MyHighlightIndicationInstance(isEnabledState: State) : - IndicationInstance { - private val isEnabled by isEnabledState - override fun ContentDrawScope.drawIndication() { +private class MyHighlightIndicationNode(private val interactionSource: InteractionSource) : + Modifier.Node(), DrawModifierNode { + private var isFocused = false + + override fun onAttach() { + coroutineScope.launch { + var focusCount = 0 + interactionSource.interactions.collect { interaction -> + when (interaction) { + is FocusInteraction.Focus -> focusCount++ + is FocusInteraction.Unfocus -> focusCount-- + } + val focused = focusCount > 0 + if (isFocused != focused) { + isFocused = focused + invalidateDraw() + } + } + } + } + + override fun ContentDrawScope.draw() { drawContent() - if (isEnabled) { + if (isFocused) { drawRect(size = size, color = Color.White, alpha = 0.2f) } } } + // [END android_compose_touchinput_focus_advanced_cues] // [START android_compose_touchinput_focus_indication] -class MyHighlightIndication : Indication { - @Composable - override fun rememberUpdatedInstance(interactionSource: InteractionSource): - IndicationInstance { - val isFocusedState = interactionSource.collectIsFocusedAsState() - return remember(interactionSource) { - MyHighlightIndicationInstance(isEnabledState = isFocusedState) - } +object MyHighlightIndication : IndicationNodeFactory { + override fun create(interactionSource: InteractionSource): DelegatableNode { + return MyHighlightIndicationNode(interactionSource) } + + override fun hashCode(): Int = -1 + + override fun equals(other: Any?) = other === this } // [END android_compose_touchinput_focus_indication] @Composable private fun ApplyIndication() { // [START android_compose_touchinput_focus_apply_indication] - val highlightIndication = remember { MyHighlightIndication() } var interactionSource = remember { MutableInteractionSource() } Card( modifier = Modifier .clickable( interactionSource = interactionSource, - indication = highlightIndication, + indication = MyHighlightIndication, enabled = true, onClick = { } ) - ) {} + ) { + Text("hello") + } // [END android_compose_touchinput_focus_apply_indication] } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt new file mode 100644 index 000000000..610143cec --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/KeyboardShortcutsHelper.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.touchinput.keyboardinput + +import android.os.Build +import android.os.Bundle +import android.view.KeyEvent +import android.view.KeyboardShortcutGroup +import android.view.KeyboardShortcutInfo +import android.view.Menu +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.annotation.RequiresApi +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text + +class MainActivity : ComponentActivity() { + // Activity codes such as overridden onStart method. + + @RequiresApi(Build.VERSION_CODES.N) + // [START android_compose_keyboard_shortcuts_helper] + override fun onProvideKeyboardShortcuts( + data: MutableList?, + menu: Menu?, + deviceId: Int + ) { + val shortcutGroup = + KeyboardShortcutGroup( + "Cursor movement", + listOf( + KeyboardShortcutInfo("Up", KeyEvent.KEYCODE_P, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Down", KeyEvent.KEYCODE_N, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Forward", KeyEvent.KEYCODE_F, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Backward", KeyEvent.KEYCODE_B, KeyEvent.META_CTRL_ON), + ) + ) + data?.add(shortcutGroup) + } + // [END android_compose_keyboard_shortcuts_helper] +} + +class AnotherActivity : ComponentActivity() { + + @RequiresApi(Build.VERSION_CODES.N) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + MaterialTheme { + // [START android_compose_keyboard_shortcuts_helper_request] + val activity = LocalActivity.current + + Button( + onClick = { + activity?.requestShowKeyboardShortcuts() + } + ) { + Text(text = "Show keyboard shortcuts") + } + // [END android_compose_keyboard_shortcuts_helper_request] + } + } + } + + @RequiresApi(Build.VERSION_CODES.N) + // [START android_compose_keyboard_shortcuts_helper_with_groups] + override fun onProvideKeyboardShortcuts( + data: MutableList?, + menu: Menu?, + deviceId: Int + ) { + val cursorMovement = KeyboardShortcutGroup( + "Cursor movement", + listOf( + KeyboardShortcutInfo("Up", KeyEvent.KEYCODE_P, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Down", KeyEvent.KEYCODE_N, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Forward", KeyEvent.KEYCODE_F, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo("Backward", KeyEvent.KEYCODE_B, KeyEvent.META_CTRL_ON), + ) + ) + + val messageEdit = KeyboardShortcutGroup( + "Message editing", + listOf( + KeyboardShortcutInfo("Select All", KeyEvent.KEYCODE_A, KeyEvent.META_CTRL_ON), + KeyboardShortcutInfo( + "Send a message", + KeyEvent.KEYCODE_ENTER, + KeyEvent.META_SHIFT_ON + ) + ) + ) + + data?.add(cursorMovement) + data?.add(messageEdit) + } + // [END android_compose_keyboard_shortcuts_helper_with_groups] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/commands.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/commands.kt new file mode 100644 index 000000000..d0c08c544 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/keyboardinput/commands.kt @@ -0,0 +1,337 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.touchinput.keyboardinput + +import android.content.Context +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.onFocusEvent +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isAltPressed +import androidx.compose.ui.input.key.isCtrlPressed +import androidx.compose.ui.input.key.isMetaPressed +import androidx.compose.ui.input.key.isShiftPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp + +@Suppress("unused") +@Composable +fun CommandsScreen() { + val context = LocalContext.current + var playerState by rememberSaveable { mutableStateOf(false) } + + val doSomething = { + showToast(context, "Doing something") + } + + val doAnotherThing = { + showToast(context, "Doing another thing") + } + + val togglePlayPause = { + playerState = !playerState + val message = if (playerState) { + "Playing" + } else { + "Paused" + } + showToast(context, message) + } + + val actionC = { + showToast(context, "Action C") + } + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(8.dp) + ) { + KeyEvents(doSomething) + ModifierKeys(doSomething) + SpacebarAndEnterKeyTriggersClickEvents(togglePlayPause) + UnconsumedKeyEvents(doSomething, doAnotherThing, actionC) + PreviewKeyEvents() + InterceptKeyEvents( + doSomething, + { keyEvent -> + showToast(context, "onPreviewKeyEvent: ${keyEvent.key.keyCode}") + }, + { keyEvent -> + showToast(context, "onKeyEvent: ${keyEvent.key.keyCode}") + } + ) + } +} + +fun showToast(context: Context, message: String) { + val toast = Toast.makeText(context, message, Toast.LENGTH_SHORT) + toast.show() +} + +@Composable +private fun BoxWithFocusIndication( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit +) { + var isFocused by remember { mutableStateOf(false) } + val backgroundColor = if (isFocused) { + MaterialTheme.colorScheme.surfaceVariant + } else { + MaterialTheme.colorScheme.surface + } + Box( + modifier = modifier + .onFocusEvent { + isFocused = it.isFocused + } + .background(backgroundColor), + content = content + ) +} + +@Composable +private fun KeyEvents( + doSomething: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier) { + // [START android_compose_touchinput_keyboardinput_keyevents] + Box( + modifier = Modifier + .onKeyEvent { + if ( + it.type == KeyEventType.KeyUp && + it.key == Key.S + ) { + doSomething() + true + } else { + false + } + } + .focusable() + ) { + Text("Press S key") + } + // [END android_compose_touchinput_keyboardinput_keyevents] + } +} + +@Composable +private fun ModifierKeys( + doSomething: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier = modifier) { + // [START android_compose_touchinput_keyboardinput_modifierkeys] + Box( + modifier = Modifier + .focusable() + .onKeyEvent { + if ( + it.type == KeyEventType.KeyUp && + it.key == Key.S && + !it.isAltPressed && + !it.isCtrlPressed && + !it.isMetaPressed && + !it.isShiftPressed + ) { + doSomething() + true + } else { + false + } + } + ) { + Text("Press S key with a modifier key") + } + // [END android_compose_touchinput_keyboardinput_modifierkeys] + } +} + +@Composable +private fun SpacebarAndEnterKeyTriggersClickEvents( + togglePausePlay: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier = modifier) { + // [START android_compose_touchinput_keyboardinput_spacebar] + MediaPlayer(modifier = Modifier.clickable { togglePausePlay() }) + // [END android_compose_touchinput_keyboardinput_spacebar] + } +} + +@Composable +private fun MediaPlayer( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(200.dp) + .background(MaterialTheme.colorScheme.primaryContainer) + ) +} + +@Composable +private fun UnconsumedKeyEvents( + actionA: () -> Unit, + actionB: () -> Unit, + actionC: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier = modifier) { + // [START android_compose_touchinput_keyboardinput_unconsumedkeyevents] + OuterComponent( + modifier = Modifier.onKeyEvent { + when { + it.type == KeyEventType.KeyUp && it.key == Key.S -> { + actionB() // This function is never called. + true + } + + it.type == KeyEventType.KeyUp && it.key == Key.D -> { + actionC() + true + } + + else -> false + } + } + ) { + InnerComponent( + modifier = Modifier.onKeyEvent { + if (it.type == KeyEventType.KeyUp && it.key == Key.S) { + actionA() + true + } else { + false + } + } + ) + } + // [END android_compose_touchinput_keyboardinput_unconsumedkeyevents] + } +} + +@Composable +private fun OuterComponent( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) = + Box(content = content, modifier = modifier.focusable()) + +@Composable +private fun InnerComponent( + modifier: Modifier = Modifier +) { + Card(modifier = modifier.focusable()) { + Text("Press S key or D key", modifier = Modifier.padding(16.dp)) + } +} + +@Composable +private fun PreviewKeyEvents() { + // [START android_compose_touchinput_keyboardinput_previewkeyevents] + val focusManager = LocalFocusManager.current + var textFieldValue by remember { mutableStateOf(TextFieldValue()) } + + TextField( + textFieldValue, + onValueChange = { + textFieldValue = it + }, + modifier = Modifier.onPreviewKeyEvent { + if (it.type == KeyEventType.KeyUp && it.key == Key.Tab) { + focusManager.moveFocus(FocusDirection.Next) + true + } else { + false + } + } + ) + // [END android_compose_touchinput_keyboardinput_previewkeyevents] +} + +@Composable +private fun InterceptKeyEvents( + previewSKey: () -> Unit, + actionForPreview: (KeyEvent) -> Unit, + actionForKeyEvent: (KeyEvent) -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithFocusIndication(modifier = modifier) { + // [START android_compose_touchinput_keyboardinput_interceptevents] + Column( + modifier = Modifier.onPreviewKeyEvent { + if (it.key == Key.S) { + previewSKey() + true + } else { + false + } + } + ) { + Box( + modifier = Modifier + .focusable() + .onPreviewKeyEvent { + actionForPreview(it) + false + } + .onKeyEvent { + actionForKeyEvent(it) + true + } + ) { + Text("Press any key") + } + } + // [END android_compose_touchinput_keyboardinput_interceptevents] + } +} diff --git a/compose/snippets/src/main/res/drawable-nodpi/cheese_1.jpg b/compose/snippets/src/main/res/drawable-nodpi/cheese_1.jpg new file mode 100644 index 000000000..65be51c6b Binary files /dev/null and b/compose/snippets/src/main/res/drawable-nodpi/cheese_1.jpg differ diff --git a/compose/snippets/src/main/res/drawable-nodpi/cheese_2.jpg b/compose/snippets/src/main/res/drawable-nodpi/cheese_2.jpg new file mode 100644 index 000000000..ae83e1ac6 Binary files /dev/null and b/compose/snippets/src/main/res/drawable-nodpi/cheese_2.jpg differ diff --git a/compose/snippets/src/main/res/drawable-nodpi/cheese_3.jpg b/compose/snippets/src/main/res/drawable-nodpi/cheese_3.jpg new file mode 100644 index 000000000..362ef3874 Binary files /dev/null and b/compose/snippets/src/main/res/drawable-nodpi/cheese_3.jpg differ diff --git a/compose/snippets/src/main/res/drawable-nodpi/cheese_4.jpg b/compose/snippets/src/main/res/drawable-nodpi/cheese_4.jpg new file mode 100644 index 000000000..052ef4c72 Binary files /dev/null and b/compose/snippets/src/main/res/drawable-nodpi/cheese_4.jpg differ diff --git a/compose/snippets/src/main/res/drawable-nodpi/cheese_5.jpg b/compose/snippets/src/main/res/drawable-nodpi/cheese_5.jpg new file mode 100644 index 000000000..137a7f3a1 Binary files /dev/null and b/compose/snippets/src/main/res/drawable-nodpi/cheese_5.jpg differ diff --git a/compose/snippets/src/main/res/values/strings.xml b/compose/snippets/src/main/res/values/strings.xml index faf8fd472..02254e29a 100644 --- a/compose/snippets/src/main/res/values/strings.xml +++ b/compose/snippets/src/main/res/values/strings.xml @@ -53,4 +53,6 @@ Shopping Profile This is just a placeholder. + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b3b72fb4e..fdc4663df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,59 +1,64 @@ [versions] accompanist = "0.36.0" -androidGradlePlugin = "8.6.1" -androidx-activity-compose = "1.9.2" +androidGradlePlugin = "8.8.0" +androidx-activity-compose = "1.10.0" androidx-appcompat = "1.7.0" -androidx-compose-bom = "2024.09.02" +androidx-compose-bom = "2025.01.01" androidx-compose-ui-test = "1.7.0-alpha08" -androidx-constraintlayout = "2.1.4" -androidx-constraintlayout-compose = "1.0.1" +androidx-constraintlayout = "2.2.0" +androidx-constraintlayout-compose = "1.1.0" androidx-coordinator-layout = "1.2.0" -androidx-corektx = "1.13.1" +androidx-corektx = "1.15.0" androidx-emoji2-views = "1.5.0" -androidx-fragment-ktx = "1.8.3" -androidx-glance-appwidget = "1.1.0" -androidx-lifecycle-compose = "2.8.6" -androidx-lifecycle-runtime-compose = "2.8.6" -androidx-navigation = "2.8.1" -androidx-paging = "3.3.2" +androidx-fragment-ktx = "1.8.5" +androidx-glance-appwidget = "1.1.1" +androidx-lifecycle-compose = "2.8.7" +androidx-lifecycle-runtime-compose = "2.8.7" +androidx-navigation = "2.8.6" +androidx-paging = "3.3.5" androidx-test = "1.6.1" androidx-test-espresso = "3.6.1" -androidx-window = "1.3.0" +androidx-window = "1.4.0-beta01" androidxHiltNavigationCompose = "1.2.0" coil = "2.7.0" # @keep -compileSdk = "34" -compose-latest = "1.7.2" +compileSdk = "35" +compose-latest = "1.7.7" composeUiTooling = "1.4.0" coreSplashscreen = "1.0.1" -coroutines = "1.7.3" +coroutines = "1.10.1" glide = "1.0.0-beta01" google-maps = "19.0.0" -gradle-versions = "0.51.0" -hilt = "2.52" -horologist = "0.6.19" +gradle-versions = "0.52.0" +guava = "33.4.0-jre" +hilt = "2.55" +horologist = "0.6.22" junit = "4.13.2" -kotlin = "2.0.20" -kotlinxSerializationJson = "1.7.3" -ksp = "2.0.20-1.0.25" -maps-compose = "6.1.2" -material = "1.13.0-alpha06" +kotlin = "2.1.10" +kotlinxSerializationJson = "1.8.0" +ksp = "2.1.10-1.0.29" +maps-compose = "6.4.2" +material = "1.13.0-alpha10" material3-adaptive = "1.0.0" -material3-adaptive-navigation-suite = "1.3.0" -media3 = "1.4.1" +material3-adaptive-navigation-suite = "1.3.1" +media3 = "1.5.1" # @keep minSdk = "21" -playServicesWearable = "18.2.0" -recyclerview = "1.3.2" +playServicesWearable = "19.0.0" +protolayout = "1.2.1" +recyclerview = "1.4.0" # @keep targetSdk = "34" -version-catalog-update = "0.8.4" +tiles = "1.4.1" +version-catalog-update = "0.8.5" +wear = "1.3.0" wearComposeFoundation = "1.4.0" wearComposeMaterial = "1.4.0" +wearToolingPreview = "1.0.0" [libraries] accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" } -accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } +accompanist-permissions = "com.google.accompanist:accompanist-permissions:0.37.0" accompanist-theme-adapter-appcompat = { module = "com.google.accompanist:accompanist-themeadapter-appcompat", version.ref = "accompanist" } accompanist-theme-adapter-material = { module = "com.google.accompanist:accompanist-themeadapter-material", version.ref = "accompanist" } accompanist-theme-adapter-material3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version.ref = "accompanist" } @@ -94,7 +99,7 @@ androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", versi androidx-glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "androidx-glance-appwidget" } androidx-graphics-shapes = "androidx.graphics:graphics-shapes:1.0.1" androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } -androidx-lifecycle-runtime = "androidx.lifecycle:lifecycle-runtime-ktx:2.8.5" +androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-runtime-compose" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-runtime-compose" } androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } @@ -103,12 +108,22 @@ androidx-media3-common = { module = "androidx.media3:media3-common", version.ref androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" } +androidx-protolayout = { module = "androidx.wear.protolayout:protolayout", version.ref = "protolayout" } +androidx-protolayout-expression = { module = "androidx.wear.protolayout:protolayout-expression", version.ref = "protolayout" } +androidx-protolayout-material = { module = "androidx.wear.protolayout:protolayout-material", version.ref = "protolayout" } androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" } androidx-test-runner = "androidx.test:runner:1.6.2" +androidx-tiles = { module = "androidx.wear.tiles:tiles", version.ref = "tiles" } +androidx-tiles-renderer = { module = "androidx.wear.tiles:tiles-renderer", version.ref = "tiles" } +androidx-tiles-testing = { module = "androidx.wear.tiles:tiles-testing", version.ref = "tiles" } +androidx-tiles-tooling = { module = "androidx.wear.tiles:tiles-tooling", version.ref = "tiles" } +androidx-tiles-tooling-preview = { module = "androidx.wear.tiles:tiles-tooling-preview", version.ref = "tiles" } +androidx-wear = { module = "androidx.wear:wear", version.ref = "wear" } +androidx-wear-tooling-preview = { module = "androidx.wear:wear-tooling-preview", version.ref = "wearToolingPreview" } androidx-window-core = { module = "androidx.window:window-core", version.ref = "androidx-window" } -androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.9.1" +androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.10.0" coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wearComposeFoundation" } compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" } @@ -117,6 +132,7 @@ glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "g google-android-material = { module = "com.google.android.material:material", version.ref = "material" } googlemaps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "maps-compose" } googlemaps-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "google-maps" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } horologist-compose-layout = { module = "com.google.android.horologist:horologist-compose-layout", version.ref = "horologist" } @@ -124,7 +140,7 @@ horologist-compose-material = { module = "com.google.android.horologist:horologi junit = { module = "junit:junit", version.ref = "junit" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } -kotlinx-coroutines-test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0" +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0aaefbcaf..df97d72b8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/misc/.gitignore b/misc/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/misc/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/misc/build.gradle.kts b/misc/build.gradle.kts new file mode 100644 index 000000000..de867bb88 --- /dev/null +++ b/misc/build.gradle.kts @@ -0,0 +1,72 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) + alias(libs.plugins.compose.compiler) +} + +android { + compileSdk = libs.versions.compileSdk.get().toInt() + namespace = "com.example.snippets" + + defaultConfig { + applicationId = "com.example.snippets" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + kotlin { + jvmToolchain(17) + } + + buildTypes { + getByName("debug") { + signingConfig = signingConfigs.getByName("debug") + } + + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + buildFeatures { + compose = true + // Disable unused AGP features + viewBinding = true + } + +} +dependencies { + val composeBom = platform(libs.androidx.compose.bom) + implementation(composeBom) + androidTestImplementation(composeBom) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.foundation.layout) + implementation(libs.androidx.compose.ui.util) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + + implementation(libs.hilt.android) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.kotlinx.serialization.json) + ksp(libs.hilt.compiler) + + implementation(libs.androidx.lifecycle.runtime) + testImplementation(libs.junit) + androidTestImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.core) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.espresso.core) +} diff --git a/shared/proguard-rules.pro b/misc/proguard-rules.pro similarity index 100% rename from shared/proguard-rules.pro rename to misc/proguard-rules.pro diff --git a/misc/src/main/AndroidManifest.xml b/misc/src/main/AndroidManifest.xml new file mode 100644 index 000000000..0a7a5c6f0 --- /dev/null +++ b/misc/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/misc/src/main/java/com/example/snippets/BroadcastReceiverJavaSnippets.java b/misc/src/main/java/com/example/snippets/BroadcastReceiverJavaSnippets.java new file mode 100644 index 000000000..083639954 --- /dev/null +++ b/misc/src/main/java/com/example/snippets/BroadcastReceiverJavaSnippets.java @@ -0,0 +1,120 @@ +package com.example.snippets; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; + +import androidx.activity.ComponentActivity; +import androidx.core.content.ContextCompat; + +import java.util.Objects; + +import javax.inject.Inject; + +import dagger.hilt.android.qualifiers.ApplicationContext; + +// Warning for reader: This file has the Java code snippets for completeness of the corresponding +// documentation page, but these snippets are not part of the actual sample. Refer to the Kotlin +// code for the actual sample. +public class BroadcastReceiverJavaSnippets { + + // [START android_broadcast_receiver_2_class_java] + public static class MyBroadcastReceiver extends BroadcastReceiver { + + @Inject + DataRepository dataRepository; + + @Override + public void onReceive(Context context, Intent intent) { + if (Objects.equals(intent.getAction(), "com.example.snippets.ACTION_UPDATE_DATA")) { + String data = intent.getStringExtra("com.example.snippets.DATA"); + // Do something with the data, for example send it to a data repository: + if (data != null) { dataRepository.updateData(data); } + } + } + } + // [END android_broadcast_receiver_2_class_java] + + /** @noinspection ConstantValue, unused */ + public static class BroadcastReceiverViewModel { + Context context; + + public BroadcastReceiverViewModel(@ApplicationContext Context context) { + this.context = context; + } + + // [START android_broadcast_receiver_3_definition_java] + MyBroadcastReceiver myBroadcastReceiver = new MyBroadcastReceiver(); + // [END android_broadcast_receiver_3_definition_java] + + public void registerBroadcastReceiver() { + // [START android_broadcast_receiver_4_intent_filter_java] + IntentFilter filter = new IntentFilter("com.example.snippets.ACTION_UPDATE_DATA"); + // [END android_broadcast_receiver_4_intent_filter_java] + // [START android_broadcast_receiver_5_exported_java] + boolean listenToBroadcastsFromOtherApps = false; + int receiverFlags = listenToBroadcastsFromOtherApps + ? ContextCompat.RECEIVER_EXPORTED + : ContextCompat.RECEIVER_NOT_EXPORTED; + // [END android_broadcast_receiver_5_exported_java] + + // [START android_broadcast_receiver_6_register_java] + ContextCompat.registerReceiver(context, myBroadcastReceiver, filter, receiverFlags); + // [END android_broadcast_receiver_6_register_java] + + // [START android_broadcast_receiver_12_register_with_permission_java] + ContextCompat.registerReceiver( + context, myBroadcastReceiver, filter, + android.Manifest.permission.ACCESS_COARSE_LOCATION, + null, // scheduler that defines thread, null means run on main thread + receiverFlags + ); + // [END android_broadcast_receiver_12_register_with_permission_java] + } + + public void unregisterBroadcastReceiver() { + context.unregisterReceiver(myBroadcastReceiver); + } + + public void sendBroadcast(String newData, boolean usePermission) { + if(usePermission) { + // [START android_broadcast_receiver_8_send_java] + Intent intent = new Intent("com.example.snippets.ACTION_UPDATE_DATA"); + intent.putExtra("com.example.snippets.DATA", newData); + intent.setPackage("com.example.snippets"); + context.sendBroadcast(intent); + // [END android_broadcast_receiver_8_send_java] + } else { + Intent intent = new Intent("com.example.snippets.ACTION_UPDATE_DATA"); + intent.putExtra("com.example.snippets.DATA", newData); + intent.setPackage("com.example.snippets"); + // [START android_broadcast_receiver_9_send_with_permission_java] + context.sendBroadcast(intent, android.Manifest.permission.ACCESS_COARSE_LOCATION); + // [END android_broadcast_receiver_9_send_with_permission_java] + } + } + } + + /** @noinspection InnerClassMayBeStatic*/ + // [START android_broadcast_receiver_13_activity_java] + class MyActivity extends ComponentActivity { + MyBroadcastReceiver myBroadcastReceiver; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // [START_EXCLUDE] + IntentFilter filter = new IntentFilter("com.example.snippets.ACTION_UPDATE_DATA"); + boolean listenToBroadcastsFromOtherApps = false; + int receiverFlags = listenToBroadcastsFromOtherApps + ? ContextCompat.RECEIVER_EXPORTED + : ContextCompat.RECEIVER_NOT_EXPORTED; + // [END_EXCLUDE] + ContextCompat.registerReceiver(this, myBroadcastReceiver, filter, receiverFlags); + // Set content + } + } + // [END android_broadcast_receiver_13_activity_java] +} diff --git a/misc/src/main/java/com/example/snippets/BroadcastReceiverSnippets.kt b/misc/src/main/java/com/example/snippets/BroadcastReceiverSnippets.kt new file mode 100644 index 000000000..dda35cbe0 --- /dev/null +++ b/misc/src/main/java/com/example/snippets/BroadcastReceiverSnippets.kt @@ -0,0 +1,272 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.LifecycleStartEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +// Warning for reader: This file contains both the code snippets for apps _sending_ broadcasts, as +// those that are _receiving_ broadcasts. Do not consider this a reference implementation. +// +// The actual sample demonstrates how data can be passed from a broadcast receiver back to the UI, +// through an intermediary data repository. + +@AndroidEntryPoint +// [START android_broadcast_receiver_2_class] +class MyBroadcastReceiver : BroadcastReceiver() { + + @Inject + lateinit var dataRepository: DataRepository + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == "com.example.snippets.ACTION_UPDATE_DATA") { + val data = intent.getStringExtra("com.example.snippets.DATA") ?: "No data" + // Do something with the data, for example send it to a data repository: + dataRepository.updateData(data) + } + } +} +// [END android_broadcast_receiver_2_class] + +@HiltViewModel +class BroadcastReceiverViewModel @Inject constructor( + @ApplicationContext private val context: Context, + dataRepository: DataRepository +) : ViewModel() { + val data = dataRepository.data + + @Suppress("MemberVisibilityCanBePrivate") + // [START android_broadcast_receiver_3_definition] + val myBroadcastReceiver = MyBroadcastReceiver() + // [END android_broadcast_receiver_3_definition] + + fun registerBroadcastReceiver() { + // [START android_broadcast_receiver_4_intent_filter] + val filter = IntentFilter("com.example.snippets.ACTION_UPDATE_DATA") + // [END android_broadcast_receiver_4_intent_filter] + // [START android_broadcast_receiver_5_exported] + val listenToBroadcastsFromOtherApps = false + val receiverFlags = if (listenToBroadcastsFromOtherApps) { + ContextCompat.RECEIVER_EXPORTED + } else { + ContextCompat.RECEIVER_NOT_EXPORTED + } + // [END android_broadcast_receiver_5_exported] + + // [START android_broadcast_receiver_6_register] + ContextCompat.registerReceiver(context, myBroadcastReceiver, filter, receiverFlags) + // [END android_broadcast_receiver_6_register] + + // [START android_broadcast_receiver_12_register_with_permission] + ContextCompat.registerReceiver( + context, myBroadcastReceiver, filter, + android.Manifest.permission.ACCESS_COARSE_LOCATION, + null, // scheduler that defines thread, null means run on main thread + receiverFlags + ) + // [END android_broadcast_receiver_12_register_with_permission] + } + + fun unregisterBroadcastReceiver() { + context.unregisterReceiver(myBroadcastReceiver) + } + + fun sendBroadcast(newData: String, usePermission: Boolean = false) { + if (!usePermission) { + // [START android_broadcast_receiver_8_send] + val intent = Intent("com.example.snippets.ACTION_UPDATE_DATA").apply { + putExtra("com.example.snippets.DATA", newData) + setPackage("com.example.snippets") + } + context.sendBroadcast(intent) + // [END android_broadcast_receiver_8_send] + } else { + val intent = Intent("com.example.snippets.ACTION_UPDATE_DATA").apply { + putExtra("com.example.snippets.DATA", newData) + setPackage("com.example.snippets") + } + // [START android_broadcast_receiver_9_send_with_permission] + context.sendBroadcast(intent, android.Manifest.permission.ACCESS_COARSE_LOCATION) + // [END android_broadcast_receiver_9_send_with_permission] + } + } +} + +@Suppress("NAME_SHADOWING") +@Composable +fun LifecycleScopedBroadcastReceiver( + registerReceiver: () -> Unit, + unregisterReceiver: () -> Unit +) { + val registerReceiver by rememberUpdatedState(registerReceiver) + val unregisterReceiver by rememberUpdatedState(unregisterReceiver) + // [START android_broadcast_receiver_7_lifecycle_scoped] + LifecycleStartEffect(true) { + registerReceiver() + onStopOrDispose { unregisterReceiver() } + } + // [END android_broadcast_receiver_7_lifecycle_scoped] +} + +@Composable +fun BroadcastReceiverSample( + modifier: Modifier = Modifier, + viewModel: BroadcastReceiverViewModel = hiltViewModel() +) { + val data by viewModel.data.collectAsStateWithLifecycle() + BroadcastReceiverSample( + modifier = modifier, + data = data, + registerBroadcastReceiver = viewModel::registerBroadcastReceiver, + unregisterBroadcastReceiver = viewModel::unregisterBroadcastReceiver, + sendBroadcast = viewModel::sendBroadcast + ) +} + +@Composable +fun BroadcastReceiverSample( + modifier: Modifier = Modifier, + data: String, + registerBroadcastReceiver: () -> Unit, + unregisterBroadcastReceiver: () -> Unit, + sendBroadcast: (newData: String) -> Unit +) { + var newData by remember { mutableStateOf("") } + Scaffold { innerPadding -> + Column( + modifier + .padding(innerPadding) + .padding(16.dp) + ) { + Text("Fill in a word, send broadcast, and see it added to the bottom") + Spacer(Modifier.height(16.dp)) + TextField(newData, onValueChange = { newData = it }, Modifier.widthIn(min = 160.dp)) + Spacer(Modifier.height(16.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Button(onClick = { sendBroadcast(newData) }) { + Text("Send broadcast") + } + } + Spacer(Modifier.height(16.dp)) + Text(data, Modifier.verticalScroll(rememberScrollState())) + } + } + LifecycleScopedBroadcastReceiver(registerBroadcastReceiver, unregisterBroadcastReceiver) +} + +class MyBroadcastReceiverWithPermission : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + // no-op, only used to demonstrate manifest registration of receiver with permission + } +} + +// Ignore following code - it's only used to demonstrate best practices, not part of the sample +@Suppress("unused") +// [START android_broadcast_receiver_13_activity] +class MyActivity : ComponentActivity() { + private val myBroadcastReceiver = MyBroadcastReceiver() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // [START_EXCLUDE] + val filter = IntentFilter("com.example.snippets.ACTION_UPDATE_DATA") + val listenToBroadcastsFromOtherApps = false + val receiverFlags = if (listenToBroadcastsFromOtherApps) { + ContextCompat.RECEIVER_EXPORTED + } else { + ContextCompat.RECEIVER_NOT_EXPORTED + } + + // [END_EXCLUDE] + ContextCompat.registerReceiver(this, myBroadcastReceiver, filter, receiverFlags) + setContent { MyApp() } + } + + override fun onDestroy() { + super.onDestroy() + // When you forget to unregister your receiver here, you're causing a leak! + this.unregisterReceiver(myBroadcastReceiver) + } +} +// [END android_broadcast_receiver_13_activity] + +@Composable +fun MyApp() {} + +@Suppress("unused") +// [START android_broadcast_receiver_14_stateless] +@Composable +fun MyStatefulScreen() { + val myBroadcastReceiver = remember { MyBroadcastReceiver() } + val context = LocalContext.current + LifecycleStartEffect(true) { + // [START_EXCLUDE] + val filter = IntentFilter("com.example.snippets.ACTION_UPDATE_DATA") + val listenToBroadcastsFromOtherApps = false + val flags = if (listenToBroadcastsFromOtherApps) { + ContextCompat.RECEIVER_EXPORTED + } else { + ContextCompat.RECEIVER_NOT_EXPORTED + } + // [END_EXCLUDE] + ContextCompat.registerReceiver(context, myBroadcastReceiver, filter, flags) + onStopOrDispose { context.unregisterReceiver(myBroadcastReceiver) } + } + MyStatelessScreen() +} + +@Composable +fun MyStatelessScreen() { + // Implement your screen +} +// [END android_broadcast_receiver_14_stateless] diff --git a/misc/src/main/java/com/example/snippets/DataRepository.kt b/misc/src/main/java/com/example/snippets/DataRepository.kt new file mode 100644 index 000000000..7e626e5a8 --- /dev/null +++ b/misc/src/main/java/com/example/snippets/DataRepository.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets + +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +@Singleton +class DataRepository @Inject constructor() { + // You would normally save this data in a database or other persistent storage + private val _data: MutableStateFlow = MutableStateFlow("This text will be updated") + val data: StateFlow = _data + + fun updateData(data: String) { + _data.value += "\n$data" + } +} diff --git a/misc/src/main/java/com/example/snippets/MainActivity.kt b/misc/src/main/java/com/example/snippets/MainActivity.kt new file mode 100644 index 000000000..a687939e9 --- /dev/null +++ b/misc/src/main/java/com/example/snippets/MainActivity.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.example.snippets.navigation.Destination +import com.example.snippets.navigation.LandingScreen +import com.example.snippets.ui.theme.SnippetsTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + SnippetsTheme { + val navController = rememberNavController() + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + NavHost(navController, startDestination = "LandingScreen") { + composable("LandingScreen") { + LandingScreen { navController.navigate(it.route) } + } + Destination.entries.forEach { destination -> + composable(destination.route) { + when (destination) { + Destination.BroadcastReceiverExamples -> BroadcastReceiverSample() + // Add your destination here + } + } + } + } + } + } + } + } +} diff --git a/misc/src/main/java/com/example/snippets/MyApplication.kt b/misc/src/main/java/com/example/snippets/MyApplication.kt new file mode 100644 index 000000000..4e4a187c6 --- /dev/null +++ b/misc/src/main/java/com/example/snippets/MyApplication.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class MyApplication : Application() diff --git a/misc/src/main/java/com/example/snippets/location/LocationPermissionsActivity.kt b/misc/src/main/java/com/example/snippets/location/LocationPermissionsActivity.kt new file mode 100644 index 000000000..674f8a42e --- /dev/null +++ b/misc/src/main/java/com/example/snippets/location/LocationPermissionsActivity.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.location + +import android.Manifest +import android.os.Build +import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi + +// Assuming this function is inside a ComponentActivity or a subclass of it +class LocationPermissionsActivity : ComponentActivity() { + + // [START android_location_requestpermissions_kotlin] + @RequiresApi(Build.VERSION_CODES.N) + fun requestPermissions() { + val locationPermissionRequest = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + when { + permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> { + // Precise location access granted. + } + permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> { + // Only approximate location access granted. + } + else -> { + // No location access granted. + } + } + } + + // Before you perform the actual permission request, check whether your app + // already has the permissions, and whether your app needs to show a permission + // rationale dialog. For more details, see Request permissions: + // https://developer.android.com/training/permissions/requesting#request-permission + locationPermissionRequest.launch( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + ) + } + // [END android_location_requestpermissions_kotlin] +} diff --git a/misc/src/main/java/com/example/snippets/location/LocationPermissionsActivityJava.java b/misc/src/main/java/com/example/snippets/location/LocationPermissionsActivityJava.java new file mode 100644 index 000000000..8d20ad5d3 --- /dev/null +++ b/misc/src/main/java/com/example/snippets/location/LocationPermissionsActivityJava.java @@ -0,0 +1,51 @@ +package com.example.snippets.location; + +import android.Manifest; +import android.os.Build; +import android.os.Bundle; +import androidx.activity.ComponentActivity; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import java.util.Map; + +public class LocationPermissionsActivityJava extends ComponentActivity { + + // [START android_location_requestpermissions_java] + private void requestPermissions() { + + ActivityResultLauncher locationPermissionRequest = + registerForActivityResult(new ActivityResultContracts + .RequestMultiplePermissions(), result -> { + + Boolean fineLocationGranted = null; + Boolean coarseLocationGranted = null; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + fineLocationGranted = result.getOrDefault( + Manifest.permission.ACCESS_FINE_LOCATION, false); + coarseLocationGranted = result.getOrDefault( + Manifest.permission.ACCESS_COARSE_LOCATION,false); + } + + if (fineLocationGranted != null && fineLocationGranted) { + // Precise location access granted. + } else if (coarseLocationGranted != null && coarseLocationGranted) { + // Only approximate location access granted. + } else { + // No location access granted. + } + } + ); + + // ... + + // Before you perform the actual permission request, check whether your app + // already has the permissions, and whether your app needs to show a permission + // rationale dialog. For more details, see Request permissions. + locationPermissionRequest.launch(new String[] { + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + }); + } + // [END android_location_requestpermissions_java] +} \ No newline at end of file diff --git a/misc/src/main/java/com/example/snippets/navigation/Destination.kt b/misc/src/main/java/com/example/snippets/navigation/Destination.kt new file mode 100644 index 000000000..59f424a7b --- /dev/null +++ b/misc/src/main/java/com/example/snippets/navigation/Destination.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.navigation + +enum class Destination(val route: String, val title: String) { + BroadcastReceiverExamples("broadcastReceiverExamples", "Broadcast Receiver Examples"), + // Add your example here +} diff --git a/misc/src/main/java/com/example/snippets/navigation/LandingScreen.kt b/misc/src/main/java/com/example/snippets/navigation/LandingScreen.kt new file mode 100644 index 000000000..f096906dd --- /dev/null +++ b/misc/src/main/java/com/example/snippets/navigation/LandingScreen.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.navigation + +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LandingScreen( + navigate: (Destination) -> Unit +) { + Scaffold( + topBar = { + TopAppBar(title = { + Text(text = "Android snippets",) + }) + } + ) { padding -> + NavigationItems(modifier = Modifier.padding(padding)) { navigate(it) } + } +} + +@Composable +fun NavigationItems( + modifier: Modifier = Modifier, + navigate: (Destination) -> Unit +) { + LazyColumn( + modifier = modifier + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + items(Destination.entries) { destination -> + NavigationItem(destination) { + navigate( + destination + ) + } + } + } +} + +@Composable +fun NavigationItem(destination: Destination, onClick: () -> Unit) { + ListItem( + headlineContent = { + Text(destination.title) + }, + modifier = Modifier + .heightIn(min = 48.dp) + .clickable { + onClick() + } + ) +} diff --git a/misc/src/main/java/com/example/snippets/ui/theme/Color.kt b/misc/src/main/java/com/example/snippets/ui/theme/Color.kt new file mode 100644 index 000000000..662ef48ff --- /dev/null +++ b/misc/src/main/java/com/example/snippets/ui/theme/Color.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) diff --git a/misc/src/main/java/com/example/snippets/ui/theme/Theme.kt b/misc/src/main/java/com/example/snippets/ui/theme/Theme.kt new file mode 100644 index 000000000..4dcf62e85 --- /dev/null +++ b/misc/src/main/java/com/example/snippets/ui/theme/Theme.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun SnippetsTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} diff --git a/misc/src/main/java/com/example/snippets/ui/theme/Type.kt b/misc/src/main/java/com/example/snippets/ui/theme/Type.kt new file mode 100644 index 000000000..f383a07ba --- /dev/null +++ b/misc/src/main/java/com/example/snippets/ui/theme/Type.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.snippets.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) diff --git a/shared/src/main/res/drawable/ic_launcher_background.xml b/misc/src/main/res/drawable/ic_launcher_background.xml similarity index 54% rename from shared/src/main/res/drawable/ic_launcher_background.xml rename to misc/src/main/res/drawable/ic_launcher_background.xml index 336b6dca0..07d5da9cb 100644 --- a/shared/src/main/res/drawable/ic_launcher_background.xml +++ b/misc/src/main/res/drawable/ic_launcher_background.xml @@ -1,171 +1,170 @@ - + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> diff --git a/misc/src/main/res/drawable/ic_launcher_foreground.xml b/misc/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/misc/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/shared/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/misc/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 79% rename from shared/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to misc/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index eca70cfe5..6f3b755bf 100644 --- a/shared/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/misc/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/shared/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/misc/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 79% rename from shared/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to misc/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index eca70cfe5..6f3b755bf 100644 --- a/shared/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/misc/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/misc/src/main/res/mipmap-hdpi/ic_launcher.webp b/misc/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/misc/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/misc/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/misc/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..b2dfe3d1b Binary files /dev/null and b/misc/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/misc/src/main/res/mipmap-mdpi/ic_launcher.webp b/misc/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/misc/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/misc/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/misc/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..62b611da0 Binary files /dev/null and b/misc/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/misc/src/main/res/mipmap-xhdpi/ic_launcher.webp b/misc/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/misc/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/misc/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/misc/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..1b9a6956b Binary files /dev/null and b/misc/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/misc/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/misc/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/misc/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/misc/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/misc/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9287f5083 Binary files /dev/null and b/misc/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/misc/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/misc/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/misc/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/misc/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/misc/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9126ae37c Binary files /dev/null and b/misc/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/misc/src/main/res/values/colors.xml b/misc/src/main/res/values/colors.xml new file mode 100644 index 000000000..f8c6127d3 --- /dev/null +++ b/misc/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/misc/src/main/res/values/strings.xml b/misc/src/main/res/values/strings.xml new file mode 100644 index 000000000..870fc4736 --- /dev/null +++ b/misc/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Background Snippets + \ No newline at end of file diff --git a/misc/src/main/res/values/themes.xml b/misc/src/main/res/values/themes.xml new file mode 100644 index 000000000..65078ebe0 --- /dev/null +++ b/misc/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + + - - diff --git a/views/.gitignore b/views/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/views/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/views/README.md b/views/README.md new file mode 100644 index 000000000..a9256a204 --- /dev/null +++ b/views/README.md @@ -0,0 +1 @@ +This is a sample project that contains the code snippets seen on https://android.devsite.corp.google.com/develop/ui/views diff --git a/views/build.gradle.kts b/views/build.gradle.kts new file mode 100644 index 000000000..80ee4edc8 --- /dev/null +++ b/views/build.gradle.kts @@ -0,0 +1,56 @@ +//Copyright 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +// +//https://www.apache.org/licenses/LICENSE-2.0 +// +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +// limitations under the License. + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.example.example.snippet.views" + compileSdk = 35 + + defaultConfig { + minSdk = 35 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.google.android.material) + implementation(libs.androidx.glance.appwidget) + +} \ No newline at end of file diff --git a/views/consumer-rules.pro b/views/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/views/proguard-rules.pro b/views/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/views/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/views/src/main/AndroidManifest.xml b/views/src/main/AndroidManifest.xml new file mode 100644 index 000000000..65ba8e1e0 --- /dev/null +++ b/views/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/views/src/main/java/com/example/example/snippet/views/appwidget/AppWidgetSnippets.kt b/views/src/main/java/com/example/example/snippet/views/appwidget/AppWidgetSnippets.kt new file mode 100644 index 000000000..cd6ac4855 --- /dev/null +++ b/views/src/main/java/com/example/example/snippet/views/appwidget/AppWidgetSnippets.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.example.snippet.views.appwidget + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.appwidget.AppWidgetProviderInfo +import android.content.ComponentName +import android.content.Context +import android.widget.RemoteViews +import androidx.glance.GlanceId +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.compose +import com.example.example.snippet.views.R + +class ExampleAppWidget:GlanceAppWidget() { + override suspend fun provideGlance(context: Context, id: GlanceId) { + TODO("Not yet implemented") + } + +} + +private object GeneratedPreviewWithoutGlance { + + lateinit var appContext: Context + + fun MyWidgetPreview() { + // [START android_view_appwidget_generatedpreview_with_remoteview] + AppWidgetManager.getInstance(appContext).setWidgetPreview( + ComponentName( + appContext, + ExampleAppWidgetReceiver::class.java + ), + AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN, + RemoteViews("com.example", R.layout.widget_preview) + ) + // [END android_view_appwidget_generatedpreview_with_remoteview] + } + + suspend fun MyGlanceWidgetPreview() { + // [START android_view_appwidget_generatedpreview_with_glance] + AppWidgetManager.getInstance(appContext).setWidgetPreview( + ComponentName( + appContext, + ExampleAppWidgetReceiver::class.java + ), + AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN, + ExampleAppWidget().compose( + context = appContext + ), + ) + + // [END android_view_appwidget_generatedpreview_with_glance] + } +} + +class ExampleAppWidgetReceiver: AppWidgetProvider() { + +} diff --git a/views/src/main/res/layout/widget_preview.xml b/views/src/main/res/layout/widget_preview.xml new file mode 100644 index 000000000..c9f449bb1 --- /dev/null +++ b/views/src/main/res/layout/widget_preview.xml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 9725e5396..154e7d37e 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -6,7 +6,7 @@ plugins { android { namespace = "com.example.wear" - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "com.example.wear" @@ -55,6 +55,17 @@ dependencies { implementation(libs.compose.ui.tooling) implementation(libs.play.services.wearable) + implementation(libs.androidx.tiles) + implementation(libs.androidx.wear) + implementation(libs.androidx.protolayout) + implementation(libs.androidx.protolayout.material) + implementation(libs.androidx.protolayout.expression) + debugImplementation(libs.androidx.tiles.renderer) + testImplementation(libs.androidx.tiles.testing) + implementation(libs.androidx.wear.tooling.preview) + implementation(libs.androidx.tiles.tooling.preview) + debugImplementation(libs.androidx.tiles.tooling) + implementation(libs.guava) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling.preview) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index f0d2328e2..84c4785a2 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -34,6 +34,24 @@
+ + + + + + + + + + + \ No newline at end of file diff --git a/wear/src/main/java/com/example/wear/snippets/navigation/Navigation.kt b/wear/src/main/java/com/example/wear/snippets/navigation/Navigation.kt index 5c7fbe062..42507078c 100644 --- a/wear/src/main/java/com/example/wear/snippets/navigation/Navigation.kt +++ b/wear/src/main/java/com/example/wear/snippets/navigation/Navigation.kt @@ -47,8 +47,8 @@ import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll @Composable fun navigation() { + // [START android_wear_navigation] AppScaffold { - // [START android_wear_navigation] val navController = rememberSwipeDismissableNavController() SwipeDismissableNavHost( navController = navController, @@ -63,16 +63,20 @@ fun navigation() { MessageDetail(id = it.arguments?.getString("id")!!) } } - // [END android_wear_navigation] } + // [START_EXCLUDE] } @OptIn(ExperimentalHorologistApi::class) @Composable fun MessageDetail(id: String) { + // [END_EXCLUDE] + // .. Screen level content goes here val scrollState = rememberScrollState() ScreenScaffold(scrollState = scrollState) { + // Screen content goes here + // [END android_wear_navigation] val padding = ScalingLazyColumnDefaults.padding( first = ItemType.Text, last = ItemType.Text diff --git a/wear/src/main/java/com/example/wear/snippets/rotary/Rotary.kt b/wear/src/main/java/com/example/wear/snippets/rotary/Rotary.kt index a6115816f..97bace500 100644 --- a/wear/src/main/java/com/example/wear/snippets/rotary/Rotary.kt +++ b/wear/src/main/java/com/example/wear/snippets/rotary/Rotary.kt @@ -243,7 +243,7 @@ fun SnapScrollableScreen() { } @Composable -fun PositionScrollIndicator(){ +fun PositionScrollIndicator() { // [START android_wear_rotary_position_indicator] val listState = rememberScalingLazyListState() Scaffold( diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt b/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt new file mode 100644 index 000000000..58cbaa758 --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.wear.snippets.tile + +import androidx.wear.protolayout.ColorBuilders.argb +import androidx.wear.protolayout.ResourceBuilders.Resources +import androidx.wear.protolayout.TimelineBuilders.Timeline +import androidx.wear.protolayout.material.Text +import androidx.wear.protolayout.material.Typography +import androidx.wear.tiles.RequestBuilders +import androidx.wear.tiles.RequestBuilders.ResourcesRequest +import androidx.wear.tiles.TileBuilders.Tile +import androidx.wear.tiles.TileService +import com.google.common.util.concurrent.Futures + +private const val RESOURCES_VERSION = "1" + +// [START android_wear_tile_mytileservice] +class MyTileService : TileService() { + + override fun onTileRequest(requestParams: RequestBuilders.TileRequest) = + Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + Text.Builder(this, "Hello World!") + .setTypography(Typography.TYPOGRAPHY_BODY1) + .setColor(argb(0xFFFFFFFF.toInt())) + .build() + ) + ) + .build() + ) + + override fun onTileResourcesRequest(requestParams: ResourcesRequest) = + Futures.immediateFuture( + Resources.Builder() + .setVersion(RESOURCES_VERSION) + .build() + ) +} +// [END android_wear_tile_mytileservice] diff --git a/wear/src/main/res/drawable/tile_preview.png b/wear/src/main/res/drawable/tile_preview.png new file mode 100644 index 000000000..b2a254a16 Binary files /dev/null and b/wear/src/main/res/drawable/tile_preview.png differ diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml index 31b64b4d3..fc59c67b8 100644 --- a/wear/src/main/res/values/strings.xml +++ b/wear/src/main/res/values/strings.xml @@ -3,4 +3,6 @@ Voice Input Voice Text Entry Message List + Hello Tile + Hello Tile Description \ No newline at end of file