Oprócz funkcji Canvas
kompozycyjnej aplikacja Compose zawiera kilka przydatnych elementów graficznychModifiers
, które ułatwiają rysowanie niestandardowych treści. Te modyfikatory są przydatne,
bo można je stosować w dowolnym elemencie kompozycyjnym.
Modyfikatory rysowania
Wszystkie polecenia rysowania są wykonywane przy użyciu modyfikatora rysunku w narzędziu Compose. W Compose są dostępne 3 główne modyfikatory rysunku:
Podstawowym modyfikatorem rysowania jest drawWithContent
. Umożliwia on określenie kolejności rysowania w obiekcie kompozycyjnym oraz poleceń rysowania wysyłanych w obrębie tego modyfikatora. drawBehind
to wygodny otoczenie wokół drawWithContent
, w którym kolejność rysowania jest ustawiona na za zawartość funkcji kompozycyjnej. Funkcja drawWithCache
wywołuje w nim onDrawBehind
lub onDrawWithContent
i zapewnia mechanizm buforowania utworzonych w nim obiektów.
Modifier.drawWithContent
: wybierz kolejność rysowania
Modifier.drawWithContent
umożliwia wykonywanie operacji DrawScope
przed treścią funkcji kompozycyjnej lub po niej. Pamiętaj, by wywołać drawContent
, by potem wyrenderować rzeczywistą zawartość elementu kompozycyjnego. Dzięki temu modyfikatorowi możesz określić kolejność działań w zależności od tego, czy treść ma być rysowana przed niestandardowymi operacjami rysowania lub po nich.
Jeśli na przykład chcesz wyrenderować gradient promieniowy na treści, aby utworzyć w interfejsie efekt dziurki od klucza w latarce, wykonaj te czynności:
var pointerOffset by remember { mutableStateOf(Offset(0f, 0f)) } Column( modifier = Modifier .fillMaxSize() .pointerInput("dragging") { detectDragGestures { change, dragAmount -> pointerOffset += dragAmount } } .onSizeChanged { pointerOffset = Offset(it.width / 2f, it.height / 2f) } .drawWithContent { drawContent() // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI. drawRect( Brush.radialGradient( listOf(Color.Transparent, Color.Black), center = pointerOffset, radius = 100.dp.toPx(), ) ) } ) { // Your composables here }
Modifier.drawBehind
: zarysowanie elementu kompozycyjnego
Modifier.drawBehind
umożliwia wykonywanie operacji DrawScope
w odniesieniu do treści kompozycyjnej, która jest rysowana na ekranie. Gdy przyjrzysz się implementacji Canvas
, możesz zauważyć, że jest to po prostu wygodna otoka wokół Modifier.drawBehind
.
Aby narysować zaokrąglony prostokąt za elementem Text
:
Text( "Hello Compose!", modifier = Modifier .drawBehind { drawRoundRect( Color(0xFFBBAAEE), cornerRadius = CornerRadius(10.dp.toPx()) ) } .padding(4.dp) )
Co daje następujący wynik:
Modifier.drawWithCache
: rysowanie i zapisywanie obiektów rysunkowych w pamięci podręcznej
Modifier.drawWithCache
przechowuje utworzone w niej obiekty w pamięci podręcznej. Obiekty są przechowywane w pamięci podręcznej, o ile rozmiar obszaru rysowania jest taki sam lub żadne odczytywane obiekty stanu nie uległy zmianie. Ten modyfikator jest przydatny przy zwiększaniu wydajności wywołań rysowania, ponieważ pozwala uniknąć ponownego przydzielania obiektów (takich jak Brush, Shader, Path
itp.), które są tworzone podczas rysowania.
Można też buforować obiekty za pomocą remember
(bez użycia modyfikatora). Jednak nie zawsze jest to możliwe, ponieważ nie zawsze masz dostęp do kompozycji. Użycie właściwości drawWithCache
może być skuteczniejsze, jeśli obiekty służą tylko do rysowania.
Jeśli na przykład utworzysz gradient za obiektem Text
, jeśli utworzysz obiekt Brush
, użycie drawWithCache
zapisze obiekt Brush
w pamięci podręcznej, dopóki rozmiar obszaru rysowania się nie zmieni:
Text( "Hello Compose!", modifier = Modifier .drawWithCache { val brush = Brush.linearGradient( listOf( Color(0xFF9E82F0), Color(0xFF42A5F5) ) ) onDrawBehind { drawRoundRect( brush, cornerRadius = CornerRadius(10.dp.toPx()) ) } } )
Modyfikatory grafiki
Modifier.graphicsLayer
: stosuj przekształcenia do elementów kompozycyjnych
Modifier.graphicsLayer
to modyfikator, który sprawia, że zawartość elementu kompozycyjnego rysuje się w warstwie rysowania. Warstwa oferuje kilka różnych funkcji, takich jak:
- Izolacja instrukcji rysowania (podobna do metody
RenderNode
). Instrukcje rysowania zarejestrowane jako część warstwy mogą być ponownie wydawane przez potok renderowania bez ponownego uruchamiania kodu aplikacji. - Przekształcenia, które mają zastosowanie do wszystkich instrukcji rysowania zawartych w warstwie.
- Rasteryzacja w celu uzyskania możliwości kompozycji. Przy zrasteryzowaniu warstwy są wykonywane jej instrukcje rysowania, a dane wyjściowe są zapisywane w buforze poza ekranem. Komponowanie takiego bufora na potrzeby kolejnych klatek jest szybsze niż wykonywanie poszczególnych instrukcji, ale po zastosowaniu przekształceń takich jak skalowanie czy obrót zachowuje się jak bitmapa.
Transformacje
Instrukcje rysowania umożliwiają korzystanie z funkcji Modifier.graphicsLayer
. Za pomocą funkcji Modifier.graphicsLayer
można na przykład stosować różne przekształcenia.
Można je animowane i modyfikować bez konieczności ponownego uruchamiania rysunku lambda.
Modifier.graphicsLayer
nie zmienia zmierzonego rozmiaru ani położenia elementu kompozycyjnego, ponieważ wpływa tylko na fazę rysowania. Oznacza to, że funkcja kompozycyjna może nakładać się na inne, jeśli rysuje poza granicami swojego układu.
Przy użyciu tego modyfikatora można zastosować te przekształcenia:
Skala – zwiększ rozmiar
scaleX
i scaleY
zmniejszają lub zmniejszają treść odpowiednio w kierunku poziomym lub pionowym. Wartość 1.0f
oznacza brak zmiany skali, a wartość 0.5f
oznacza połowę wymiaru.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.scaleX = 1.2f this.scaleY = 0.8f } )
Tłumaczenie
Elementy translationX
i translationY
można zmienić za pomocą przycisku graphicsLayer
, a funkcja translationX
przesuwa element kompozycyjny w lewo lub w prawo. translationY
przesuwa element kompozycyjny w górę lub w dół.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.translationX = 100.dp.toPx() this.translationY = 10.dp.toPx() } )
Obrót
Ustaw rotationX
, by obracać elementy w poziomie, rotationY
, żeby obracać je w pionie, a rotationZ
, żeby obracać je w okolicy Z (obrót standardowy). Wartość ta jest podawana w stopniach (0–360).
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
Miejsce wylotu
Można określić transformOrigin
. Jest on następnie używany jako punkt,
od którego zachodzą przekształcenia. Do tej pory we wszystkich przykładach używana jest wartość TransformOrigin.Center
, która ma wartość (0.5f, 0.5f)
. Jeśli określisz punkt początkowy (0f, 0f)
, przekształcenia będą się zaczynać od lewego górnego rogu elementu kompozycyjnego.
Jeśli zmienisz źródło za pomocą przekształcenia rotationZ
, zobaczysz, że element obraca się w lewym górnym rogu funkcji kompozycyjnej:
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.transformOrigin = TransformOrigin(0f, 0f) this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
Klip i kształt
Kształt określa kontur, do którego będą przechodzić treści, gdy clip = true
. W tym przykładzie ustawiliśmy 2 pola z 2 różnymi klipami – jedno ze zmienną klipu graphicsLayer
, a drugie z wygodnym opakowaniem Modifier.clip
.
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .size(200.dp) .graphicsLayer { clip = true shape = CircleShape } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(CircleShape) .background(Color(0xFF4DB6AC)) ) }
Zawartość pierwszego pola (tekst „Hello Compose”) zostanie skrócona do kształtu koła:
Jeśli potem zastosujesz wymiar translationY
do górnego różowego okręgu, zobaczysz, że granice funkcji kompozycyjnej są takie same, ale okrąg rysuje się pod dolnym okręgiem (i poza jego granicami).
Aby przyciąć kompozycję do regionu, który jest utworzony, możesz dodać kolejny Modifier.clip(RectangleShape)
na początku łańcucha modyfikatorów. Treść pozostaje wtedy w oryginalnych granicach.
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .clip(RectangleShape) .size(200.dp) .border(2.dp, Color.Black) .graphicsLayer { clip = true shape = CircleShape translationY = 50.dp.toPx() } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(RoundedCornerShape(500.dp)) .background(Color(0xFF4DB6AC)) ) }
Alfa
Za pomocą Modifier.graphicsLayer
możesz ustawić alpha
(przezroczystość) dla całej warstwy. Element 1.0f
jest całkowicie nieprzezroczysty, a element 0.0f
jest niewidoczny.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )
Strategia komponowania
Korzystanie z wersji alfa i przejrzystości może być trudniejsze niż zmiana pojedynczej wartości alfa. Oprócz zmiany wersji alfa możesz też ustawić CompositingStrategy
na graphicsLayer
. CompositingStrategy
określa sposób, w jaki zawartość elementu kompozycyjnego jest komponowana (połączona) z innymi treściami widocznymi już na ekranie.
Dostępne strategie to:
Automatyczna (domyślna)
Strategia komponowania jest określana przez pozostałe parametry graphicsLayer
. Jeśli wartość alfa jest mniejsza niż 1,0f lub ustawiona jest wartość RenderEffect
, renderuje warstwę w buforze poza ekranem. Gdy wartość alfa jest mniejsza niż 1f, automatycznie tworzona jest warstwa komponująca w celu wyrenderowania treści, a następnie rysowanie tego bufora pozaekranowego do miejsca docelowego z odpowiednią wersją alfa. Ustawienie RenderEffect
lub nadmierne przewinięcie zawsze powoduje renderowanie treści w buforze poza ekranem niezależnie od ustawienia CompositingStrategy
.
Poza ekranem
Przed wyrenderowaniem treści w miejscu docelowym zawartość funkcji kompozycyjnej jest zawsze zrastrowana na teksturę pozaekranową lub bitmapę. Jest to przydatne przy stosowaniu operacji BlendMode
do maskowania treści oraz podczas renderowania złożonych zestawów instrukcji rysowania.
Przykład użycia CompositingStrategy.Offscreen
z BlendModes
. Biorąc pod uwagę poniższy przykład, załóżmy, że chcesz usunąć fragmenty funkcji kompozycyjnej Image
, wykonując polecenie rysowania z użyciem BlendMode.Clear
. Jeśli nie ustawisz compositingStrategy
na CompositingStrategy.Offscreen
, BlendMode
będzie współdziałać ze całą zawartością znajdującą się pod nim.
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 ) ) } } )
Ustawienie CompositingStrategy
na Offscreen
powoduje utworzenie tekstury poza ekranem, dla której są wykonywane polecenia (stosując BlendMode
tylko do zawartości tego elementu kompozycyjnego). Następnie renderuje go nad tym, co jest już wyrenderowane na ekranie, i nie ma wpływu na te treści.
Jeśli nie używasz CompositingStrategy.Offscreen
, użycie BlendMode.Clear
spowoduje usunięcie wszystkich pikseli w miejscu docelowym niezależnie od tego, co było już ustawione – bufor renderowania okna (czarny) będzie widoczny. Wiele elementów BlendModes
, które obejmują wersję alfa, nie będzie działać zgodnie z oczekiwaniami bez bufora poza ekranem. Zwróć uwagę na czarny pierścień wokół wskaźnika czerwonego okręgu:
Aby to wyjaśnić: gdyby aplikacja miała przezroczyste tło okna i nie użyto CompositingStrategy.Offscreen
, BlendMode
działałby z całą aplikacją. Zostaną usunięte wszystkie piksele i pojawią się pod spodem aplikacja lub tapeta, jak w tym przykładzie:
Warto zauważyć, że przy korzystaniu z funkcji CompositingStrategy.Offscreen
tekstura pozaekranowa o rozmiarze obszaru rysowania jest tworzona i renderowana z powrotem na ekranie. Wszystkie polecenia rysowania wykonywane w ramach tej strategii są domyślnie przypięte do tego regionu. Fragment kodu poniżej pokazuje różnice w zastosowaniu tekstur poza ekranem:
@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())) } } }
ModulateAlpha
Ta strategia kompozycji moduluje alfa dla każdego z instrukcji rysowania zarejestrowanych w graphicsLayer
. Nie utworzy bufora poza ekranem dla wersji alfa poniżej 1,0f, jeśli nie jest ustawiony atrybut RenderEffect
, dzięki czemu może być bardziej wydajny podczas renderowania w wersji alfa. Efekty nakładania się treści
mogą jednak być inne. W przypadkach, w których z góry wiadomo, że treści się nie nakładają, może to zapewnić większą skuteczność niż CompositingStrategy.Auto
, przy których wartości alfa są mniejsze niż 1.
Poniżej znajdziesz kolejny przykład różnych strategii kompozycji – zastosowanie różnych wersji alfa do różnych części elementów kompozycyjnych oraz zastosowanie strategii Modulate
:
@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() } } } 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 Purple = Color(0xFF7E57C2) val Yellow = Color(0xFFFFCA28) val Red = Color(0xFFEF5350)
Zapisz zawartość elementu kompozycyjnego w bitmapie
Typowym przypadkiem użycia jest utworzenie Bitmap
z funkcji kompozycyjnej. Aby skopiować zawartość kompozytu do funkcji Bitmap
, utwórz GraphicsLayer
za pomocą parametru rememberGraphicsLayer()
.
Przekieruj polecenia rysowania do nowej warstwy za pomocą drawWithContent()
i graphicsLayer.record{}
. Następnie narysuj warstwę w widocznym obszarze roboczym za pomocą drawLayer
:
val coroutineScope = rememberCoroutineScope() val graphicsLayer = rememberGraphicsLayer() Box( modifier = Modifier .drawWithContent { // call record to capture the content in the graphics layer graphicsLayer.record { // draw the contents of the composable into the graphics layer this@drawWithContent.drawContent() } // draw the graphics layer on the visible canvas drawLayer(graphicsLayer) } .clickable { coroutineScope.launch { val bitmap = graphicsLayer.toImageBitmap() // do something with the newly acquired bitmap } } .background(Color.White) ) { Text("Hello Android", fontSize = 26.sp) }
Możesz zapisać tę mapę bitową na dysku i ją udostępnić. Aby dowiedzieć się więcej, zobacz pełny przykładowy fragment kodu. Przed zapisaniem na dysku sprawdź uprawnienia urządzenia.
Niestandardowy modyfikator rysunku
Aby utworzyć własny modyfikator niestandardowy, zaimplementuj interfejs DrawModifier
. Daje Ci to dostęp do obiektu ContentDrawScope
, który jest taki sam jak ten, który jest widoczny podczas korzystania z funkcji Modifier.drawWithContent()
. Następnie można wyodrębnić typowe operacje rysowania do niestandardowych modyfikatorów rysowania, aby wyczyścić kod i zapewnić wygodne otoki, np. Modifier.background()
to wygodna DrawModifier
.
Jeśli na przykład chcesz zaimplementować obiekt Modifier
, który odwraca treści w pionie, możesz utworzyć taki dokument:
class FlippedModifier : DrawModifier { override fun ContentDrawScope.draw() { scale(1f, -1f) { this@draw.drawContent() } } } fun Modifier.flipped() = this.then(FlippedModifier())
Następnie użyj tego odwróconego modyfikatora zastosowanego w elemencie Text
:
Text( "Hello Compose!", modifier = Modifier .flipped() )
Dodatkowe materiały
Więcej przykładów użycia obiektu graphicsLayer
i rysunku niestandardowego znajdziesz w tych materiałach:
Polecane dla Ciebie
- Uwaga: tekst linku wyświetla się, gdy JavaScript jest wyłączony
- Grafika w funkcji tworzenia wiadomości
- Dostosowywanie obrazu {:#customize-image}
- Kotlin w Jetpack Compose