圖形修飾符

除了 Canvas 可組合項之外,Compose 還有數個實用的圖形 Modifiers,可協助您繪製自訂內容。這些輔助鍵很實用,因為可將其套用至任何可組合項。

繪圖修飾符

所有繪圖指令都是在 Compose 中使用繪圖輔助鍵完成。Compose 有三個主要繪圖輔助鍵:

繪圖的基本輔助鍵為 drawWithContent,可讓您決定可組合項的繪製順序,以及在輔助鍵內發出的繪圖指令。drawBehinddrawWithContent 周圍的便利包裝函式,其繪圖順序設為可組合元件內容的後方。drawWithCache 會在其中呼叫 onDrawBehindonDrawWithContent,並提供快取機制,以便對其中建立的物件進行快取。

Modifier.drawWithContent:選擇繪圖順序

Modifier.drawWithContent 可讓您針對可組合項的內容前後執行 DrawScope 作業。請務必呼叫 drawContent,然後算繪可組合項的實際內容。透過這項輔助鍵,您可以在自訂繪圖作業之前或之後繪製內容,以便決定作業順序。

例如,如果您想在內容上方顯示放射漸層,以便在使用者介面上建立手電筒孔效果,您可以執行下列操作:

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
}

圖 1:在 Composable 上方使用 Modifier.drawWithContent 以建立手電筒類型使用者介面體驗。

Modifier.drawBehind:在可組合項後方繪圖

Modifier.drawBehind 可讓您針對在螢幕上繪製的可組合內容後方執行 DrawScope 作業。如果您查看 Canvas 的實作,可能會發現這只是 Modifier.drawBehind 周圍的便利包裝函式。

如何在 Text 後方繪製圓角矩形:

Text(
    "Hello Compose!",
    modifier = Modifier
        .drawBehind {
            drawRoundRect(
                Color(0xFFBBAAEE),
                cornerRadius = CornerRadius(10.dp.toPx())
            )
        }
        .padding(4.dp)
)

這會產生以下結果:

使用 Modifier.drawBehind 繪製的文字和背景
圖 2:使用 Modifier.drawBehind 繪製的文字和背景

Modifier.drawWithCache:繪製及快取繪圖物件

Modifier.drawWithCache 會將其建立的物件保留在快取中。只要繪圖區域的大小相同,或者所讀取的任何狀態物件都未變更,則系統會快取物件。這個輔助鍵可用於改善繪製呼叫的效能,避免重新指派繪製時建立的物件 (例如:Brush, Shader, Path 等)。

或者,您也可以使用 remember (在輔助鍵之外) 快取物件。然而,並非如此,因為您不一定能始終存取該組合項。如果物件僅用於繪圖,則使用 drawWithCache 可能會更有效率。

舉例來說,如果您建立 Brush 以繪製 Text 後方的漸層,請使用 drawWithCache 快取 Brush 物件,直到繪圖區域的大小改變:

Text(
    "Hello Compose!",
    modifier = Modifier
        .drawWithCache {
            val brush = Brush.linearGradient(
                listOf(
                    Color(0xFF9E82F0),
                    Color(0xFF42A5F5)
                )
            )
            onDrawBehind {
                drawRoundRect(
                    brush,
                    cornerRadius = CornerRadius(10.dp.toPx())
                )
            }
        }
)

使用 drawWithCache 快取 Brush 物件
圖 3:使用 drawWithCache 快取 Brush 物件

圖形修飾符

Modifier.graphicsLayer:將轉換套用至可組合項

Modifier.graphicsLayer 是一個輔助鍵,將可組合項的繪圖內容繪製到繪圖圖層中。圖層提供幾項不同的函式,例如:

  • 隔離繪圖操作說明 (類似 RenderNode)。作為圖層一部分擷取到的繪圖操作說明可由算繪管道有效地重新發出,而無需重新執行應用程式程式碼。
  • 轉換會套用至圖層中包含的所有繪圖操作說明。
  • 可組合功能的光柵化。當圖層光柵化時,系統會執行其繪圖操作說明,並將輸出內容擷取到螢幕外的緩衝區中。針對後續頁框的緩衝處理,組合此類緩衝區比執行個別操作說明更快,但在套用縮放或旋轉等轉換時,此行為將作為點陣圖。

變形

Modifier.graphicsLayer 提供其繪圖操作說明隔離,舉例來說,您可以使用 Modifier.graphicsLayer 套用各種轉換作業。這些作業可以是動畫或修改,而不需要重新執行繪圖 lambda。

Modifier.graphicsLayer 不會變更可組合元件的測量大小或位置,因為它只會影響繪製階段。也就是說,如果可組合項在版面配置邊界外繪製,可能會重疊其他元件。

