1 /* <lambda>null2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.compose.ui.platform 18 19 import android.os.Build 20 import androidx.compose.ui.geometry.MutableRect 21 import androidx.compose.ui.geometry.Offset 22 import androidx.compose.ui.geometry.center 23 import androidx.compose.ui.geometry.isUnspecified 24 import androidx.compose.ui.graphics.Canvas 25 import androidx.compose.ui.graphics.CompositingStrategy as OldCompositingStrategy 26 import androidx.compose.ui.graphics.Fields 27 import androidx.compose.ui.graphics.GraphicsContext 28 import androidx.compose.ui.graphics.Matrix 29 import androidx.compose.ui.graphics.Outline 30 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope 31 import androidx.compose.ui.graphics.TransformOrigin 32 import androidx.compose.ui.graphics.drawscope.CanvasDrawScope 33 import androidx.compose.ui.graphics.drawscope.DrawScope 34 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 35 import androidx.compose.ui.graphics.isIdentity 36 import androidx.compose.ui.graphics.layer.CompositingStrategy 37 import androidx.compose.ui.graphics.layer.GraphicsLayer 38 import androidx.compose.ui.graphics.layer.drawLayer 39 import androidx.compose.ui.graphics.layer.setOutline 40 import androidx.compose.ui.internal.checkPreconditionNotNull 41 import androidx.compose.ui.internal.requirePrecondition 42 import androidx.compose.ui.layout.GraphicLayerInfo 43 import androidx.compose.ui.node.OwnedLayer 44 import androidx.compose.ui.ui.FrameRateCategory 45 import androidx.compose.ui.unit.Density 46 import androidx.compose.ui.unit.IntOffset 47 import androidx.compose.ui.unit.IntSize 48 import androidx.compose.ui.unit.LayoutDirection 49 import androidx.compose.ui.unit.toSize 50 51 internal class GraphicsLayerOwnerLayer( 52 private var graphicsLayer: GraphicsLayer, 53 // when we have a context it means the object is created by us and we need to release it 54 private val context: GraphicsContext?, 55 private val ownerView: AndroidComposeView, 56 drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit, 57 invalidateParentLayer: () -> Unit 58 ) : OwnedLayer, GraphicLayerInfo { 59 private var drawBlock: ((canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit)? = drawBlock 60 private var invalidateParentLayer: (() -> Unit)? = invalidateParentLayer 61 62 private var size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE) 63 private var isDestroyed = false 64 private val matrixCache = Matrix() 65 private var inverseMatrixCache: Matrix? = null 66 67 private var isDirty = false 68 set(value) { 69 if (value != field) { 70 field = value 71 ownerView.notifyLayerIsDirty(this, value) 72 } 73 } 74 75 private var density = Density(1f) 76 private var layoutDirection = LayoutDirection.Ltr 77 private val scope = CanvasDrawScope() 78 private var mutatedFields: Int = 0 79 private var transformOrigin: TransformOrigin = TransformOrigin.Center 80 private var outline: Outline? = null 81 private var isMatrixDirty = false 82 private var isInverseMatrixDirty = false 83 private var isIdentity = true 84 override var frameRate: Float = 0f 85 override var isFrameRateFromParent = false 86 87 override fun updateLayerProperties(scope: ReusableGraphicsLayerScope) { 88 val maybeChangedFields = scope.mutatedFields or mutatedFields 89 this.layoutDirection = scope.layoutDirection 90 this.density = scope.graphicsDensity 91 if (maybeChangedFields and Fields.TransformOrigin != 0) { 92 this.transformOrigin = scope.transformOrigin 93 } 94 if (maybeChangedFields and Fields.ScaleX != 0) { 95 graphicsLayer.scaleX = scope.scaleX 96 } 97 if (maybeChangedFields and Fields.ScaleY != 0) { 98 graphicsLayer.scaleY = scope.scaleY 99 } 100 if (maybeChangedFields and Fields.Alpha != 0) { 101 graphicsLayer.alpha = scope.alpha 102 } 103 if (maybeChangedFields and Fields.TranslationX != 0) { 104 graphicsLayer.translationX = scope.translationX 105 } 106 if (maybeChangedFields and Fields.TranslationY != 0) { 107 graphicsLayer.translationY = scope.translationY 108 } 109 if (maybeChangedFields and Fields.ShadowElevation != 0) { 110 graphicsLayer.shadowElevation = scope.shadowElevation 111 // TODO We should somehow move it into the android specific GraphicsLayer 112 // implementation where the code enabling Z is located. It is not yet clear how we 113 // can trigger the full layer invalidation from there when such changes happen, but 114 // seems like we have to figure it out. This issue is tracked in b/333862760 115 if (scope.shadowElevation > 0f && !drawnWithEnabledZ) { 116 // we need to redraw with enabling Z 117 invalidateParentLayer?.invoke() 118 } 119 } 120 if (maybeChangedFields and Fields.AmbientShadowColor != 0) { 121 graphicsLayer.ambientShadowColor = scope.ambientShadowColor 122 } 123 if (maybeChangedFields and Fields.SpotShadowColor != 0) { 124 graphicsLayer.spotShadowColor = scope.spotShadowColor 125 } 126 if (maybeChangedFields and Fields.RotationZ != 0) { 127 graphicsLayer.rotationZ = scope.rotationZ 128 } 129 if (maybeChangedFields and Fields.RotationX != 0) { 130 graphicsLayer.rotationX = scope.rotationX 131 } 132 if (maybeChangedFields and Fields.RotationY != 0) { 133 graphicsLayer.rotationY = scope.rotationY 134 } 135 if (maybeChangedFields and Fields.CameraDistance != 0) { 136 graphicsLayer.cameraDistance = scope.cameraDistance 137 } 138 if (maybeChangedFields and Fields.TransformOrigin != 0) { 139 if (transformOrigin == TransformOrigin.Center) { 140 graphicsLayer.pivotOffset = Offset.Unspecified 141 } else { 142 graphicsLayer.pivotOffset = 143 Offset( 144 transformOrigin.pivotFractionX * size.width, 145 transformOrigin.pivotFractionY * size.height 146 ) 147 } 148 } 149 if (maybeChangedFields and Fields.Clip != 0) { 150 graphicsLayer.clip = scope.clip 151 } 152 if (maybeChangedFields and Fields.RenderEffect != 0) { 153 graphicsLayer.renderEffect = scope.renderEffect 154 } 155 if (maybeChangedFields and Fields.ColorFilter != 0) { 156 graphicsLayer.colorFilter = scope.colorFilter 157 } 158 if (maybeChangedFields and Fields.BlendMode != 0) { 159 graphicsLayer.blendMode = scope.blendMode 160 } 161 if (maybeChangedFields and Fields.CompositingStrategy != 0) { 162 graphicsLayer.compositingStrategy = 163 when (scope.compositingStrategy) { 164 OldCompositingStrategy.Auto -> CompositingStrategy.Auto 165 OldCompositingStrategy.Offscreen -> CompositingStrategy.Offscreen 166 OldCompositingStrategy.ModulateAlpha -> CompositingStrategy.ModulateAlpha 167 else -> throw IllegalStateException("Not supported composition strategy") 168 } 169 } 170 if (maybeChangedFields and Fields.MatrixAffectingFields != 0) { 171 isMatrixDirty = true 172 isInverseMatrixDirty = true 173 } 174 175 var outlineChanged = false 176 177 if (outline != scope.outline) { 178 outlineChanged = true 179 outline = scope.outline 180 updateOutline() 181 } 182 183 mutatedFields = scope.mutatedFields 184 if (maybeChangedFields != 0 || outlineChanged) { 185 triggerRepaint() 186 if (ownerView.isArrEnabled) { 187 ownerView.requestedFrameRate = frameRate 188 } 189 } 190 } 191 192 // TODO: GraphicsLayer api should be doing it on its own every time any layer property 193 // has been updated. This issue is tracked in b/329417380. 194 private fun triggerRepaint() { 195 // onDescendantInvalidated is only supported on O+ 196 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 197 WrapperRenderNodeLayerHelperMethods.onDescendantInvalidated(ownerView) 198 } else { 199 ownerView.invalidate() 200 } 201 } 202 203 private fun updateOutline() { 204 val outline = outline ?: return 205 graphicsLayer.setOutline(outline) 206 if (outline is Outline.Generic && Build.VERSION.SDK_INT < 33) { 207 // before 33 many of the paths are not clipping by rendernode. instead we have to 208 // manually clip on a canvas. it means we have redraw the parent layer when it changes 209 // TODO We should somehow move it into the android specific GraphicsLayer 210 // implementation where the clipping logic is located. 211 // This issue is tracked in b/333862760 212 invalidateParentLayer?.invoke() 213 } 214 } 215 216 override fun isInLayer(position: Offset): Boolean { 217 val x = position.x 218 val y = position.y 219 220 if (graphicsLayer.clip) { 221 return isInOutline(graphicsLayer.outline, x, y) 222 } 223 224 return true 225 } 226 227 override fun move(position: IntOffset) { 228 if (ownerView.isArrEnabled) { 229 ownerView.requestedFrameRate = FrameRateCategory.High.value 230 } 231 graphicsLayer.topLeft = position 232 triggerRepaint() 233 } 234 235 override fun resize(size: IntSize) { 236 if (size != this.size) { 237 if (ownerView.isArrEnabled) { 238 ownerView.requestedFrameRate = FrameRateCategory.High.value 239 } 240 this.size = size 241 invalidate() 242 } 243 } 244 245 private var drawnWithEnabledZ = false 246 247 override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) { 248 updateDisplayList() 249 drawnWithEnabledZ = graphicsLayer.shadowElevation > 0 250 scope.drawContext.also { 251 it.canvas = canvas 252 it.graphicsLayer = parentLayer 253 } 254 scope.drawLayer(graphicsLayer) 255 } 256 257 override fun updateDisplayList() { 258 if (ownerView.isArrEnabled && frameRate != 0f) { 259 ownerView.requestedFrameRate = frameRate 260 } 261 if (isDirty) { 262 if (transformOrigin != TransformOrigin.Center && graphicsLayer.size != size) { 263 graphicsLayer.pivotOffset = 264 Offset( 265 transformOrigin.pivotFractionX * size.width, 266 transformOrigin.pivotFractionY * size.height 267 ) 268 } 269 graphicsLayer.record(density, layoutDirection, size, recordLambda) 270 isDirty = false 271 } 272 } 273 274 private val recordLambda: DrawScope.() -> Unit = { 275 drawIntoCanvas { canvas -> 276 this@GraphicsLayerOwnerLayer.drawBlock?.let { it(canvas, drawContext.graphicsLayer) } 277 } 278 } 279 280 override fun invalidate() { 281 if (!isDirty && !isDestroyed) { 282 ownerView.invalidate() 283 isDirty = true 284 } 285 } 286 287 override fun destroy() { 288 frameRate = 0f 289 isFrameRateFromParent = false 290 drawBlock = null 291 invalidateParentLayer = null 292 isDestroyed = true 293 isDirty = false 294 if (context != null) { 295 context.releaseGraphicsLayer(graphicsLayer) 296 ownerView.recycle(this) 297 } 298 } 299 300 override fun mapOffset(point: Offset, inverse: Boolean): Offset { 301 val matrix = 302 if (inverse) { 303 getInverseMatrix() ?: return Offset.Infinite 304 } else { 305 getMatrix() 306 } 307 return if (isIdentity) { 308 point 309 } else { 310 matrix.map(point) 311 } 312 } 313 314 override fun mapBounds(rect: MutableRect, inverse: Boolean) { 315 val matrix = if (inverse) getInverseMatrix() else getMatrix() 316 if (!isIdentity) { 317 if (matrix == null) { 318 rect.set(0f, 0f, 0f, 0f) 319 } else { 320 matrix.map(rect) 321 } 322 } 323 } 324 325 override fun reuseLayer( 326 drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit, 327 invalidateParentLayer: () -> Unit 328 ) { 329 val context = 330 checkPreconditionNotNull(context) { 331 "currently reuse is only supported when we manage the layer lifecycle" 332 } 333 requirePrecondition(graphicsLayer.isReleased) { 334 "layer should have been released before reuse" 335 } 336 337 // recreate a layer 338 graphicsLayer = context.createGraphicsLayer() 339 isDestroyed = false 340 341 // apply new params 342 this.drawBlock = drawBlock 343 this.invalidateParentLayer = invalidateParentLayer 344 345 // reset mutable variables to their initial values 346 isMatrixDirty = false 347 isInverseMatrixDirty = false 348 isIdentity = true 349 matrixCache.reset() 350 inverseMatrixCache?.reset() 351 transformOrigin = TransformOrigin.Center 352 drawnWithEnabledZ = false 353 size = IntSize(Int.MAX_VALUE, Int.MAX_VALUE) 354 outline = null 355 mutatedFields = 0 356 } 357 358 override fun transform(matrix: Matrix) { 359 matrix.timesAssign(getMatrix()) 360 } 361 362 override fun inverseTransform(matrix: Matrix) { 363 val inverse = getInverseMatrix() 364 if (inverse != null) { 365 matrix.timesAssign(inverse) 366 } 367 } 368 369 override val layerId: Long 370 get() = graphicsLayer.layerId 371 372 override val ownerViewId: Long 373 get() = graphicsLayer.ownerViewId 374 375 private fun getMatrix(): Matrix { 376 updateMatrix() 377 return matrixCache 378 } 379 380 override val underlyingMatrix: Matrix 381 get() = getMatrix() 382 383 private fun getInverseMatrix(): Matrix? { 384 val inverseMatrix = inverseMatrixCache ?: Matrix().also { inverseMatrixCache = it } 385 if (!isInverseMatrixDirty) { 386 if (inverseMatrix[0, 0].isNaN()) { 387 return null 388 } 389 return inverseMatrix 390 } 391 isInverseMatrixDirty = false 392 val matrix = getMatrix() 393 return if (isIdentity) { 394 matrix 395 } else if (matrix.invertTo(inverseMatrix)) { 396 inverseMatrix 397 } else { 398 inverseMatrix[0, 0] = Float.NaN 399 null 400 } 401 } 402 403 private fun updateMatrix() { 404 if (isMatrixDirty) { 405 with(graphicsLayer) { 406 val (x, y) = 407 if (pivotOffset.isUnspecified) { 408 this@GraphicsLayerOwnerLayer.size.toSize().center 409 } else { 410 pivotOffset 411 } 412 413 matrixCache.resetToPivotedTransform( 414 x, 415 y, 416 translationX, 417 translationY, 418 1.0f, 419 rotationX, 420 rotationY, 421 rotationZ, 422 scaleX, 423 scaleY, 424 1.0f 425 ) 426 } 427 isMatrixDirty = false 428 isIdentity = matrixCache.isIdentity() 429 } 430 } 431 } 432