1 /* <lambda>null2 * Copyright 2020 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.annotation.SuppressLint 20 import android.os.Build 21 import android.view.View 22 import android.view.ViewOutlineProvider 23 import androidx.annotation.RequiresApi 24 import androidx.compose.ui.geometry.MutableRect 25 import androidx.compose.ui.geometry.Offset 26 import androidx.compose.ui.graphics.Canvas 27 import androidx.compose.ui.graphics.CanvasHolder 28 import androidx.compose.ui.graphics.CompositingStrategy 29 import androidx.compose.ui.graphics.Fields 30 import androidx.compose.ui.graphics.Matrix 31 import androidx.compose.ui.graphics.Paint 32 import androidx.compose.ui.graphics.Path 33 import androidx.compose.ui.graphics.RectangleShape 34 import androidx.compose.ui.graphics.RenderEffect 35 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope 36 import androidx.compose.ui.graphics.TransformOrigin 37 import androidx.compose.ui.graphics.layer.GraphicsLayer 38 import androidx.compose.ui.graphics.toArgb 39 import androidx.compose.ui.layout.GraphicLayerInfo 40 import androidx.compose.ui.node.OwnedLayer 41 import androidx.compose.ui.unit.IntOffset 42 import androidx.compose.ui.unit.IntSize 43 import java.lang.reflect.Field 44 import java.lang.reflect.Method 45 46 /** View implementation of OwnedLayer. */ 47 @SuppressLint("ViewConstructor") 48 internal class ViewLayer( 49 val ownerView: AndroidComposeView, 50 val container: DrawChildContainer, 51 drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit, 52 invalidateParentLayer: () -> Unit 53 ) : View(ownerView.context), OwnedLayer, GraphicLayerInfo { 54 private var drawBlock: ((canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit)? = drawBlock 55 private var invalidateParentLayer: (() -> Unit)? = invalidateParentLayer 56 57 private val outlineResolver = OutlineResolver() 58 // Value of the layerModifier's clipToBounds property 59 private var clipToBounds = false 60 private var clipBoundsCache: android.graphics.Rect? = null 61 private val manualClipPath: Path? 62 get() = 63 if (!clipToOutline || outlineResolver.outlineClipSupported) { 64 null 65 } else { 66 outlineResolver.clipPath 67 } 68 69 var isInvalidated = false 70 private set(value) { 71 if (value != field) { 72 field = value 73 ownerView.notifyLayerIsDirty(this, value) 74 } 75 } 76 77 private var layerPaint: Paint? = null 78 79 private var drawnWithZ = false 80 private val canvasHolder = CanvasHolder() 81 82 private val matrixCache = LayerMatrixCache(getMatrix) 83 84 override val underlyingMatrix: Matrix 85 get() = matrixCache.calculateMatrix(this) 86 87 override var frameRate: Float = 0f 88 89 override var isFrameRateFromParent = false 90 91 /** 92 * Local copy of the transform origin as GraphicsLayerModifier can be implemented as a model 93 * object. Update this field within [updateLayerProperties] and use it in [resize] or other 94 * methods 95 */ 96 private var mTransformOrigin: TransformOrigin = TransformOrigin.Center 97 98 private var mHasOverlappingRendering = true 99 100 init { 101 setWillNotDraw(false) // we WILL draw 102 container.addView(this) 103 } 104 105 override val layerId: Long = generateViewId().toLong() 106 107 override val ownerViewId: Long 108 get() = 109 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 110 UniqueDrawingIdApi29.getUniqueDrawingId(ownerView) 111 } else { 112 -1 113 } 114 115 @RequiresApi(29) 116 private object UniqueDrawingIdApi29 { 117 @JvmStatic fun getUniqueDrawingId(view: View) = view.uniqueDrawingId 118 } 119 120 /** 121 * Configure the camera distance on the View in pixels. View already has a get/setCameraDistance 122 * API however, that operates in Dp values. 123 */ 124 var cameraDistancePx: Float 125 get() { 126 // View internally converts distance to dp so divide by density here to have 127 // consistent usage of pixels with RenderNode that is backing the View 128 return cameraDistance / resources.displayMetrics.densityDpi 129 } 130 set(value) { 131 // View internally converts distance to dp so multiply by density here to have 132 // consistent usage of pixels with RenderNode that is backing the View 133 cameraDistance = value * resources.displayMetrics.densityDpi 134 } 135 136 private var mutatedFields: Int = 0 137 138 override fun updateLayerProperties(scope: ReusableGraphicsLayerScope) { 139 val maybeChangedFields = scope.mutatedFields or mutatedFields 140 if (maybeChangedFields and Fields.TransformOrigin != 0) { 141 this.mTransformOrigin = scope.transformOrigin 142 this.pivotX = mTransformOrigin.pivotFractionX * width 143 this.pivotY = mTransformOrigin.pivotFractionY * height 144 } 145 if (maybeChangedFields and Fields.ScaleX != 0) { 146 this.scaleX = scope.scaleX 147 } 148 if (maybeChangedFields and Fields.ScaleY != 0) { 149 this.scaleY = scope.scaleY 150 } 151 if (maybeChangedFields and Fields.Alpha != 0) { 152 this.alpha = scope.alpha 153 } 154 if (maybeChangedFields and Fields.TranslationX != 0) { 155 this.translationX = scope.translationX 156 } 157 if (maybeChangedFields and Fields.TranslationY != 0) { 158 this.translationY = scope.translationY 159 } 160 if (maybeChangedFields and Fields.ShadowElevation != 0) { 161 this.elevation = scope.shadowElevation 162 } 163 if (maybeChangedFields and Fields.RotationZ != 0) { 164 this.rotation = scope.rotationZ 165 } 166 if (maybeChangedFields and Fields.RotationX != 0) { 167 this.rotationX = scope.rotationX 168 } 169 if (maybeChangedFields and Fields.RotationY != 0) { 170 this.rotationY = scope.rotationY 171 } 172 if (maybeChangedFields and Fields.CameraDistance != 0) { 173 this.cameraDistancePx = scope.cameraDistance 174 } 175 val wasClippingManually = manualClipPath != null 176 val clipToOutline = scope.clip && scope.shape !== RectangleShape 177 if (maybeChangedFields and (Fields.Clip or Fields.Shape) != 0) { 178 this.clipToBounds = scope.clip && scope.shape === RectangleShape 179 resetClipBounds() 180 this.clipToOutline = clipToOutline 181 } 182 val shapeChanged = 183 outlineResolver.update( 184 scope.outline, 185 scope.alpha, 186 clipToOutline, 187 scope.shadowElevation, 188 scope.size 189 ) 190 if (outlineResolver.cacheIsDirty) { 191 updateOutlineResolver() 192 } 193 val isClippingManually = manualClipPath != null 194 if (wasClippingManually != isClippingManually || (isClippingManually && shapeChanged)) { 195 invalidate() // have to redraw the content 196 } 197 if (!drawnWithZ && elevation > 0) { 198 invalidateParentLayer?.invoke() 199 } 200 if (maybeChangedFields and Fields.MatrixAffectingFields != 0) { 201 matrixCache.invalidate() 202 } 203 if (Build.VERSION.SDK_INT >= 28) { 204 if (maybeChangedFields and Fields.AmbientShadowColor != 0) { 205 ViewLayerVerificationHelper28.setOutlineAmbientShadowColor( 206 this, 207 scope.ambientShadowColor.toArgb() 208 ) 209 } 210 if (maybeChangedFields and Fields.SpotShadowColor != 0) { 211 ViewLayerVerificationHelper28.setOutlineSpotShadowColor( 212 this, 213 scope.spotShadowColor.toArgb() 214 ) 215 } 216 } 217 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 218 if (maybeChangedFields and Fields.RenderEffect != 0) { 219 ViewLayerVerificationHelper31.setRenderEffect(this, scope.renderEffect) 220 } 221 } 222 223 val requireLayer = 224 maybeChangedFields and Fields.ColorFilter != 0 || 225 maybeChangedFields and Fields.BlendMode != 0 226 227 if (maybeChangedFields and Fields.CompositingStrategy != 0 || requireLayer) { 228 val strategy = 229 if (requireLayer) { 230 CompositingStrategy.Offscreen 231 } else { 232 scope.compositingStrategy 233 } 234 235 mHasOverlappingRendering = 236 when (strategy) { 237 CompositingStrategy.Offscreen -> { 238 val paint = 239 if (requireLayer) { 240 obtainLayerPaint() 241 .apply { 242 colorFilter = scope.colorFilter 243 blendMode = scope.blendMode 244 } 245 .asFrameworkPaint() 246 } else { 247 null 248 } 249 setLayerType(LAYER_TYPE_HARDWARE, paint) 250 true 251 } 252 CompositingStrategy.ModulateAlpha -> { 253 setLayerType(LAYER_TYPE_NONE, null) 254 false 255 } 256 else -> { // CompositingStrategy.Auto 257 setLayerType(LAYER_TYPE_NONE, null) 258 true 259 } 260 } 261 } 262 263 mutatedFields = scope.mutatedFields 264 } 265 266 private fun obtainLayerPaint(): Paint = layerPaint ?: Paint().also { layerPaint = it } 267 268 override fun hasOverlappingRendering(): Boolean { 269 return mHasOverlappingRendering 270 } 271 272 override fun isInLayer(position: Offset): Boolean { 273 val x = position.x 274 val y = position.y 275 if (clipToBounds) { 276 return 0f <= x && x < width && 0f <= y && y < height 277 } 278 279 if (clipToOutline) { 280 return outlineResolver.isInOutline(position) 281 } 282 283 return true 284 } 285 286 private fun updateOutlineResolver() { 287 this.outlineProvider = 288 if (outlineResolver.androidOutline != null) { 289 OutlineProvider 290 } else { 291 null 292 } 293 } 294 295 private fun resetClipBounds() { 296 this.clipBounds = 297 if (clipToBounds) { 298 if (clipBoundsCache == null) { 299 clipBoundsCache = android.graphics.Rect(0, 0, width, height) 300 } else { 301 clipBoundsCache!!.set(0, 0, width, height) 302 } 303 clipBoundsCache 304 } else { 305 null 306 } 307 } 308 309 override fun resize(size: IntSize) { 310 val width = size.width 311 val height = size.height 312 if (width != this.width || height != this.height) { 313 pivotX = mTransformOrigin.pivotFractionX * width 314 pivotY = mTransformOrigin.pivotFractionY * height 315 updateOutlineResolver() 316 layout(left, top, left + width, top + height) 317 resetClipBounds() 318 matrixCache.invalidate() 319 } 320 } 321 322 override fun move(position: IntOffset) { 323 val left = position.x 324 325 if (left != this.left) { 326 offsetLeftAndRight(left - this.left) 327 matrixCache.invalidate() 328 } 329 val top = position.y 330 if (top != this.top) { 331 offsetTopAndBottom(top - this.top) 332 matrixCache.invalidate() 333 } 334 } 335 336 override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) { 337 drawnWithZ = elevation > 0f 338 if (drawnWithZ) { 339 canvas.enableZ() 340 } 341 container.drawChild(canvas, this, drawingTime) 342 if (drawnWithZ) { 343 canvas.disableZ() 344 } 345 } 346 347 override fun dispatchDraw(canvas: android.graphics.Canvas) { 348 canvasHolder.drawInto(canvas) { 349 var didClip = false 350 val clipPath = manualClipPath 351 if (clipPath != null || !canvas.isHardwareAccelerated) { 352 didClip = true 353 save() 354 outlineResolver.clipToOutline(this) 355 } 356 drawBlock?.invoke(this, null) 357 if (didClip) { 358 restore() 359 } 360 } 361 isInvalidated = false 362 } 363 364 override fun invalidate() { 365 if (!isInvalidated) { 366 isInvalidated = true 367 super.invalidate() 368 ownerView.invalidate() 369 } 370 } 371 372 override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {} 373 374 override fun destroy() { 375 isInvalidated = false 376 ownerView.requestClearInvalidObservations() 377 drawBlock = null 378 invalidateParentLayer = null 379 380 val recycle = ownerView.recycle(this@ViewLayer) 381 382 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M || shouldUseDispatchDraw || !recycle) { 383 container.removeViewInLayout(this) 384 } else { 385 visibility = GONE 386 } 387 } 388 389 override fun updateDisplayList() { 390 if (isInvalidated && !shouldUseDispatchDraw) { 391 updateDisplayList(this) 392 isInvalidated = false 393 } 394 } 395 396 override fun forceLayout() { 397 // Don't do anything. These Views are treated as RenderNodes, so a forced layout 398 // should not do anything. If we keep this, we get more redrawing than is necessary. 399 } 400 401 override fun mapOffset(point: Offset, inverse: Boolean): Offset { 402 return if (inverse) { 403 matrixCache.mapInverse(this, point) 404 } else { 405 matrixCache.map(this, point) 406 } 407 } 408 409 override fun mapBounds(rect: MutableRect, inverse: Boolean) { 410 if (inverse) { 411 matrixCache.mapInverse(this, rect) 412 } else { 413 matrixCache.map(this, rect) 414 } 415 } 416 417 override fun reuseLayer( 418 drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit, 419 invalidateParentLayer: () -> Unit 420 ) { 421 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M || shouldUseDispatchDraw) { 422 container.addView(this) 423 } else { 424 visibility = VISIBLE 425 } 426 matrixCache.reset() 427 clipToBounds = false 428 drawnWithZ = false 429 mTransformOrigin = TransformOrigin.Center 430 this.drawBlock = drawBlock 431 this.invalidateParentLayer = invalidateParentLayer 432 isInvalidated = false 433 } 434 435 override fun transform(matrix: Matrix) { 436 matrix.timesAssign(matrixCache.calculateMatrix(this)) 437 } 438 439 override fun inverseTransform(matrix: Matrix) { 440 val inverse = matrixCache.calculateInverseMatrix(this) 441 if (inverse != null) { 442 matrix.timesAssign(inverse) 443 } 444 } 445 446 companion object { 447 private val getMatrix: (View, android.graphics.Matrix) -> Unit = { view, matrix -> 448 val newMatrix = view.matrix 449 matrix.set(newMatrix) 450 } 451 452 val OutlineProvider = 453 object : ViewOutlineProvider() { 454 override fun getOutline(view: View, outline: android.graphics.Outline) { 455 view as ViewLayer 456 outline.set(view.outlineResolver.androidOutline!!) 457 } 458 } 459 private var updateDisplayListIfDirtyMethod: Method? = null 460 private var recreateDisplayList: Field? = null 461 var hasRetrievedMethod = false 462 private set 463 464 var shouldUseDispatchDraw = false 465 internal set // internal so that tests can use it. 466 467 @SuppressLint("BanUncheckedReflection") 468 fun updateDisplayList(view: View) { 469 try { 470 if (!hasRetrievedMethod) { 471 hasRetrievedMethod = true 472 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { 473 updateDisplayListIfDirtyMethod = 474 View::class.java.getDeclaredMethod("updateDisplayListIfDirty") 475 recreateDisplayList = 476 View::class.java.getDeclaredField("mRecreateDisplayList") 477 } else { 478 val getDeclaredMethod = 479 Class::class 480 .java 481 .getDeclaredMethod( 482 "getDeclaredMethod", 483 String::class.java, 484 arrayOf<Class<*>>()::class.java 485 ) 486 updateDisplayListIfDirtyMethod = 487 getDeclaredMethod.invoke( 488 View::class.java, 489 "updateDisplayListIfDirty", 490 emptyArray<Class<*>>() 491 ) as Method? 492 val getDeclaredField = 493 Class::class 494 .java 495 .getDeclaredMethod("getDeclaredField", String::class.java) 496 recreateDisplayList = 497 getDeclaredField.invoke(View::class.java, "mRecreateDisplayList") 498 as Field? 499 } 500 updateDisplayListIfDirtyMethod?.isAccessible = true 501 recreateDisplayList?.isAccessible = true 502 } 503 recreateDisplayList?.setBoolean(view, true) 504 updateDisplayListIfDirtyMethod?.invoke(view) 505 } catch (_: Throwable) { 506 shouldUseDispatchDraw = true 507 } 508 } 509 } 510 } 511 512 @RequiresApi(Build.VERSION_CODES.S) 513 private object ViewLayerVerificationHelper31 { 514 setRenderEffectnull515 fun setRenderEffect(view: View, target: RenderEffect?) { 516 view.setRenderEffect(target?.asAndroidRenderEffect()) 517 } 518 } 519 520 @RequiresApi(Build.VERSION_CODES.P) 521 private object ViewLayerVerificationHelper28 { 522 setOutlineAmbientShadowColornull523 fun setOutlineAmbientShadowColor(view: View, target: Int) { 524 view.outlineAmbientShadowColor = target 525 } 526 setOutlineSpotShadowColornull527 fun setOutlineSpotShadowColor(view: View, target: Int) { 528 view.outlineSpotShadowColor = target 529 } 530 } 531