This is my second attempt at the modally focused s...
# codereview
t
This is my second attempt at the modally focused screen. I chose to have another go that was more generic and didn't rely on futzing with a Dialog. My general approach in this case was: • Use a top level "scrim shield capable composition" • Allow child compositions to modally communicate intent to activate the screen wide shield via a CompositionLocalProvider by sharing their bounds AND shape (so we can do more than rectangles) The first video is the preview "test" app that demonstrates the idea. The second is its use in a real world app. Code placed in thread. It's really pretty simple, majority is the actual demo. Feedback and guidance appreciated. I'm learning.
👍 1
Copy code
data class ModalPeekhole(val bounds: Rect, val shape: Shape = RectangleShape)

val LocalPeekhole = compositionLocalOf { mutableStateOf<ModalPeekhole?>(null) }

@SuppressLint("UnrememberedMutableState")
@Composable
fun ModalPeekShield(
   modifier: Modifier = Modifier,
   color: Color = Color.Black.copy(alpha = 0.5f),
   content: @Composable BoxScope.() -> Unit
) {
   CompositionLocalProvider(LocalPeekhole provides mutableStateOf(null)) {
      Box(modifier = modifier.fillMaxSize()) {
         content()
//       AnimatedVisibility(
//          visible = LocalPeekhole.current.value != null,
//          enter = fadeIn(tween(1000)),
//          exit = fadeOut(
//             tween(1000)
//          )
//       ) {
         LocalPeekhole.current.value?.let { hole ->
            ScrimCover(hole, color)
//          }
         }
      }
   }
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun ScrimCover(
   hole: ModalPeekhole, color: Color
) {
   val outline = hole.shape.createOutline(
      size = hole.bounds.size,
      layoutDirection = LocalLayoutDirection.current,
      density = LocalDensity.current
   )
   val cutout = Path().apply {
      addOutline(outline)
      translate(Offset(hole.bounds.left, <http://hole.bounds.top|hole.bounds.top>))
   }
   Canvas(modifier = Modifier
      .fillMaxSize()
      .pointerInteropFilter { event ->
         val center = Offset(event.x, event.y)
         val peephole = Path().apply { addRect(Rect(center = center, 1f)) }
         val isOutside = Path.combine(PathOperation.Intersect, cutout, peephole).isEmpty
         isOutside
      }) {
      val full = Path().apply { addRect(Rect(Offset.Zero, size)) }
      val cover = Path.combine(operation = PathOperation.Difference, path1 = full, path2 = cutout)
      drawPath(path = cover, color = color, style = Fill)
   }
}

// Everything past here is just for demo/example
@Composable
private fun ExperimentScreen(modifier: Modifier = Modifier) {
   ModalPeekShield(modifier = modifier, color = Color.hsv(0.2f, 0.2f, 1f, alpha = 0.75f)) {
      Box(
         modifier = Modifier
            .fillMaxSize()
            .padding(start = 20.dp)
            .background(color = Color.Red.copy(alpha = 0.5f))
      ) {
         Level1()
      }
   }
}


@Composable
private fun Level1() {
   Box(
      modifier = Modifier
         .fillMaxSize()
         .padding(top = 30.dp)
         .background(color = Color.Blue.copy(alpha = 0.5f))
   ) {
      Level2()
   }
}

@Composable
private fun Level2() {
   Box(
      modifier = Modifier
         .fillMaxSize()
         .padding(end = 40.dp)
         .background(color = Color.Green.copy(alpha = 0.5f))
   ) {
      Level3()
   }
}

@Composable
private fun Level3() {
   Box(
      modifier = Modifier
         .fillMaxSize()
         .padding(bottom = 50.dp)
         .background(color = Color.Yellow.copy(alpha = 0.5f))
   ) {
      SomeToggles()
   }
}

@Composable
private fun SomeToggles() {
   Box(modifier = Modifier.fillMaxSize()) {
      Column(
         modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally
      ) {
         LocalToggle(1)
         LocalToggle(2)
         LocalToggle(3)
         LocalToggle(4)
      }
   }
}

@Composable
private fun LocalToggle(index: Int) {
   var selected by remember { mutableStateOf(false) }
   var frame by remember { mutableStateOf(Rect.Zero) }
   val peekhole = LocalPeekhole.current
   RadioButton(selected = selected,
      onClick = {
         selected = selected.not()
         peekhole.value =
            selected.opt({ ModalPeekhole(bounds = frame, shape = CircleShape) }, null)
      },
      modifier = Modifier
         .background(Color.hsv(index.toFloat() / 4f * 360, 1f, 1f))
         .width(150.dp)
         .height(150.dp)
         .onGloballyPositioned { layoutCoordinates ->
            frame = layoutCoordinates.boundsInRoot()
         })
}


@Preview(showBackground = true)
@Composable
private fun ExperimentScreenPreview() {
   StatusDemoTheme {
      ExperimentScreen()
   }
}
1. I wish I could get the scrim to fade in/fade out. But with the AnimatedVisibility present, the whole app visually freezes after the first activation 2. I was rather proud of how I accomplished the non rectangular hit detection by using a Path intersection to test touch point intersection with the peekhole 3. I had to use
pointerInteropFilter
to selectively filter the MotionEvents. I would love to know how to do this with the newer stock coroutine stuff, but that wasn't clear to me at all.
4. I'm not entirely convinced that the idea behind passing the mutable state down the composition tree works. What I want is for a remote/arbitrary child to communicate back to the parent a shape/bounds that should be active. Clearing it back to null when it is "complete" seems trickier, because I think there's cases where the return to null could be missed (if the child recomposed for example)