下列轉換可使用此輔助鍵套用:

規模:增加大小

scaleXscaleY 分別為水平或垂直方向放大或縮小內容。1.0f 值表示縮放沒有變化,0.5f 值代表尺寸的一半。

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.scaleX = 1.2f
            this.scaleY = 0.8f
        }
)

圖 4:scaleX 和 scaleY 套用到 Image 可組合項
翻譯

translationXtranslationY 可以使用 graphicsLayer 變更,translationX 會將可組合項向左或向右移動。translationY 可組合項可向上或向下移動。

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.translationX = 100.dp.toPx()
            this.translationY = 10.dp.toPx()
        }
)

圖 5:透過 Modifier.graphicsLayer,將 translationX 和 translationY 套用至圖片
旋轉

設定 rotationX 可水平旋轉,rotationY 可垂直旋轉,rotationZ 則設為在 Z 軸旋轉 (標準旋轉)。這個值以度數 (0-360) 為單位。

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "Sunset",
    modifier = Modifier
        .graphicsLayer {
            this.rotationX = 90f
            this.rotationY = 275f
            this.rotationZ = 180f
        }
)

圖 6:透過 Modifier.graphicsLayer 在圖片上設定 rotationX、rotationY 和 rotationZ
來源

您可以指定 transformOrigin,並使用這個來源做為轉換的發生點。到目前為止,所有範例都使用了位於 (0.5f, 0.5f)TransformOrigin.Center。如果您在 (0f, 0f) 指定來源,則轉換會從可組合項的左上角開始。

如果使用 rotationZ 轉換變更來源,可看到項目會圍繞可組合項的左上角旋轉:

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

圖 7:在 TransformOrigin 設定為 0f、0f 的情況下套用旋轉

短片和形狀

形狀會指定內容在 clip = true 時剪輯的外框。在這個範例中,我們設定了兩個方塊,其中有兩個不同的片段:一個使用 graphicsLayer 剪輯變數,另一個則使用便利的包裝函式 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))
    )
}

第一個方塊的內容 (「Hello Compose」文字) 會裁剪成圓形形狀:

已將短片套用到 Box 可組合項
圖 8:將短片套用到 Box 可組合項

然後,如果將 translationY 套用至頂端粉紅色圓形,您會發現可組合項的邊界仍相同,但圓形會落在底圓形下方 (以及其邊界外)。

以 translationY 套用短片,且外框以紅色邊框顯示
圖 9:以 translationY 套用短片,且外框以紅色邊框顯示

如要將可組合項剪輯至其繪製的區域,您可以在輔助鍵鏈結的開頭另外新增 Modifier.clip(RectangleShape)。然後,內容仍會保留在原始邊界內。

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

套用在圖形圖層轉換之上的短片
圖 10:套用在圖形圖層轉換之上的短片

Alpha 測試版

Modifier.graphicsLayer 可用來設定整個圖層的 alpha (透明度)。1.0f 完全不透明,0.0f 隱藏。

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "clock",
    modifier = Modifier
        .graphicsLayer {
            this.alpha = 0.5f
        }
)

已套用 Alpha 的圖片
圖 11:已套用 Alpha 的圖片

組合策略

使用 Alpha 值和透明度可能沒有像變更單一 Alpha 值來得簡單。除了變更 Alpha 以外,您也可以選擇在 graphicsLayer 上設定 CompositingStrategyCompositingStrategy 將決定可組合項內容如何與螢幕上已繪製的其他內容組合在一起。

這些不同策略包括:

自動 (預設)

撰寫策略取決於 graphicsLayer 參數的其餘部分。如果 Alpha 值低於 1.0f 或已設定 RenderEffect,它會將圖層算繪到螢幕外緩衝區。只要 Alpha 值小於 1f,系統就會自動建立合成層以算繪內容,然後將此螢幕外緩衝區繪製到對應的 Alpha。設定 RenderEffect 或過度捲動一律會將內容算繪到螢幕外緩衝區,不論 CompositingStrategy 設定為何。

螢幕外

在算繪至目的地之前,可組合項的內容一律以光柵化為螢幕外紋理或點陣圖。這適用於將 BlendMode 作業套用至遮蓋內容,以及算繪複雜的繪圖操作說明集時的效能。

使用 CompositingStrategy.Offscreen 的範例為透過 BlendModes。請看以下範例,假設您想發出使用 BlendMode.Clear 的繪圖指令,藉此移除 Image 可組合項的某些部分。如果您未將 compositingStrategy 設為 CompositingStrategy.OffscreenBlendMode 會與其下方的所有內容互動。

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

       }
)

透過將 CompositingStrategy 設為 Offscreen,系統會建立執行指令所需的螢幕外紋理 (只是將 BlendMode 套用至這個可組合項的內容)。然後算繪已於螢幕上算繪的內容,不會影響已繪製的內容。

