Sultan
Sultan

Reputation: 148

How to create watermark text effect in jetpack compose

I would like to create a watermark effect in my app using text as shown in the picture below. I achieved this by using canvas and bitmap, is there any other reliable way to do this?

Here is my composable function

@Composable
fun WaterMark(
  modifier: Modifier = Modifier,
  content: (@Composable BoxScope.() -> Unit)? = null,
) {

  val watermarkText: String = "some mutable text"

  val paint = Paint(Paint.ANTI_ALIAS_FLAG)
  paint.textSize = LocalContext.current.dpToPx(24).toFloat()
  paint.color = PSCoreColours.psCoreColours.onSurface.hashCode()
  paint.textAlign = Paint.Align.LEFT
  paint.alpha = (255 * 0.25).toInt()
  val baseline: Float = -paint.ascent()
  val image: Bitmap = Bitmap.createBitmap(paint.measureText(watermarkText).toInt(),
    (baseline + paint.descent()).toInt(),
    Bitmap.Config.ARGB_8888)

  val canvas = android.graphics.Canvas(image)
  canvas.drawText(watermarkText, 0f, baseline, paint)
  val rotationMatrix: Matrix = Matrix().apply { postRotate(-45f) }

  val rotatedImage: Bitmap = Bitmap.createBitmap(image, 0, 0, image.width, image.height, rotationMatrix, true)

  val pattern: ImageBitmap = rotatedImage.asImageBitmap()

  Box {
    content?.let { it() }
    Canvas(
      modifier = modifier
    ) {
      val totalWidth = size.width / pattern.width
      val totalHeight = size.height / pattern.height
      var x = 0f
      var y = 0f
      for (i in 0..totalHeight.toInt()) {
        y = (i * pattern.height).toFloat()
        for (j in 0..totalWidth.toInt()) {
          x = (j * pattern.width).toFloat()
          drawImage(
            pattern,
            colorFilter = null,
            topLeft = Offset(x, y),
          )
        }
      }
    }
  }
}

Upvotes: 3

Views: 971

Answers (2)

xxfast
xxfast

Reputation: 737

You can do custom layouts in compose for this

private const val SPACING = 100

@Composable
fun Watermark(
  content: @Composable BoxScope.() -> Unit,
) {
  Box {
    content()

    Layout(
      content = {
        // Repeating the placeables, 6 should do for now but we should be able to calculate this too
        repeat(6) { 
          Text(
            text = watermarkText,
            ..
          )
        }
      }
    ) { measurables, constraints ->
      // Measuring all the placables
      val placeables: List<Placeable> = measurables
        .map { measurable -> measurable.measure(constraints) }
        
      layout(constraints.maxWidth, constraints.maxHeight) {
        // Calculating the max width of a placable
        val maxWidth: Double = placeables.maxOf { it.width }.toDouble()

        // Calculating the max width of a tile given the text is rotated
        val tileSize: Int = (constraints.maxWidth / atan(maxWidth)).toInt()
        
        placeables
          .chunked(2)  // Placing 2 columns 
          .forEachIndexed { index, (first, second) ->
            val indexedTileSize: Int = index * tileSize
            first.placeRelativeWithLayer(-SPACING, indexedTileSize + SPACING) { rotationZ = -45f }
            second.placeRelativeWithLayer(tileSize, indexedTileSize) { rotationZ = -45f }
        }
      }
    }
  }
}

Upvotes: 3

Thracian
Thracian

Reputation: 66599

Watermark function creates instance of Paint and Bitmap on each recomposition. You should wrap them with remember as in this answer.

However you might, i think, do what you do fully Compose way without Paint and Bitmap either using Modifier.drawWithContent{} and drawText function of DrawScope and using translate or rotate inside DrawScope.

This is a drawText sample to understand how you can create and store TextLayoutResult remember.

And another sample using Modifier.drawWithContent

You can also try using Modifier.drawWithCache to cache TextLayoutResult in layout phase instead of composition phase which is suggested by Google Compose developer works on Text here

Upvotes: 1

Related Questions