@@ -5,6 +5,7 @@ import android.graphics.Paint
55import android.graphics.RadialGradient
66import android.graphics.Shader
77import androidx.annotation.StringRes
8+ import androidx.compose.animation.core.Animatable
89import androidx.compose.animation.core.FastOutSlowInEasing
910import androidx.compose.animation.core.LinearEasing
1011import androidx.compose.animation.core.animateFloatAsState
@@ -113,15 +114,15 @@ private const val CAPTURE_GUIDE_VERTICAL_DIAMETER_RATIO = 0.56f
113114// FaceDetectorTransitioner validates centering against the normalized image center.
114115private const val CAPTURE_GUIDE_CENTER_Y_RATIO = 0.5f
115116private const val CAPTURE_GUIDE_SHADOW_ANIMATION_TIME = 180
116- private const val CAPTURE_GUIDE_SHADOW_FEATHER_DP = 44
117- private const val CAPTURE_GUIDE_SHADOW_CLEAR_STOP_MAX = 0.96f
118- private const val CAPTURE_GUIDE_SHADOW_FIRST_VISIBLE_STOP_OFFSET = 0.012f
119- private const val CAPTURE_GUIDE_SHADOW_FIRST_VISIBLE_STOP_MAX = 0.98f
120- private const val CAPTURE_GUIDE_SHADOW_FEATHER_STOP_OFFSET = 0.02f
121- private const val CAPTURE_GUIDE_SHADOW_FEATHER_STOP_MAX = 0.99f
122- private const val CAPTURE_GUIDE_SHADOW_INNER_ALPHA = 0.20f
123- private const val CAPTURE_GUIDE_SHADOW_MID_ALPHA = 0.24f
124- private const val CAPTURE_GUIDE_SHADOW_OUTER_ALPHA = 0.28f
117+ private const val CAPTURE_GUIDE_SHADOW_FEATHER_DP = 34
118+ private const val CAPTURE_GUIDE_SHADOW_FEATHER_LOCATION_MIN = 0.06f
119+ private const val CAPTURE_GUIDE_SHADOW_FEATHER_LOCATION_MAX = 0.96f
120+ private const val CAPTURE_GUIDE_SHADOW_INNER_LOCATION_FACTOR = 0.25f
121+ private const val CAPTURE_GUIDE_SHADOW_MID_LOCATION_FACTOR = 0.6f
122+ private const val CAPTURE_GUIDE_SHADOW_INNER_ALPHA = 0.14f
123+ private const val CAPTURE_GUIDE_SHADOW_MID_ALPHA = 0.28f
124+ private const val CAPTURE_GUIDE_SHADOW_RING_ALPHA = 0.36f
125+ private const val CAPTURE_GUIDE_SHADOW_OUTER_ALPHA = 0.42f
125126private const val CAPTURE_GUIDE_TICK_ALPHA = 0.8f
126127private const val CAPTURE_GUIDE_TICK_SHADOW_ALPHA = 0.3f
127128private const val CAPTURED_SELFIE_OVERLAY_ALPHA = 0.16f
@@ -489,14 +490,10 @@ private fun DrawScope.drawCenteredGuideShadow(
489490 }
490491
491492 val featherRadius = min(verticalRadius + CAPTURE_GUIDE_SHADOW_FEATHER_DP .dp.toPx(), outerRadius)
492- val clearStop = (clearRadius / outerRadius).coerceIn(0f , CAPTURE_GUIDE_SHADOW_CLEAR_STOP_MAX )
493- val firstVisibleStop = min(
494- clearStop + CAPTURE_GUIDE_SHADOW_FIRST_VISIBLE_STOP_OFFSET ,
495- CAPTURE_GUIDE_SHADOW_FIRST_VISIBLE_STOP_MAX
496- )
497- val featherStop = min(
498- max(featherRadius / outerRadius, firstVisibleStop + CAPTURE_GUIDE_SHADOW_FEATHER_STOP_OFFSET ),
499- CAPTURE_GUIDE_SHADOW_FEATHER_STOP_MAX
493+ val gradientStops = centeredGuideShadowGradientStops(
494+ clearRadius = clearRadius,
495+ featherRadius = featherRadius,
496+ outerRadius = outerRadius
500497 )
501498 val shadowOpacity = opacity.coerceIn(0f , 1f )
502499
@@ -506,39 +503,90 @@ private fun DrawScope.drawCenteredGuideShadow(
506503 nativeCanvas.translate(center.x, center.y)
507504 nativeCanvas.scale(scaleX, 1f )
508505
509- val paint = Paint (Paint .ANTI_ALIAS_FLAG ).apply {
510- style = Paint .Style .FILL
511- shader = RadialGradient (
512- 0f ,
513- 0f ,
514- outerRadius,
515- intArrayOf(
516- Color .Transparent .toArgb(),
517- Color .Transparent .toArgb(),
518- Color .Black .copy(alpha = CAPTURE_GUIDE_SHADOW_INNER_ALPHA * shadowOpacity).toArgb(),
519- Color .Black .copy(alpha = CAPTURE_GUIDE_SHADOW_MID_ALPHA * shadowOpacity).toArgb(),
520- Color .Black .copy(alpha = CAPTURE_GUIDE_SHADOW_OUTER_ALPHA * shadowOpacity).toArgb()
521- ),
522- floatArrayOf(
523- 0f ,
524- clearStop,
525- firstVisibleStop,
526- featherStop,
527- 1f
528- ),
529- Shader .TileMode .CLAMP
530- )
531- }
532- val shadowPath = AndroidPath ().apply {
533- fillType = AndroidPath .FillType .EVEN_ODD
534- addCircle(0f , 0f , outerRadius, AndroidPath .Direction .CW )
535- addCircle(0f , 0f , clearRadius, AndroidPath .Direction .CW )
536- }
537- nativeCanvas.drawPath(shadowPath, paint)
506+ nativeCanvas.drawPath(
507+ centeredGuideShadowPath(outerRadius, clearRadius),
508+ centeredGuideShadowPaint(outerRadius, gradientStops, shadowOpacity)
509+ )
538510 nativeCanvas.restore()
539511 }
540512}
541513
514+ private data class CenteredGuideShadowGradientStops (
515+ val clear : Float ,
516+ val inner : Float ,
517+ val mid : Float ,
518+ val ring : Float
519+ )
520+
521+ private fun centeredGuideShadowGradientStops (
522+ clearRadius : Float ,
523+ featherRadius : Float ,
524+ outerRadius : Float
525+ ): CenteredGuideShadowGradientStops {
526+ val shadowSpan = outerRadius - clearRadius
527+ val featherLocation = min(
528+ max(
529+ (featherRadius - clearRadius) / shadowSpan,
530+ CAPTURE_GUIDE_SHADOW_FEATHER_LOCATION_MIN
531+ ),
532+ CAPTURE_GUIDE_SHADOW_FEATHER_LOCATION_MAX
533+ )
534+
535+ fun absoluteStop (relativeLocation : Float ): Float {
536+ return (clearRadius + (shadowSpan * relativeLocation)) / outerRadius
537+ }
538+
539+ return CenteredGuideShadowGradientStops (
540+ clear = clearRadius / outerRadius,
541+ inner = absoluteStop(featherLocation * CAPTURE_GUIDE_SHADOW_INNER_LOCATION_FACTOR ),
542+ mid = absoluteStop(featherLocation * CAPTURE_GUIDE_SHADOW_MID_LOCATION_FACTOR ),
543+ ring = absoluteStop(featherLocation)
544+ )
545+ }
546+
547+ private fun centeredGuideShadowPaint (
548+ outerRadius : Float ,
549+ gradientStops : CenteredGuideShadowGradientStops ,
550+ shadowOpacity : Float
551+ ): Paint {
552+ return Paint (Paint .ANTI_ALIAS_FLAG ).apply {
553+ style = Paint .Style .FILL
554+ shader = RadialGradient (
555+ 0f ,
556+ 0f ,
557+ outerRadius,
558+ intArrayOf(
559+ Color .Transparent .toArgb(),
560+ Color .Transparent .toArgb(),
561+ Color .Black .copy(alpha = CAPTURE_GUIDE_SHADOW_INNER_ALPHA * shadowOpacity).toArgb(),
562+ Color .Black .copy(alpha = CAPTURE_GUIDE_SHADOW_MID_ALPHA * shadowOpacity).toArgb(),
563+ Color .Black .copy(alpha = CAPTURE_GUIDE_SHADOW_RING_ALPHA * shadowOpacity).toArgb(),
564+ Color .Black .copy(alpha = CAPTURE_GUIDE_SHADOW_OUTER_ALPHA * shadowOpacity).toArgb()
565+ ),
566+ floatArrayOf(
567+ 0f ,
568+ gradientStops.clear,
569+ gradientStops.inner,
570+ gradientStops.mid,
571+ gradientStops.ring,
572+ 1f
573+ ),
574+ Shader .TileMode .CLAMP
575+ )
576+ }
577+ }
578+
579+ private fun centeredGuideShadowPath (
580+ outerRadius : Float ,
581+ clearRadius : Float
582+ ): AndroidPath {
583+ return AndroidPath ().apply {
584+ fillType = AndroidPath .FillType .EVEN_ODD
585+ addCircle(0f , 0f , outerRadius, AndroidPath .Direction .CW )
586+ addCircle(0f , 0f , clearRadius, AndroidPath .Direction .CW )
587+ }
588+ }
589+
542590@Composable
543591private fun SelfieStatusBadge (
544592 status : SelfieStatus ,
@@ -585,14 +633,18 @@ private fun SelfieStatusBadge(
585633
586634@Composable
587635private fun CaptureGuide (showCenteredShadow : Boolean ) {
588- val centeredShadowAlpha by animateFloatAsState(
589- targetValue = if (showCenteredShadow) 1f else 0f ,
590- animationSpec = tween(
591- durationMillis = CAPTURE_GUIDE_SHADOW_ANIMATION_TIME ,
592- easing = FastOutSlowInEasing
593- ),
594- label = " centeredShadowAlpha"
595- )
636+ val centeredShadowAlpha = remember { Animatable (0f ) }
637+
638+ LaunchedEffect (showCenteredShadow) {
639+ centeredShadowAlpha.animateTo(
640+ targetValue = if (showCenteredShadow) 1f else 0f ,
641+ animationSpec = tween(
642+ durationMillis = CAPTURE_GUIDE_SHADOW_ANIMATION_TIME ,
643+ easing = FastOutSlowInEasing
644+ )
645+ )
646+ }
647+
596648 Canvas (
597649 modifier = Modifier
598650 .fillMaxSize()
@@ -603,12 +655,12 @@ private fun CaptureGuide(showCenteredShadow: Boolean) {
603655 }
604656
605657 val geometry = captureGuideGeometry()
606- if (centeredShadowAlpha > 0f ) {
658+ if (centeredShadowAlpha.value > 0f ) {
607659 drawCenteredGuideShadow(
608660 center = geometry.center,
609661 horizontalRadius = geometry.horizontalRadius,
610662 verticalRadius = geometry.verticalRadius,
611- opacity = centeredShadowAlpha
663+ opacity = centeredShadowAlpha.value
612664 )
613665 }
614666
0 commit comments