顯示圓形標示的圖片上使用 Modifier.drawWithContent,且應用程式內含 BlendMode.Clear
圖 12:顯示圓形標示的圖片上使用 Modifier.drawWithContent,且應用程式內含 BlendMode.Clear 和 CompositingStrategy.Offscreen

如果您並未使用 CompositingStrategy.Offscreen,則套用 BlendMode.Clear 的結果會清除目的地中的所有像素 (不論是否已設定),讓視窗的算繪緩衝區 (黑色) 變為可見。許多涉及 Alpha 的 BlendModes 在沒有螢幕外緩衝區的情況下無法正常運作。請注意紅色圓環周圍的黑色圓環:

顯示圓形標示的圖片上使用 Modifier.drawWithContent,且帶有 BlendMode.Clear,但未設定 CompositingStrategy
圖 13:顯示圓形標示的圖片上使用 Modifier.drawWithContent,且帶有 BlendMode.Clear,但未設定 CompositingStrategy

深入瞭解:如果應用程式使用半透明的視窗背景,而您沒有使用 CompositingStrategy.OffscreenBlendMode 就會與整個應用程式互動。這會清除下方顯示的所有應用程式或桌布的像素,如以下範例所示:

未設定 CompositingStrategy,搭配具半透明視窗背景的應用程式使用 BlendMode.Clear。透過紅色狀態圓圈周圍的區域顯示粉紅色桌布。
圖 14:未設定 CompositingStrategy,並搭配採用半透明視窗背景的應用程式使用 BlendMode.Clear。請注意,粉紅色桌布如何顯示在紅色狀態圓圈周圍區域。

值得注意的是,使用 CompositingStrategy.Offscreen 時,系統會建立繪圖區域大小的螢幕外紋理,並將其算繪回螢幕上。根據預設,使用此策略完成的所有繪圖指令都會剪輯到這個地區。以下程式碼片段說明改用螢幕外紋理時的差異:

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

CompositingStrategy.Auto 與 CompositingStrategy.Offscreen - 將螢幕外短片剪輯至不會自動執行的區域
圖 15:CompositingStrategy.Auto 與 CompositingStrategy.Offscreen - 將螢幕外短片剪輯至自動功能未執行的區域
ModulateAlpha

撰寫策略會針對 graphicsLayer 中記錄的每個繪圖操作說明修改 Alpha 版。除非設定了 RenderEffect,否則這項策略不會為低於 1.0f 的 Alpha 建立螢幕外緩衝區,以便提升 Alpha 算繪效率。但針對重疊內容提供不同的結果。對於事先知道內容不會重疊的使用情境,與 Alpha 值小於 1 的 CompositingStrategy.Auto 相比,此策略可提供更好的效能。

以下為不同組合策略的另一個範例:將不同的 Alpha 套用至可組合項的不同部分,然後套用 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)

ModulateAlpha 會將 Alpha 設定值套用至每個單獨繪圖指令
圖 16: ModulateAlpha 會將 Alpha 設定值套用至每個單獨繪圖指令

將可組合項的內容寫入點陣圖

常見的用途是從可組合項建立 Bitmap。如要將可組合項內容複製到 Bitmap,請使用 rememberGraphicsLayer() 建立 GraphicsLayer

使用 drawWithContent()graphicsLayer.record{},將繪圖指令重新導向至新圖層。然後使用 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)
}

您可以將點陣圖儲存至磁碟並共用。詳情請參閱完整程式碼片段範例。儲存到磁碟前,請務必檢查裝置的權限。

自訂繪圖輔助鍵

如要建立自訂輔助鍵,請實作 DrawModifier 介面。這會讓您存取 ContentDrawScope,這與使用 Modifier.drawWithContent() 時公開的內容相同。接著,您可以將常見的繪圖作業擷取至自訂繪圖修飾符,藉此清理程式碼並提供方便的包裝函式;例如,Modifier.background() 是便捷的 DrawModifier

舉例來說,如果您想實作可垂直翻轉的 Modifier,您可以按照以下方式建立:

class FlippedModifier : DrawModifier {
    override fun ContentDrawScope.draw() {
        scale(1f, -1f) {
            this@draw.drawContent()
        }
    }
}

fun Modifier.flipped() = this.then(FlippedModifier())

然後在 Text 上套用這個翻轉輔助鍵:

Text(
    "Hello Compose!",
    modifier = Modifier
        .flipped()
)

文字的自訂翻轉輔助鍵
圖 17:文字的自訂翻轉輔助鍵

其他資源

如需更多使用 graphicsLayer 和自訂繪圖的範例,請參閱下列資源: