@@ -4,7 +4,9 @@ import android.animation.Animator
44import android.animation.AnimatorListenerAdapter
55import android.animation.ObjectAnimator
66import android.content.Context
7+ import android.graphics.Outline
78import android.view.View
9+ import android.view.ViewOutlineProvider
810import android.view.animation.PathInterpolator
911import androidx.dynamicanimation.animation.DynamicAnimation
1012import androidx.dynamicanimation.animation.SpringAnimation
@@ -23,6 +25,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
2325 private var prevRotate: Float? = null
2426 private var prevRotateX: Float? = null
2527 private var prevRotateY: Float? = null
28+ private var prevBorderRadius: Float? = null
2629
2730 // --- First mount tracking ---
2831 private var isFirstMount: Boolean = true
@@ -48,6 +51,23 @@ class EaseView(context: Context) : ReactViewGroup(context) {
4851 applyTransformOrigin()
4952 }
5053
54+ // --- Border radius (hardware-accelerated via outline clipping) ---
55+ // Animated via ObjectAnimator("borderRadius") — setter invalidates outline each frame.
56+ private var _borderRadius : Float = 0f
57+
58+ fun getBorderRadius (): Float = _borderRadius
59+ fun setBorderRadius (value : Float ) {
60+ if (_borderRadius != value) {
61+ _borderRadius = value
62+ if (value > 0f ) {
63+ clipToOutline = true
64+ } else {
65+ clipToOutline = false
66+ }
67+ invalidateOutline()
68+ }
69+ }
70+
5171 // --- Hardware layer ---
5272 var useHardwareLayer: Boolean = false
5373
@@ -68,6 +88,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
6888 var initialAnimateRotate: Float = 0.0f
6989 var initialAnimateRotateX: Float = 0.0f
7090 var initialAnimateRotateY: Float = 0.0f
91+ var initialAnimateBorderRadius: Float = 0.0f
7192
7293 // --- Pending animate values (buffered per-view, applied in onAfterUpdateTransaction) ---
7394 var pendingOpacity: Float = 1.0f
@@ -78,6 +99,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
7899 var pendingRotate: Float = 0.0f
79100 var pendingRotateX: Float = 0.0f
80101 var pendingRotateY: Float = 0.0f
102+ var pendingBorderRadius: Float = 0.0f
81103
82104 // --- Running animations ---
83105 private val runningAnimators = mutableMapOf<String , ObjectAnimator >()
@@ -97,12 +119,19 @@ class EaseView(context: Context) : ReactViewGroup(context) {
97119 const val MASK_ROTATE = 1 shl 5
98120 const val MASK_ROTATE_X = 1 shl 6
99121 const val MASK_ROTATE_Y = 1 shl 7
100-
122+ const val MASK_BORDER_RADIUS = 1 shl 8
101123 }
102124
103125 init {
104126 // Set camera distance for 3D perspective rotations (rotateX/rotateY)
105127 cameraDistance = resources.displayMetrics.density * 850f
128+
129+ // ViewOutlineProvider reads _borderRadius dynamically — set once, invalidated on each frame.
130+ outlineProvider = object : ViewOutlineProvider () {
131+ override fun getOutline (view : View , outline : Outline ) {
132+ outline.setRoundRect(0 , 0 , view.width, view.height, _borderRadius )
133+ }
134+ }
106135 }
107136
108137 // --- Hardware layer management ---
@@ -140,7 +169,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
140169 }
141170
142171 fun applyPendingAnimateValues () {
143- applyAnimateValues(pendingOpacity, pendingTranslateX, pendingTranslateY, pendingScaleX, pendingScaleY, pendingRotate, pendingRotateX, pendingRotateY)
172+ applyAnimateValues(pendingOpacity, pendingTranslateX, pendingTranslateY, pendingScaleX, pendingScaleY, pendingRotate, pendingRotateX, pendingRotateY, pendingBorderRadius )
144173 }
145174
146175 private fun applyAnimateValues (
@@ -151,7 +180,8 @@ class EaseView(context: Context) : ReactViewGroup(context) {
151180 scaleY : Float ,
152181 rotate : Float ,
153182 rotateX : Float ,
154- rotateY : Float
183+ rotateY : Float ,
184+ borderRadius : Float
155185 ) {
156186 if (pendingBatchAnimationCount > 0 ) {
157187 onTransitionEnd?.invoke(false )
@@ -175,7 +205,8 @@ class EaseView(context: Context) : ReactViewGroup(context) {
175205 (mask and MASK_SCALE_Y != 0 && initialAnimateScaleY != scaleY) ||
176206 (mask and MASK_ROTATE != 0 && initialAnimateRotate != rotate) ||
177207 (mask and MASK_ROTATE_X != 0 && initialAnimateRotateX != rotateX) ||
178- (mask and MASK_ROTATE_Y != 0 && initialAnimateRotateY != rotateY)
208+ (mask and MASK_ROTATE_Y != 0 && initialAnimateRotateY != rotateY) ||
209+ (mask and MASK_BORDER_RADIUS != 0 && initialAnimateBorderRadius != borderRadius)
179210
180211 if (hasInitialAnimation) {
181212 // Set initial values for animated properties
@@ -187,6 +218,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
187218 if (mask and MASK_ROTATE != 0 ) this .rotation = initialAnimateRotate
188219 if (mask and MASK_ROTATE_X != 0 ) this .rotationX = initialAnimateRotateX
189220 if (mask and MASK_ROTATE_Y != 0 ) this .rotationY = initialAnimateRotateY
221+ if (mask and MASK_BORDER_RADIUS != 0 ) setBorderRadius(initialAnimateBorderRadius)
190222
191223 // Animate properties that differ from initial to target
192224 if (mask and MASK_OPACITY != 0 && initialAnimateOpacity != opacity) {
@@ -213,6 +245,9 @@ class EaseView(context: Context) : ReactViewGroup(context) {
213245 if (mask and MASK_ROTATE_Y != 0 && initialAnimateRotateY != rotateY) {
214246 animateProperty(" rotationY" , DynamicAnimation .ROTATION_Y , initialAnimateRotateY, rotateY, loop = true )
215247 }
248+ if (mask and MASK_BORDER_RADIUS != 0 && initialAnimateBorderRadius != borderRadius) {
249+ animateProperty(" borderRadius" , null , initialAnimateBorderRadius, borderRadius, loop = true )
250+ }
216251 } else {
217252 // No initial animation — set target values directly (skip non-animated)
218253 if (mask and MASK_OPACITY != 0 ) this .alpha = opacity
@@ -223,6 +258,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
223258 if (mask and MASK_ROTATE != 0 ) this .rotation = rotate
224259 if (mask and MASK_ROTATE_X != 0 ) this .rotationX = rotateX
225260 if (mask and MASK_ROTATE_Y != 0 ) this .rotationY = rotateY
261+ if (mask and MASK_BORDER_RADIUS != 0 ) setBorderRadius(borderRadius)
226262 }
227263 } else if (transitionType == " none" ) {
228264 // No transition — set values immediately, cancel running animations
@@ -235,6 +271,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
235271 if (mask and MASK_ROTATE != 0 ) this .rotation = rotate
236272 if (mask and MASK_ROTATE_X != 0 ) this .rotationX = rotateX
237273 if (mask and MASK_ROTATE_Y != 0 ) this .rotationY = rotateY
274+ if (mask and MASK_BORDER_RADIUS != 0 ) setBorderRadius(borderRadius)
238275 onTransitionEnd?.invoke(true )
239276 } else {
240277 // Subsequent updates: animate changed properties (skip non-animated)
@@ -277,6 +314,11 @@ class EaseView(context: Context) : ReactViewGroup(context) {
277314 val from = getCurrentValue(" rotationY" )
278315 animateProperty(" rotationY" , DynamicAnimation .ROTATION_Y , from, rotateY)
279316 }
317+
318+ if (prevBorderRadius != null && mask and MASK_BORDER_RADIUS != 0 && prevBorderRadius != borderRadius) {
319+ val from = getCurrentValue(" borderRadius" )
320+ animateProperty(" borderRadius" , null , from, borderRadius)
321+ }
280322 }
281323
282324 prevOpacity = opacity
@@ -287,6 +329,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
287329 prevRotate = rotate
288330 prevRotateX = rotateX
289331 prevRotateY = rotateY
332+ prevBorderRadius = borderRadius
290333 }
291334
292335 private fun getCurrentValue (propertyName : String ): Float = when (propertyName) {
@@ -298,17 +341,18 @@ class EaseView(context: Context) : ReactViewGroup(context) {
298341 " rotation" -> this .rotation
299342 " rotationX" -> this .rotationX
300343 " rotationY" -> this .rotationY
344+ " borderRadius" -> getBorderRadius()
301345 else -> 0f
302346 }
303347
304348 private fun animateProperty (
305349 propertyName : String ,
306- viewProperty : DynamicAnimation .ViewProperty ,
350+ viewProperty : DynamicAnimation .ViewProperty ? ,
307351 fromValue : Float ,
308352 toValue : Float ,
309353 loop : Boolean = false
310354 ) {
311- if (transitionType == " spring" ) {
355+ if (transitionType == " spring" && viewProperty != null ) {
312356 animateSpring(viewProperty, toValue)
313357 } else {
314358 animateTiming(propertyName, fromValue, toValue, loop)
@@ -479,6 +523,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
479523 prevRotate = null
480524 prevRotateX = null
481525 prevRotateY = null
526+ prevBorderRadius = null
482527
483528 this .alpha = 1f
484529 this .translationX = 0f
@@ -488,6 +533,7 @@ class EaseView(context: Context) : ReactViewGroup(context) {
488533 this .rotation = 0f
489534 this .rotationX = 0f
490535 this .rotationY = 0f
536+ setBorderRadius(0f )
491537
492538 isFirstMount = true
493539 transitionLoop = " none"
0 commit comments