Search code examples
androidkotlinandroid-jetpack-composewatermark

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

Solution

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