commonMain.androidx.compose.ui.graphics.painter.Painter.kt Maven / Gradle / Ivy
/*
* Copyright 2019 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.painter
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.inset
import androidx.compose.ui.graphics.withSaveLayer
import androidx.compose.ui.unit.LayoutDirection
/**
* Abstraction for something that can be drawn. In addition to providing the ability to draw
* into a specified bounded area, [Painter] provides a few high level mechanisms
* that consumers can use to configure how the content is drawn. These include alpha,
* ColorFilter, and RTL
*
* Implementations should provide a meaningful equals method that compares values of
* different [Painter] subclasses and not rely on just referential equality
*/
abstract class Painter {
/**
* Optional [Paint] used to draw contents into an offscreen layer in order to apply
* alpha or [ColorFilter] parameters accordingly. If no alpha or [ColorFilter] is
* provided or the [Painter] implementation implements [applyAlpha] and
* [applyColorFilter] then this paint is not used
*/
private var layerPaint: Paint? = null
/**
* Lazily create a [Paint] object or return the existing instance if it is already allocated
*/
private fun obtainPaint(): Paint {
var target = layerPaint
if (target == null) {
target = Paint()
layerPaint = target
}
return target
}
/**
* Determine if an additional rasterization layer should be used in order to draw the
* [Painter]. This would be necessary if there is an alpha value between 0.0 and 1.0
* and the [Painter] implementation does not handle alpha internally. Similarly
* a layer would be used if a [ColorFilter] is provided and the implementation is not
* handled internally.
*/
private var useLayer = false
/**
* Currently configured ColorFilter
*/
private var colorFilter: ColorFilter? = null
/**
* Optional [ColorFilter] used to modify the source pixels when drawn to the destination
* The default implementation of [Painter] will render it's contents into a separate
* layer then render that layer to the destination with the [ColorFilter] applied.
* Implementations that can handle applying a [ColorFilter] more directly should also
* implement the [applyColorFilter] method and return true
*/
private fun configureColorFilter(colorFilter: ColorFilter?) {
if (this.colorFilter != colorFilter) {
val consumedColorFilter = applyColorFilter(colorFilter)
if (!consumedColorFilter) {
if (colorFilter == null) {
layerPaint?.colorFilter = null
useLayer = false
} else {
obtainPaint().colorFilter = colorFilter
useLayer = true
}
}
this.colorFilter = colorFilter
}
}
/**
* Currently configured alpha/opacity value
*/
private var alpha: Float = DefaultAlpha
/**
* Specify the alpha transparency to apply to the [Painter] when it is drawn into
* the destination. The default implementation within [Painter] will render it's
* contents into an offscreen layer, then draw that layer into the destination with the
* alpha applied.
* Implementations of [Painter] that can handle alpha configuration more optimally
* should implement the [applyAlpha] interface and avoid intermediate layer rendering
* where ever possible. For example, [Painter] implementations that draw a Bitmap
* would be able to draw their contents to the destination with the alpha applied directly
* without needing an offscreen buffer. Additionally, drawing a shape with a color
* applied can modulate the alpha channel directly without requiring a layer to achieve
* the same visual effect
*/
private fun configureAlpha(alpha: Float) {
if (this.alpha != alpha) {
val consumed = applyAlpha(alpha)
if (!consumed) {
if (alpha == DefaultAlpha) {
// Only update the paint parameter if we had it allocated before
layerPaint?.alpha = alpha
useLayer = false
} else {
obtainPaint().alpha = alpha
useLayer = true
}
}
this.alpha = alpha
}
}
private var layoutDirection: LayoutDirection = LayoutDirection.Ltr
/**
* Flag indicating that the contents of the [Painter] should be drawn with
* to support locales with right-to-left languages.
* Implementations of [Painter] that support right to left contents should implement
* the [applyLayoutDirection] method
*/
private fun configureLayoutDirection(rtl: LayoutDirection) {
if (this.layoutDirection != rtl) {
applyLayoutDirection(rtl)
this.layoutDirection = rtl
}
}
private val drawLambda: DrawScope.() -> Unit = { onDraw() }
/**
* Return the intrinsic size of the [Painter].
* If the there is no intrinsic size (i.e. filling bounds with an arbitrary color) return
* [Size.Unspecified].
* If there is no intrinsic size in a single dimension, return [Size] with
* [Float.NaN] in the desired dimension.
* If a [Painter] does not have an intrinsic size, it will always draw within the full
* bounds of the destination
*/
abstract val intrinsicSize: Size
/**
* Implementation of drawing logic for instances of [Painter]. This is invoked
* internally within [draw] after the positioning and configuring the [Painter]
*/
protected abstract fun DrawScope.onDraw()
/**
* Apply the provided alpha value returning true if it was applied successfully,
* or false if it could not be applied
*/
protected open fun applyAlpha(alpha: Float): Boolean = false
/**
* Apply the provided color filter returning true if it was applied successfully,
* or false if it could not be applied
*/
protected open fun applyColorFilter(colorFilter: ColorFilter?): Boolean = false
/**
* Apply the appropriate internal configuration to positioning content with the
* given [LayoutDirection]
*/
protected open fun applyLayoutDirection(layoutDirection: LayoutDirection): Boolean = false
fun DrawScope.draw(
size: Size,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null
) {
configureAlpha(alpha)
configureColorFilter(colorFilter)
configureLayoutDirection(layoutDirection)
// b/156512437 to expose saveLayer on DrawScope
inset(
left = 0.0f,
top = 0.0f,
right = this.size.width - size.width,
bottom = this.size.height - size.height
) {
if (alpha > 0.0f && size.width > 0 && size.height > 0) {
if (useLayer) {
val layerRect = Rect(Offset.Zero, Size(size.width, size.height))
// TODO (b/154550724) njawad replace with RenderNode/Layer API usage
drawIntoCanvas { canvas ->
canvas.withSaveLayer(layerRect, obtainPaint()) {
onDraw()
}
}
} else {
onDraw()
}
}
}
}
}