blob: 5a05a5d5953e590efa45b524b2284666430fd160 [file]
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.ui.graphics.layer
import android.graphics.Outline as AndroidOutline
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isUnspecified
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.BlurEffect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.asAndroidPath
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.DefaultDensity
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.util.fastRoundToInt
@Suppress("NotCloseable")
actual class GraphicsLayer internal constructor(
internal val impl: GraphicsLayerImpl,
private val layerManager: LayerManager,
ownerViewId: Long
) {
private var density = DefaultDensity
private var layoutDirection = LayoutDirection.Ltr
private var drawBlock: DrawScope.() -> Unit = {}
private var androidOutline: AndroidOutline? = null
private var outlineDirty = true
private var roundRectOutlineTopLeft: Offset = Offset.Zero
private var roundRectOutlineSize: Size = Size.Unspecified
private var roundRectCornerRadius: Float = 0f
private var internalOutline: Outline? = null
private var outlinePath: Path? = null
private var roundRectClipPath: Path? = null
private var usePathForClip = false
// Paint used only in Software rendering scenarios for API 21 when rendering to a Bitmap
private var softwareLayerPaint: Paint? = null
/**
* Tracks the amount of the parent layers currently drawing this layer as a child.
*/
private var parentLayerUsages = 0
/**
* Keeps track of the child layers we currently draw into this layer.
*/
private val childDependenciesTracker = ChildLayerDependenciesTracker()
init {
impl.clip = false
}
/**
* Determines if this [GraphicsLayer] has been released. Any attempts to use a [GraphicsLayer]
* after it has been released is an error.
*/
actual var isReleased: Boolean = false
private set
/**
* [CompositingStrategy] determines whether or not the contents of this layer are rendered into
* an offscreen buffer. This is useful in order to optimize alpha usages with
* [CompositingStrategy.ModulateAlpha] which will skip the overhead of an offscreen buffer but can
* generate different rendering results depending on whether or not the contents of the layer are
* overlapping. Similarly leveraging [CompositingStrategy.Offscreen] is useful in situations where
* creating an offscreen buffer is preferred usually in conjunction with [BlendMode] usage.
*/
actual var compositingStrategy: CompositingStrategy
get() = impl.compositingStrategy
set(value) {
if (impl.compositingStrategy != value) {
impl.compositingStrategy = value
}
}
/**
* Offset in pixels where this [GraphicsLayer] will render within a provided canvas when
* [drawLayer] is called. This is configured by calling [record]
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerTopLeftSample
*/
actual var topLeft: IntOffset = IntOffset.Zero
set(value) {
if (field != value) {
field = value
setPosition(value, size)
}
}
/**
* Size in pixels of the [GraphicsLayer]. By default [GraphicsLayer] contents can draw outside
* of the bounds specified by [topLeft] and [size], however, rasterization of this layer into
* an offscreen buffer will be sized according to the specified size. This is configured
* by calling [record]
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerSizeSample
*/
actual var size: IntSize = IntSize.Zero
private set(value) {
if (field != value) {
field = value
setPosition(topLeft, value)
if (roundRectOutlineSize.isUnspecified) {
outlineDirty = true
configureOutline()
}
}
}
/**
* Alpha of the content of the [GraphicsLayer] between 0f and 1f. Any value between 0f and 1f
* will be translucent, where 0f will cause the layer to be completely invisible and 1f will be
* entirely opaque.
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerAlphaSample
*/
actual var alpha: Float
get() = impl.alpha
set(value) {
if (impl.alpha != value) {
impl.alpha = value
}
}
/**
* BlendMode to use when drawing this layer to the destination in [drawLayer].
* The default is [BlendMode.SrcOver].
* Any value other than [BlendMode.SrcOver] will force this [GraphicsLayer] to use an offscreen
* compositing layer for rendering and is equivalent to using [CompositingStrategy.Offscreen].
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerBlendModeSample
*/
actual var blendMode: BlendMode
get() = impl.blendMode
set(value) {
if (impl.blendMode != value) {
impl.blendMode = value
}
}
/**
* ColorFilter applied when drawing this layer to the destination in [drawLayer].
* Setting of this to any non-null will force this [GraphicsLayer] to use an offscreen
* compositing layer for rendering and is equivalent to using [CompositingStrategy.Offscreen]
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerColorFilterSample
*/
actual var colorFilter: ColorFilter?
get() = impl.colorFilter
set(value) {
if (impl.colorFilter != value) {
impl.colorFilter = value
}
}
/**
* [Offset] in pixels used as the center for any rotation or scale transformation. If this value
* is [Offset.Unspecified], then the center of the [GraphicsLayer] is used relative to [topLeft]
* and [size]
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerScaleAndPivotSample
*/
actual var pivotOffset: Offset = Offset.Unspecified
set(value) {
if (field != value) {
field = value
impl.pivotOffset = value
}
}
/**
* The horizontal scale of the drawn area. Default value is `1`.
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerScaleAndPivotSample
*/
actual var scaleX: Float
get() = impl.scaleX
set(value) {
if (impl.scaleX != value) {
impl.scaleX = value
}
}
/**
* The vertical scale of the drawn area. Default value is `1`.
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerScaleAndPivotSample
*/
actual var scaleY: Float
get() = impl.scaleY
set(value) {
if (impl.scaleY != value) {
impl.scaleY = value
}
}
/**
* Horizontal pixel offset of the layer relative to its left bound. Default value is `0`.
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerTranslateSample
*/
actual var translationX: Float
get() = impl.translationX
set(value) {
if (impl.translationX != value) {
impl.translationX = value
}
}
/**
* Vertical pixel offset of the layer relative to its top bound. Default value is `0`
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerTranslateSample
*/
actual var translationY: Float
get() = impl.translationY
set(value) {
if (impl.translationY != value) {
impl.translationY = value
}
}
/**
* Sets the elevation for the shadow in pixels. With the [shadowElevation] > 0f and
* [Outline] set, a shadow is produced. Default value is `0` and the value must not be
* negative. Configuring a non-zero [shadowElevation] enables clipping of [GraphicsLayer]
* content.
*
* Note that if you provide a non-zero [shadowElevation] and if the passed [Outline] is concave
* the shadow will not be drawn on Android versions less than 10.
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerShadowSample
*/
actual var shadowElevation: Float
get() = impl.shadowElevation
set(value) {
if (impl.shadowElevation != value) {
impl.shadowElevation = value
impl.clip = clip || value > 0f
outlineDirty = true
configureOutline()
}
}
/**
* The rotation, in degrees, of the contents around the horizontal axis in degrees. Default
* value is `0`.
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerRotationX
*/
actual var rotationX: Float
get() = impl.rotationX
set(value) {
if (impl.rotationX != value) {
impl.rotationX = value
}
}
/**
* The rotation, in degrees, of the contents around the vertical axis in degrees. Default
* value is `0`.
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerRotationYWithCameraDistance
*/
actual var rotationY: Float
get() = impl.rotationY
set(value) {
if (impl.rotationY != value) {
impl.rotationY = value
}
}
/**
* The rotation, in degrees, of the contents around the Z axis in degrees. Default value is
* `0`.
*/
actual var rotationZ: Float
get() = impl.rotationZ
set(value) {
if (impl.rotationZ != value) {
impl.rotationZ = value
}
}
/**
* Sets the distance along the Z axis (orthogonal to the X/Y plane on which
* layers are drawn) from the camera to this layer. The camera's distance
* affects 3D transformations, for instance rotations around the X and Y
* axis. If the rotationX or rotationY properties are changed and this view is
* large (more than half the size of the screen), it is recommended to always
* use a camera distance that's greater than the height (X axis rotation) or
* the width (Y axis rotation) of this view.
*
* The distance of the camera from the drawing plane can have an affect on the
* perspective distortion of the layer when it is rotated around the x or y axis.
* For example, a large distance will result in a large viewing angle, and there
* will not be much perspective distortion of the view as it rotates. A short
* distance may cause much more perspective distortion upon rotation, and can
* also result in some drawing artifacts if the rotated view ends up partially
* behind the camera (which is why the recommendation is to use a distance at
* least as far as the size of the view, if the view is to be rotated.)
*
* The distance is expressed in pixels and must always be positive.
* Default value is [DefaultCameraDistance]
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerRotationYWithCameraDistance
*/
actual var cameraDistance: Float
get() = impl.cameraDistance
set(value) {
if (impl.cameraDistance != value) {
impl.cameraDistance = value
}
}
/**
* Determines if the [GraphicsLayer] should be clipped to the rectangular bounds specified by
* [topLeft] and [size] or to the Outline if one is provided. The default is false.
* Note if elevation is provided then clipping will be enabled.
*/
@Suppress("GetterSetterNames")
@get:Suppress("GetterSetterNames")
actual var clip: Boolean
get() = impl.clip
set(value) {
if (impl.clip != value) {
impl.clip = value
outlineDirty = true
configureOutline()
}
}
/**
* Configure the [RenderEffect] to apply to this [GraphicsLayer].
* This will apply a visual effect to the results of the [GraphicsLayer] before it is
* drawn. For example if [BlurEffect] is provided, the contents will be drawn in a separate
* layer, then this layer will be blurred when this [GraphicsLayer] is drawn.
*
* Note this parameter is only supported on Android 12
* and above. Attempts to use this Modifier on older Android versions will be ignored.
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerRenderEffectSample
*/
actual var renderEffect: RenderEffect?
get() = impl.renderEffect
set(value) {
if (impl.renderEffect != value) {
impl.renderEffect = value
}
}
/**
* Configures the [topLeft] and [size] of this [GraphicsLayer]. For covenience in use cases
*
* @param topLeft Offset of the [GraphicsLayer]. For convenience, this is set to [topLeft]
* for use cases where only the [size] is desired to be changed.
* @param size Size of the [GraphicsLayer]. For convenience, this is set to [size]
* for use cases where only the [topLeft] is desired to be changed
*/
private fun setPosition(topLeft: IntOffset, size: IntSize) {
impl.setPosition(topLeft.x, topLeft.y, size)
}
/**
* Constructs the display list of drawing commands into this layer that will be rendered
* when this [GraphicsLayer] is drawn elsewhere with [drawLayer].
* @param density [Density] used to assist in conversions of density independent pixels to raw
* pixels to draw.
* @param layoutDirection [LayoutDirection] of the layout being drawn in.
* @param size [Size] of the [GraphicsLayer]
* @param block lambda that is called to issue drawing commands on this [DrawScope]
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerTopLeftSample
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerBlendModeSample
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerTranslateSample
*/
actual fun record(
density: Density,
layoutDirection: LayoutDirection,
size: IntSize,
block: DrawScope.() -> Unit
) {
this.size = size
this.density = density
this.layoutDirection = layoutDirection
this.drawBlock = block
impl.isInvalidated = true
recordInternal()
}
private fun recordInternal() {
childDependenciesTracker.withTracking(
onDependencyRemoved = { it.onRemovedFromParentLayer() }
) {
impl.record(density, layoutDirection, this, drawBlock)
}
}
private fun addSubLayer(graphicsLayer: GraphicsLayer) {
if (childDependenciesTracker.onDependencyAdded(graphicsLayer)) {
graphicsLayer.onAddedToParentLayer()
}
}
private fun transformCanvas(androidCanvas: android.graphics.Canvas) {
val left = topLeft.x.toFloat()
val top = topLeft.y.toFloat()
val right = topLeft.x + size.width.toFloat()
val bottom = topLeft.y + size.height.toFloat()
// If there is alpha applied, we must render into an offscreen buffer to
// properly blend the contents of this layer against the background content
val layerAlpha = alpha
val layerColorFilter = colorFilter
val layerBlendMode = blendMode
val useSaveLayer = layerAlpha < 1.0f ||
layerBlendMode != BlendMode.SrcOver ||
layerColorFilter != null ||
compositingStrategy == CompositingStrategy.Offscreen
if (useSaveLayer) {
val paint = (softwareLayerPaint ?: Paint().also { softwareLayerPaint = it })
.apply {
alpha = layerAlpha
blendMode = layerBlendMode
colorFilter = layerColorFilter
}
androidCanvas.saveLayer(
left,
top,
right,
bottom,
paint.asFrameworkPaint()
)
} else {
androidCanvas.save()
}
// If we are software rendered we must translate the canvas based on the offset provided
// in the move call which operates directly on the RenderNode
androidCanvas.translate(left, top)
androidCanvas.concat(impl.calculateMatrix())
}
internal fun drawForPersistence(canvas: Canvas) {
if (canvas.nativeCanvas.isHardwareAccelerated) {
recreateDisplayListIfNeeded()
impl.draw(canvas)
}
}
private fun recreateDisplayListIfNeeded() {
// If the displaylist has been discarded from underneath us, attempt to recreate it.
// This can happen if the application resumes from a background state after a trim memory
// callback has been invoked with a level greater than or equal to hidden. During which
// HWUI attempts to cull out resources that can be recreated quickly.
// Because recording instructions invokes the draw lambda again, there can be the potential
// for the objects referenced to be invalid for example in the case of a lazylist removal
// animation for a Composable that has been disposed, but the GraphicsLayer is drawn
// for a transient animation. However, when the application is backgrounded, animations are
// stopped anyway so attempts to recreate the displaylist from the draw lambda should
// be safe as the draw lambdas should still be valid. If not catch potential exceptions
// and continue as UI state would be recreated on resume anyway.
if (!impl.hasDisplayList) {
try {
recordInternal()
} catch (_: Throwable) {
// NO-OP
}
}
}
/**
* Draw the contents of this [GraphicsLayer] into the specified [Canvas]
*/
internal actual fun draw(canvas: Canvas, parentLayer: GraphicsLayer?) {
if (isReleased) {
return
}
recreateDisplayListIfNeeded()
configureOutline()
val useZ = shadowElevation > 0f
if (useZ) {
canvas.enableZ()
}
val androidCanvas = canvas.nativeCanvas
val softwareRendered = !androidCanvas.isHardwareAccelerated
if (softwareRendered) {
androidCanvas.save()
transformCanvas(androidCanvas)
}
val willClipPath = usePathForClip || (softwareRendered && clip)
if (willClipPath) {
canvas.save()
when (val tmpOutline = outline) {
is Outline.Rectangle -> {
canvas.clipRect(tmpOutline.bounds)
}
is Outline.Rounded -> {
val rRectPath = roundRectClipPath?.apply { rewind() }
?: Path().also { roundRectClipPath = it }
rRectPath.addRoundRect(tmpOutline.roundRect)
canvas.clipPath(rRectPath)
}
is Outline.Generic -> {
canvas.clipPath(tmpOutline.path)
}
}
}
parentLayer?.addSubLayer(this)
impl.draw(canvas)
if (willClipPath) {
canvas.restore()
}
if (useZ) {
canvas.disableZ()
}
if (softwareRendered) {
androidCanvas.restore()
}
}
private fun onAddedToParentLayer() {
parentLayerUsages++
}
private fun onRemovedFromParentLayer() {
parentLayerUsages--
discardContentIfReleasedAndHaveNoParentLayerUsages()
}
private var skipOutlineConfiguration = false
private fun configureOutline() {
if (outlineDirty && !skipOutlineConfiguration) {
val outlineIsNeeded = clip || shadowElevation > 0f
if (!outlineIsNeeded) {
impl.setOutline(null)
} else {
val tmpPath = outlinePath
if (tmpPath != null) {
val androidOutline = updatePathOutline(tmpPath).apply {
alpha = this@GraphicsLayer.alpha
}
impl.setOutline(androidOutline)
} else {
val roundRectOutline = obtainAndroidOutline().apply {
resolveOutlinePosition { outlineTopLeft, outlineSize ->
setRoundRect(
outlineTopLeft.x.fastRoundToInt(),
outlineTopLeft.y.fastRoundToInt(),
(outlineTopLeft.x + outlineSize.width).fastRoundToInt(),
(outlineTopLeft.y + outlineSize.height).fastRoundToInt(),
roundRectCornerRadius
)
}
}.apply {
alpha = this@GraphicsLayer.alpha
}
impl.setOutline(roundRectOutline)
}
}
outlineDirty = false
}
}
private inline fun <T> resolveOutlinePosition(block: (Offset, Size) -> T): T {
val layerSize = this.size.toSize()
val rRectTopLeft = roundRectOutlineTopLeft
val rRectSize = roundRectOutlineSize
val outlineSize = if (rRectSize.isUnspecified) {
layerSize
} else {
rRectSize
}
return block(rRectTopLeft, outlineSize)
}
// Suppress deprecation for usage of setConvexPath in favor of setPath on API levels that
// previously only supported convex path outlines
@Suppress("deprecation")
private fun updatePathOutline(path: Path): AndroidOutline {
val resultOutline = obtainAndroidOutline()
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P || path.isConvex) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
OutlineVerificationHelper.setPath(resultOutline, path)
} else {
resultOutline.setConvexPath(path.asAndroidPath())
}
usePathForClip = !resultOutline.canClip()
} else { // Concave outlines are not supported on older API levels
androidOutline?.setEmpty()
usePathForClip = true
impl.isInvalidated = true
}
outlinePath = path
return resultOutline
}
/**
* Helper method to return the previously created [AndroidOutline] instance or creates and
* caches it if it was not created previously.
*/
private fun obtainAndroidOutline(): AndroidOutline =
androidOutline ?: AndroidOutline().also { androidOutline = it }
/**
* Determines if this [GraphicsLayer] has been released. Any attempts to use a [GraphicsLayer]
* after it has been released is an error.
*/
internal fun release() {
if (!isReleased) {
isReleased = true
discardContentIfReleasedAndHaveNoParentLayerUsages()
}
}
private fun discardContentIfReleasedAndHaveNoParentLayerUsages() {
if (isReleased && parentLayerUsages == 0) {
layerManager.release(this)
}
}
/**
* Discards the displaylist of the GraphicsLayer. Used internally
* for management of GraphicsLayer resources
*/
internal fun discardDisplayList() {
// discarding means we don't draw children layer anymore and need to remove dependencies:
childDependenciesTracker.removeDependencies {
it.onRemovedFromParentLayer()
}
impl.discardDisplayList()
}
/**
* The ID of the layer. This is used by tooling to match a layer to the associated
* LayoutNode.
*/
val layerId: Long
get() = impl.layerId
/**
* The uniqueDrawingId of the owner view of this graphics layer. This is used by
* tooling to match a layer to the associated owner View.
*/
var ownerViewId: Long = ownerViewId
private set
actual val outline: Outline
get() {
val tmpOutline = internalOutline
val tmpPath = outlinePath
return if (tmpOutline != null) {
tmpOutline
} else if (tmpPath != null) {
Outline.Generic(tmpPath).also { internalOutline = it }
} else {
resolveOutlinePosition { outlineTopLeft, outlineSize ->
val left = outlineTopLeft.x
val top = outlineTopLeft.y
val right = left + outlineSize.width
val bottom = top + outlineSize.height
val cornerRadius = this.roundRectCornerRadius
if (cornerRadius > 0f) {
Outline.Rounded(
RoundRect(left, top, right, bottom, CornerRadius(cornerRadius))
)
} else {
Outline.Rectangle(Rect(left, top, right, bottom))
}
}.also { internalOutline = it }
}
}
private fun resetOutlineParams() {
internalOutline = null
outlinePath = null
roundRectOutlineSize = Size.Unspecified
roundRectOutlineTopLeft = Offset.Zero
roundRectCornerRadius = 0f
outlineDirty = true
}
/**
* Specifies the given path to be configured as the outline for this [GraphicsLayer].
* When [shadowElevation] is non-zero a shadow is produced using this [Outline].
*
* @param path Path to be used as the Outline for the [GraphicsLayer]
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerOutlineSample
*/
actual fun setPathOutline(path: Path) {
resetOutlineParams()
this.outlinePath = path
configureOutline()
}
/**
* Configures a rounded rect outline for this [GraphicsLayer]. By default, [topLeft] is set to
* [Size.Zero] and [size] is set to [Size.Unspecified] indicating that the outline
* should match the size of the [GraphicsLayer]. When [shadowElevation] is non-zero a shadow
* is produced using an [Outline] created from the round rect parameters provided. Additionally
* if [clip] is true, the contents of this [GraphicsLayer] will be clipped to this geometry.
*
* @param topLeft The top left of the rounded rect outline
* @param size The size of the rounded rect outline
* @param cornerRadius The corner radius of the rounded rect outline
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerRoundRectOutline
*/
actual fun setRoundRectOutline(topLeft: Offset, size: Size, cornerRadius: Float) {
if (this.roundRectOutlineTopLeft != topLeft ||
this.roundRectOutlineSize != size ||
this.roundRectCornerRadius != cornerRadius
) {
resetOutlineParams()
this.roundRectOutlineTopLeft = topLeft
this.roundRectOutlineSize = size
this.roundRectCornerRadius = cornerRadius
configureOutline()
}
}
/**
* Configures a rectangular outline for this [GraphicsLayer]. By default, both [topLeft] and
* [size] are set to [Offset.Unspecified] and [Size.Unspecified] indicating that the outline
* should match the bounds of the [GraphicsLayer]. When [shadowElevation] is non-zero a shadow
* is produced using with an [Outline] created from the rect parameters provided. Additionally
* if [clip] is true, the contents of this [GraphicsLayer] will be clipped to this geometry.
*
* @param topLeft The top left of the rounded rect outline
* @param size The size of the rounded rect outline
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerRectOutline
*/
actual fun setRectOutline(
topLeft: Offset,
size: Size
) {
setRoundRectOutline(topLeft, size, 0f)
}
/**
* Sets the color of the ambient shadow that is drawn when [shadowElevation] > 0f.
*
* By default the shadow color is black. Generally, this color will be opaque so the intensity
* of the shadow is consistent between different graphics layers with different colors.
*
* The opacity of the final ambient shadow is a function of the shadow caster height, the
* alpha channel of the [ambientShadowColor] (typically opaque), and the
* [android.R.attr.ambientShadowAlpha] theme attribute.
*
* Note that this parameter is only supported on Android 9 (Pie) and above. On older versions,
* this property always returns [Color.Black] and setting new values is ignored.
*/
actual var ambientShadowColor: Color = Color.Black
set(value) {
if (field != value) {
impl.ambientShadowColor = value
field = value
}
}
/**
* Sets the color of the spot shadow that is drawn when [shadowElevation] > 0f.
*
* By default the shadow color is black. Generally, this color will be opaque so the intensity
* of the shadow is consistent between different graphics layers with different colors.
*
* The opacity of the final spot shadow is a function of the shadow caster height, the
* alpha channel of the [spotShadowColor] (typically opaque), and the
* [android.R.attr.spotShadowAlpha] theme attribute.
*
* Note that this parameter is only supported on Android 9 (Pie) and above. On older versions,
* this property always returns [Color.Black] and setting new values is ignored.
*/
actual var spotShadowColor: Color = Color.Black
set(value) {
if (field != value) {
impl.spotShadowColor = value
field = value
}
}
/**
* Create an [ImageBitmap] with the contents of this [GraphicsLayer] instance. Note that
* [GraphicsLayer.record] must be invoked first to record drawing operations before invoking
* this method.
*
* @sample androidx.compose.ui.graphics.samples.GraphicsLayerToImageBitmap
*/
actual suspend fun toImageBitmap(): ImageBitmap =
SnapshotImpl.toBitmap(this).asImageBitmap()
internal fun reuse(ownerViewId: Long) {
// apply new owner id
this.ownerViewId = ownerViewId
// mark the layer as not released
isReleased = false
// prepare the implementation to be reused
impl.onReused()
// forget the previous draw lambda
drawBlock = {}
// multiple of the setters can cause configureOutline() calls, however we don't want
// to execute it multiple times, so we set this flag to true
skipOutlineConfiguration = true
// reset properties to the default values
alpha = 1f
blendMode = BlendMode.SrcOver
colorFilter = null
pivotOffset = Offset.Unspecified
scaleX = 1f
scaleY = 1f
translationX = 0f
translationY = 0f
shadowElevation = 0f
rotationX = 0f
rotationY = 0f
rotationZ = 0f
ambientShadowColor = Color.Black
spotShadowColor = Color.Black
cameraDistance = DefaultCameraDistance
renderEffect = null
compositingStrategy = CompositingStrategy.Auto
clip = false
size = IntSize.Zero
topLeft = IntOffset.Zero
setRectOutline()
// unset this flag. if outlineDirty is true we will call configureOutline() again when
// the layer will be drawn for the first time.
skipOutlineConfiguration = false
}
companion object {
private val SnapshotImpl = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
LayerSnapshotV28
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1 &&
SurfaceUtils.isLockHardwareCanvasAvailable()) {
LayerSnapshotV22
} else {
LayerSnapshotV21
}
}
}
internal interface GraphicsLayerImpl {
/**
* The ID of the layer. This is used by tooling to match a layer to the associated
* LayoutNode.
*/
val layerId: Long
/**
* @see GraphicsLayer.compositingStrategy
*/
var compositingStrategy: CompositingStrategy
/**
* @see GraphicsLayer.pivotOffset
*/
var pivotOffset: Offset
/**
* @see GraphicsLayer.alpha
*/
var alpha: Float
/**
* @see GraphicsLayer.blendMode
*/
var blendMode: BlendMode
/**
* @see GraphicsLayer.colorFilter
*/
var colorFilter: ColorFilter?
/**
* @see GraphicsLayer.scaleX
*/
var scaleX: Float
/**
* @see GraphicsLayer.scaleY
*/
var scaleY: Float
/**
* @see GraphicsLayer.translationX
*/
var translationX: Float
/**
* @see GraphicsLayer.translationY
*/
var translationY: Float
/**
* @see GraphicsLayer.shadowElevation
*/
var shadowElevation: Float
/**
* @see GraphicsLayer.ambientShadowColor
*/
var ambientShadowColor: Color
/**
* @see GraphicsLayer.spotShadowColor
*/
var spotShadowColor: Color
/**
* @see GraphicsLayer.rotationX
*/
var rotationX: Float
/**
* @see GraphicsLayer.rotationY
*/
var rotationY: Float
/**
* @see GraphicsLayer.rotationZ
*/
var rotationZ: Float
/**
* @see GraphicsLayer.cameraDistance
*/
var cameraDistance: Float
/**
* @see GraphicsLayer.clip
*/
var clip: Boolean
/**
* @see GraphicsLayer.renderEffect
*/
var renderEffect: RenderEffect?
/**
* Determine whether the GraphicsLayer implementation should invalidate itself
*/
var isInvalidated: Boolean
/**
* @see GraphicsLayer.setPosition
*/
fun setPosition(x: Int, y: Int, size: IntSize)
/**
* @see GraphicsLayer.setPathOutline
* @see GraphicsLayer.setRoundRectOutline
*/
fun setOutline(outline: AndroidOutline?)
/**
* Draw the GraphicsLayer into the provided canvas
*/
fun draw(canvas: Canvas)
/**
* @see GraphicsLayer.record
*/
fun record(
density: Density,
layoutDirection: LayoutDirection,
layer: GraphicsLayer,
block: DrawScope.() -> Unit
)
val hasDisplayList: Boolean
get() = true
/**
* @see GraphicsLayer.discardDisplayList
*/
fun discardDisplayList()
/**
* Calculate the current transformation matrix for the layer implementation
*/
fun calculateMatrix(): android.graphics.Matrix
fun onReused() {}
companion object {
val DefaultDrawBlock: DrawScope.() -> Unit = { drawRect(Color.Transparent) }
}
}
@RequiresApi(Build.VERSION_CODES.R)
internal object OutlineVerificationHelper {
@androidx.annotation.DoNotInline
fun setPath(outline: AndroidOutline, path: Path) {
outline.setPath(path.asAndroidPath())
}
}