blob: 051250b830bc13880ac3cb25fcc0187ed3337c96 [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.Canvas
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import android.os.Build
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.compose.testutils.assertPixelColor
import androidx.compose.testutils.captureToImage
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.center
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.BlurEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter.Companion.tint
import androidx.compose.ui.graphics.GraphicsContext
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PixelMap
import androidx.compose.ui.graphics.TestActivity
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.compositeOver
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.drawscope.translate
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection.Ltr
import androidx.compose.ui.unit.center
import androidx.compose.ui.unit.toIntSize
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.test.assertNotNull
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class AndroidGraphicsLayerTest {
companion object {
const val TEST_WIDTH = 600
const val TEST_HEIGHT = 400
val TEST_SIZE = IntSize(TEST_WIDTH, TEST_HEIGHT)
}
@Test
fun testGraphicsLayerBitmap() {
lateinit var layer: GraphicsLayer
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
assertEquals(IntSize.Zero, this.size)
record {
drawRect(
Color.Red,
size = size / 2f
)
drawRect(
Color.Blue,
topLeft = Offset(size.width / 2f, 0f),
size = size / 2f
)
drawRect(
Color.Green,
topLeft = Offset(0f, size.height / 2f),
size = size / 2f
)
drawRect(
Color.Black,
topLeft = Offset(size.width / 2f, size.height / 2f),
size = size / 2f
)
}
}
},
verify = {
val bitmap: ImageBitmap = layer.toImageBitmap()
assertNotNull(bitmap)
assertEquals(TEST_SIZE, IntSize(bitmap.width, bitmap.height))
bitmap.toPixelMap().verifyQuadrants(
Color.Red,
Color.Blue,
Color.Green,
Color.Black
)
}
)
}
@Test
fun testGraphicsLayerBitmapAfterDependencyReleased() {
class ColorProvider {
val color: Color = Color.Red
}
var provider: ColorProvider? = ColorProvider()
var graphicsLayer: GraphicsLayer? = null
graphicsLayerTest(
block = { graphicsContext ->
graphicsContext.createGraphicsLayer().apply {
graphicsLayer = this
assertEquals(IntSize.Zero, this.size)
record {
drawRect(provider!!.color)
}
}
},
verify = {
// Nulling out the dependency here should be safe despite attempting to obtain an
// ImageBitmap afterwards
provider = null
graphicsLayer!!.toImageBitmap().toPixelMap().verifyQuadrants(
Color.Red,
Color.Red,
Color.Red,
Color.Red
)
}
)
}
@Test
fun testDrawLayer() {
var layer: GraphicsLayer? = null
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
assertEquals(IntSize.Zero, this.size)
record {
drawRect(Color.Red)
}
}
drawLayer(layer!!)
},
verify = {
assertEquals(TEST_SIZE, layer!!.size)
assertEquals(IntOffset.Zero, layer!!.topLeft)
it.verifyQuadrants(Color.Red, Color.Red, Color.Red, Color.Red)
}
)
}
@Test
fun testDrawAfterDiscard() {
var layer: GraphicsLayer? = null
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
assertEquals(IntSize.Zero, this.size)
record {
drawRect(Color.Red)
}
discardDisplayList()
}
drawLayer(layer!!)
},
verify = {
assertEquals(TEST_SIZE, layer!!.size)
assertEquals(IntOffset.Zero, layer!!.topLeft)
it.verifyQuadrants(Color.Red, Color.Red, Color.Red, Color.Red)
}
)
}
// this test is failing on API 21 as there toImageBitmap() is using software rendering
// and we reverted the software rendering b/333866398
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP_MR1)
@Test
fun testPersistenceDrawAfterHwuiDiscardsDisplaylists() {
// Layer persistence calls should not fail even if the DisplayList is discarded beforehand
// This differs from testDrawAfterDiscard as this invokes the internal discardDisplaylist
// call in order to mirror the corresponding system call made to cull out displaylists
// without updating GraphicsLayer internal state
graphicsLayerTest(
block = { graphicsContext ->
val layer = graphicsContext.createGraphicsLayer().apply {
assertEquals(IntSize.Zero, this.size)
record {
drawRect(Color.Red)
}
this.impl.discardDisplayList()
}
drawIntoCanvas { layer.drawForPersistence(it) }
},
verify = {
it.verifyQuadrants(Color.Red, Color.Red, Color.Red, Color.Red)
}
)
}
@Test
fun testRecordLayerWithSize() {
graphicsLayerTest(
block = { graphicsContext ->
val layer = graphicsContext.createGraphicsLayer().apply {
record(IntSize(TEST_WIDTH / 2, TEST_HEIGHT / 2)) {
drawRect(Color.Red)
}
}
drawLayer(layer)
},
verify = {
it.verifyQuadrants(Color.Red, Color.Black, Color.Black, Color.Black)
}
)
}
@Test
fun testRecordLayerWithOffset() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset(TEST_WIDTH / 2, TEST_HEIGHT / 2)
val size = IntSize(TEST_WIDTH, TEST_HEIGHT)
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(Color.Red)
}
this.topLeft = topLeft
}
drawLayer(layer!!)
},
verify = {
assertEquals(topLeft, layer!!.topLeft)
assertEquals(size, layer!!.size)
it.verifyQuadrants(Color.Black, Color.Black, Color.Black, Color.Red)
}
)
}
@Test
fun testSetOffset() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset(4, 4)
val size = IntSize(TEST_WIDTH, TEST_HEIGHT)
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
inset(0f, 0f, -4f, -4f) {
drawRect(Color.Red)
}
}
this.topLeft = topLeft
}
drawLayer(layer!!)
},
verify = {
assertEquals(topLeft, layer!!.topLeft)
assertEquals(size, layer!!.size)
assertEquals(Color.Red, it[topLeft.x + 1, topLeft.y + 1])
assertEquals(Color.Black, it[topLeft.x + 1, topLeft.y - 1])
assertEquals(Color.Black, it[topLeft.x - 1, topLeft.y + 1])
assertEquals(Color.Red, it[size.width - 2, size.height - 2])
}
)
}
@Test
fun testSetAlpha() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset.Zero
val size = TEST_SIZE
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(Color.Red)
}
alpha = 0.5f
}
drawLayer(layer!!)
},
verify = {
assertEquals(topLeft, layer!!.topLeft)
assertEquals(size, layer!!.size)
val compositedColor = Color.Red.copy(0.5f).compositeOver(Color.Black)
it.verifyQuadrants(
compositedColor,
compositedColor,
compositedColor,
compositedColor
)
}
)
}
@Test
fun testSetScaleX() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset.Zero
val size = TEST_SIZE
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(
Color.Red,
size = Size(this.size.width / 2, this.size.height / 2)
)
}
scaleX = 2f
pivotOffset = Offset.Zero
}
drawLayer(layer!!)
},
verify = {
assertEquals(topLeft, layer!!.topLeft)
assertEquals(size, layer!!.size)
it.verifyQuadrants(Color.Red, Color.Red, Color.Black, Color.Black)
}
)
}
@Test
fun testSetScaleY() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset.Zero
val size = TEST_SIZE
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(
Color.Red,
size = Size(this.size.width / 2, this.size.height / 2)
)
}
scaleY = 2f
pivotOffset = Offset.Zero
}
drawLayer(layer!!)
},
verify = {
assertEquals(topLeft, layer!!.topLeft)
assertEquals(size, layer!!.size)
it.verifyQuadrants(Color.Red, Color.Black, Color.Red, Color.Black)
}
)
}
@Test
fun testDefaultPivot() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset.Zero
val size = TEST_SIZE
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
inset(this.size.width / 4, this.size.height / 4) {
drawRect(Color.Red)
}
}
scaleY = 2f
scaleX = 2f
}
drawLayer(layer!!)
},
verify = {
assertEquals(topLeft, layer!!.topLeft)
assertEquals(size, layer!!.size)
it.verifyQuadrants(Color.Red, Color.Red, Color.Red, Color.Red)
}
)
}
@Test
fun testBottomRightPivot() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset.Zero
val size = TEST_SIZE
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(Color.Red)
}
scaleY = 0.5f
scaleX = 0.5f
pivotOffset = Offset(this.size.width.toFloat(), this.size.height.toFloat())
}
drawLayer(layer!!)
},
verify = {
assertEquals(topLeft, layer!!.topLeft)
assertEquals(size, layer!!.size)
it.verifyQuadrants(Color.Black, Color.Black, Color.Black, Color.Red)
}
)
}
@Test
fun testResettingToDefaultPivot() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset.Zero
val size = TEST_SIZE
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
inset(this.size.width / 4, this.size.height / 4) {
drawRect(Color.Red)
}
}
scaleY = 2f
scaleX = 2f
// first set to some custom value
pivotOffset = Offset(this.size.width.toFloat(), this.size.height.toFloat())
// and then get back to the default
pivotOffset = Offset.Unspecified
}
drawLayer(layer!!)
},
verify = {
assertEquals(topLeft, layer!!.topLeft)
assertEquals(size, layer!!.size)
it.verifyQuadrants(Color.Red, Color.Red, Color.Red, Color.Red)
}
)
}
@Test
fun testTranslationX() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset.Zero
val size = TEST_SIZE
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(Color.Red, size = this.size / 2f)
}
translationX = this.size.width / 2f
}
drawLayer(layer!!)
},
verify = {
assertEquals(topLeft, layer!!.topLeft)
assertEquals(size, layer!!.size)
it.verifyQuadrants(Color.Black, Color.Red, Color.Black, Color.Black)
}
)
}
@Test
fun testRectOutlineWithNonZeroTopLeft() {
graphicsLayerTest(
block = { graphicsContext ->
var layerSize = Size.Zero
val layer = graphicsContext.createGraphicsLayer().apply {
record {
layerSize = this.size
drawRect(Color.Red, size = this.size / 2f)
}
topLeft = IntOffset(20, 30)
setRectOutline()
}
drawLayer(layer)
val outline = layer.outline
assertEquals(Rect(0f, 0f, layerSize.width, layerSize.height), outline.bounds)
}
)
}
@Test
fun testRoundRectOutlineWithNonZeroTopLeft() {
graphicsLayerTest(
block = { graphicsContext ->
var layerSize = Size.Zero
val layer = graphicsContext.createGraphicsLayer().apply {
record {
layerSize = this.size
drawRect(Color.Red, size = this.size / 2f)
}
topLeft = IntOffset(20, 30)
setRoundRectOutline()
}
drawLayer(layer)
val outline = layer.outline
assertEquals(Rect(0f, 0f, layerSize.width, layerSize.height), outline.bounds)
}
)
}
@Test
fun testRecordOverwritesPreviousRecord() {
graphicsLayerTest(
block = { graphicsContext ->
val layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(Color.Red)
}
}
layer.record {
drawRect(Color.Blue)
}
drawLayer(layer)
},
verify = {
it.verifyQuadrants(Color.Blue, Color.Blue, Color.Blue, Color.Blue)
}
)
}
@Test
fun testTranslationY() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset.Zero
val size = TEST_SIZE
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(Color.Red, size = this.size / 2f)
}
translationY = this.size.height / 2f
}
drawLayer(layer!!)
},
verify = {
assertEquals(topLeft, layer!!.topLeft)
assertEquals(size, layer!!.size)
it.verifyQuadrants(Color.Black, Color.Black, Color.Red, Color.Black)
}
)
}
@Test
fun testRotationX() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset.Zero
val size = TEST_SIZE
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(
Color.Red,
size = Size(this.size.width, this.size.height / 2)
)
}
rotationX = 45f
}
drawLayer(layer!!)
},
verify = {
assertEquals(topLeft, layer!!.topLeft)
assertEquals(size, layer!!.size)
assertEquals(Color.Red, it[size.width / 2, size.height / 4])
assertEquals(Color.Black, it[size.width / 2, size.height / 2 + 2])
assertEquals(Color.Black, it[4, size.height / 4])
assertEquals(Color.Black, it[size.width - 4, size.height / 4])
}
)
}
@Test
fun testRotationY() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset.Zero
val size = TEST_SIZE
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(Color.Red)
}
pivotOffset = Offset(0f, this.size.height / 2f)
rotationY = 45f
}
drawLayer(layer!!)
},
verify = {
assertEquals(topLeft, layer!!.topLeft)
assertEquals(size, layer!!.size)
assertEquals(Color.Red, it[2, size.height / 2])
assertEquals(Color.Red, it[size.width / 4, size.height / 2])
assertEquals(Color.Black, it[size.width / 2, size.height / 2])
assertEquals(Color.Black, it[size.width / 4, 4])
assertEquals(Color.Black, it[size.width / 4, size.height - 4])
}
)
}
@Test
fun testRotationZ() {
var layer: GraphicsLayer? = null
val topLeft = IntOffset.Zero
val size = TEST_SIZE
val rectSize = 100
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(
Color.Red,
topLeft = Offset(
this.size.width / 2f - rectSize / 2f,
this.size.height / 2 - rectSize / 2f
),
Size(rectSize.toFloat(), rectSize.toFloat())
)
}
rotationZ = 45f
}
drawLayer(layer!!)
},
verify = {
assertEquals(topLeft, layer!!.topLeft)
assertEquals(size, layer!!.size)
it.verifyQuadrants(Color.Black, Color.Black, Color.Black, Color.Black)
assertEquals(Color.Red, it[size.width / 2, size.height / 2])
assertEquals(Color.Red, it[size.width / 2, size.height / 2 - rectSize / 2])
assertEquals(Color.Red, it[size.width / 2, size.height / 2 + rectSize / 2])
assertEquals(Color.Red, it[size.width / 2 - rectSize / 2, size.height / 2])
assertEquals(Color.Red, it[size.width / 2 + rectSize / 2, size.height / 2])
assertEquals(
Color.Black,
it[size.width / 2 - rectSize / 3, size.height / 2 - rectSize / 2 + 4]
)
assertEquals(
Color.Black,
it[size.width / 2 - rectSize / 3, size.height / 2 + rectSize / 2 - 4]
)
assertEquals(
Color.Black,
it[size.width / 2 + rectSize / 3, size.height / 2 - rectSize / 2 + 4]
)
assertEquals(
Color.Black,
it[size.width / 2 + rectSize / 3, size.height / 2 + rectSize / 2 - 4]
)
}
)
}
@Test
fun testUnboundedClip() {
var layer: GraphicsLayer?
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(
Color.Red,
size = Size(100000f, 100000f)
)
}
// Layer clipping is disabled by default
}
drawLayer(layer!!)
},
verify = {
assertEquals(Color.Red, it[0, 0])
assertEquals(Color.Red, it[it.width - 1, 0])
assertEquals(Color.Red, it[0, it.height - 1])
assertEquals(Color.Red, it[it.width - 1, it.height - 1])
assertEquals(Color.Red, it[it.width / 2, it.height / 2])
},
entireScene = true
)
}
@Test
fun testBoundedClip() {
var layer: GraphicsLayer?
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(
Color.Red,
size = Size(100000f, 100000f)
)
}
clip = true
}
drawLayer(layer!!)
},
verify = {
assertEquals(Color.Red, it[0, 0])
assertEquals(Color.Red, it[TEST_WIDTH - 1, 0])
assertEquals(Color.Red, it[0, TEST_HEIGHT - 1])
assertEquals(Color.Red, it[TEST_WIDTH - 1, TEST_HEIGHT - 1])
assertEquals(Color.Red, it[TEST_WIDTH / 2, 0])
assertEquals(Color.Red, it[TEST_WIDTH / 2, TEST_HEIGHT / 2])
assertEquals(Color.White, it[0, TEST_HEIGHT + 2])
assertEquals(Color.White, it[0, it.height - 1])
assertEquals(Color.White, it[TEST_WIDTH - 1, TEST_HEIGHT + 2])
assertEquals(Color.White, it[TEST_WIDTH + 1, TEST_HEIGHT])
assertEquals(Color.White, it[it.width - 1, TEST_HEIGHT - 2])
},
entireScene = true
)
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun testElevation() {
var layer: GraphicsLayer?
var left = 0
var top = 0
var right = 0
var bottom = 0
val targetColor = Color.White
graphicsLayerTest(
block = { graphicsContext ->
val halfSize = IntSize(
(this.size.width / 2f).toInt(),
(this.size.height / 2f).toInt()
)
layer = graphicsContext.createGraphicsLayer().apply {
record(halfSize) {
drawRect(targetColor)
}
shadowElevation = 10f
}
drawRect(targetColor)
left = (this.size.width / 4f).toInt()
top = (this.size.width / 4f).toInt()
right = left + halfSize.width
bottom = top + halfSize.height
translate(this.size.width / 4, this.size.height / 4) {
drawLayer(layer!!)
}
},
verify = { pixmap ->
var shadowPixelCount = 0
with(pixmap) {
for (x in left until right) {
for (y in top until bottom) {
if (this[x, y] != targetColor) {
shadowPixelCount++
}
}
}
}
assertTrue(shadowPixelCount > 0)
},
usePixelCopy = true
)
}
// Test requires validation of elevation shadows which require readback operations from
// the PixelCopy APIs
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun testElevationPath() {
var layer: GraphicsLayer?
var left = 0
var top = 0
var right = 0
var bottom = 0
val targetColor = Color.White
graphicsLayerTest(
block = { graphicsContext ->
val halfSize = IntSize(
(this.size.width / 2f).toInt(),
(this.size.height / 2f).toInt()
)
layer = graphicsContext.createGraphicsLayer().apply {
record(halfSize) {
drawRect(targetColor)
}
setPathOutline(
Path().apply {
addRect(
Rect(
0f,
0f,
halfSize.width.toFloat(),
halfSize.height.toFloat()
)
)
}
)
shadowElevation = 10f
}
drawRect(targetColor)
left = (this.size.width / 4f).toInt()
top = (this.size.width / 4f).toInt()
right = left + halfSize.width
bottom = top + halfSize.height
translate(this.size.width / 4, this.size.height / 4) {
drawLayer(layer!!)
}
},
verify = { pixmap ->
var shadowPixelCount = 0
with(pixmap) {
for (x in left until right) {
for (y in top until bottom) {
if (this[x, y] != targetColor) {
shadowPixelCount++
}
}
}
}
Assert.assertTrue(shadowPixelCount > 0)
},
usePixelCopy = true
)
}
// Test requires validation of elevation shadows which require readback operations from
// the PixelCopy APIs
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
fun testElevationRoundRect() {
var layer: GraphicsLayer?
var left = 0
var top = 0
var right = 0
var bottom = 0
val targetColor = Color.White
val radius = 50f
graphicsLayerTest(
block = { graphicsContext ->
val halfSize = IntSize(
(this.size.width / 2f).toInt(),
(this.size.height / 2f).toInt()
)
left = (this.size.width / 4f).toInt()
top = (this.size.width / 4f).toInt()
right = left + halfSize.width
bottom = top + halfSize.height
layer = graphicsContext.createGraphicsLayer().apply {
record(halfSize) {
drawRect(targetColor)
}
setRoundRectOutline(Offset.Zero, halfSize.toSize(), radius)
shadowElevation = 20f
}
drawRect(targetColor)
translate(left.toFloat(), top.toFloat()) {
drawLayer(layer!!)
}
},
verify = { pixmap ->
fun PixelMap.hasShadowPixels(
targetColor: Color,
l: Int,
t: Int,
r: Int,
b: Int
): Boolean {
var shadowCount = 0
for (i in l until r) {
for (j in t until b) {
if (this[i, j] != targetColor) {
shadowCount++
}
}
}
return shadowCount > 0
}
with(pixmap) {
assertTrue(
hasShadowPixels(
targetColor,
left,
top,
left + radius.toInt(),
top + radius.toInt()
)
)
assertTrue(
hasShadowPixels(
targetColor,
right - radius.toInt(),
top,
right,
top + radius.toInt()
)
)
assertTrue(
hasShadowPixels(
targetColor,
left,
bottom - radius.toInt(),
left + radius.toInt(),
bottom
)
)
assertTrue(
hasShadowPixels(
targetColor,
right - radius.toInt(),
bottom - radius.toInt(),
right,
bottom
)
)
}
},
usePixelCopy = true
)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
@Test
fun testRenderEffect() {
var layer: GraphicsLayer?
val blurRadius = 10f
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(Color.Red)
}
renderEffect = BlurEffect(blurRadius, blurRadius, TileMode.Decal)
}
drawRect(Color.Black)
drawLayer(layer!!)
},
verify = {
var nonPureRedCount = 0
for (x in 0 until it.width - blurRadius.toInt()) {
for (y in 0 until it.height - blurRadius.toInt()) {
val pixelColor = it[x, y]
if (pixelColor.blue > 0 || pixelColor.green > 0) {
Assert.fail(
"Only blue colors are expected. Pixel at [$x, $y] $pixelColor"
)
}
if (pixelColor.red > 0 && pixelColor.red < 1f) {
nonPureRedCount++
}
}
}
assertTrue(nonPureRedCount > 0)
},
entireScene = false
)
}
@Test
fun testCompositingStrategyAuto() {
var layer: GraphicsLayer?
val bgColor = Color.Black
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
inset(0f, 0f, size.width / 3, size.height / 3) {
drawRect(color = Color.Red)
}
inset(size.width / 3, size.height / 3, 0f, 0f) {
drawRect(color = Color.Blue)
}
}
alpha = 0.5f
compositingStrategy = CompositingStrategy.Auto
}
drawRect(bgColor)
drawLayer(layer!!)
},
verify = { pixelMap ->
with(pixelMap) {
val redWithAlpha = Color.Red.copy(alpha = 0.5f)
val blueWithAlpha = Color.Blue.copy(alpha = 0.5f)
val expectedTopLeft = redWithAlpha.compositeOver(bgColor)
val expectedBottomRight = blueWithAlpha.compositeOver(bgColor)
val expectedCenter = blueWithAlpha.compositeOver(bgColor)
assertPixelColor(expectedTopLeft, 0, 0)
assertPixelColor(Color.Black, width - 1, 0)
assertPixelColor(expectedBottomRight, width - 1, height - 1)
assertPixelColor(Color.Black, 0, height - 1)
assertPixelColor(expectedCenter, width / 2, height / 2)
}
}
)
}
@Test
fun testCompositingStrategyOffscreen() {
var layer: GraphicsLayer?
val bgColor = Color.LightGray
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
inset(0f, 0f, size.width / 3, size.height / 3) {
drawRect(color = Color.Red)
}
inset(size.width / 3, size.height / 3, 0f, 0f) {
drawRect(color = Color.Blue, blendMode = BlendMode.Xor)
}
}
compositingStrategy = CompositingStrategy.Offscreen
}
drawRect(bgColor)
drawLayer(layer!!)
},
verify = { pixelMap ->
with(pixelMap) {
assertPixelColor(Color.Red, 0, 0)
assertPixelColor(bgColor, width - 1, 0)
assertPixelColor(Color.Blue, width - 1, height - 1)
assertPixelColor(bgColor, 0, height - 1)
assertPixelColor(bgColor, width / 2, height / 2)
}
}
)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP_MR1)
@Test
fun testCompositingStrategyModulateAlpha() {
var layer: GraphicsLayer?
val bgColor = Color.Black
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
inset(0f, 0f, size.width / 3, size.height / 3) {
drawRect(color = Color.Red)
}
inset(size.width / 3, size.height / 3, 0f, 0f) {
drawRect(color = Color.Blue)
}
}
alpha = 0.5f
compositingStrategy = CompositingStrategy.ModulateAlpha
}
drawRect(bgColor)
drawLayer(layer!!)
},
verify = { pixelMap ->
with(pixelMap) {
val redWithAlpha = Color.Red.copy(alpha = 0.5f)
val blueWithAlpha = Color.Blue.copy(alpha = 0.5f)
val bg = Color.Black
val expectedTopLeft = redWithAlpha.compositeOver(bg)
val expectedBottomRight = blueWithAlpha.compositeOver(bg)
val expectedCenter = blueWithAlpha.compositeOver(redWithAlpha).compositeOver(bg)
assertPixelColor(expectedTopLeft, 0, 0)
assertPixelColor(Color.Black, width - 1, 0)
assertPixelColor(expectedBottomRight, width - 1, height - 1)
assertPixelColor(Color.Black, 0, height - 1)
assertPixelColor(expectedCenter, width / 2, height / 2)
}
}
)
}
@Test
fun testCameraDistanceWithRotationY() {
var layer: GraphicsLayer?
val bgColor = Color.Gray
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(Color.Red)
}
cameraDistance = 5.0f
rotationY = 25f
}
drawRect(bgColor)
drawLayer(layer!!)
},
verify = { pixelMap ->
with(pixelMap) {
assertPixelColor(Color.Red, 0, 0)
assertPixelColor(Color.Red, 0, height - 1)
assertPixelColor(Color.Red, width / 2 - 10, height / 2)
assertPixelColor(Color.Gray, width - 1 - 10, height / 2)
assertPixelColor(Color.Gray, width - 1, 0)
assertPixelColor(Color.Gray, width - 1, height - 1)
}
}
)
}
@Test
fun testTintColorFilter() {
var layer: GraphicsLayer?
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(Color.Red)
}
colorFilter = tint(Color.Blue)
}
drawLayer(layer!!)
},
verify = { pixelMap ->
with(pixelMap) {
assertPixelColor(Color.Blue, 0, 0)
assertPixelColor(Color.Blue, width - 1, 0)
assertPixelColor(Color.Blue, 0, height - 1)
assertPixelColor(Color.Blue, width - 1, height - 1)
assertPixelColor(Color.Blue, width / 2, height / 2)
}
}
)
}
@Test
fun testBlendMode() {
var layer: GraphicsLayer?
graphicsLayerTest(
block = { graphicsContext ->
val drawScopeSize = this.size
layer = graphicsContext.createGraphicsLayer().apply {
val topLeft = IntOffset(
(drawScopeSize.width / 4).toInt(),
(drawScopeSize.height / 4).toInt()
)
val layerSize = IntSize(
(drawScopeSize.width / 2).toInt(),
(drawScopeSize.height / 2).toInt()
)
record(layerSize) {
drawRect(Color.Red)
}
this.topLeft = topLeft
this.blendMode = BlendMode.Xor
}
drawRect(Color.Green)
drawLayer(layer!!)
// The layer should clear the original pixels in the destination rendered by the
// layer. Draw blue underneath the destination to fill the transparent pixels
// cleared by the layer
drawRect(Color.Blue, blendMode = BlendMode.DstOver)
},
verify = { pixelMap ->
with(pixelMap) {
assertPixelColor(Color.Green, 0, 0)
assertPixelColor(Color.Green, width - 1, 0)
assertPixelColor(Color.Green, 0, height - 1)
assertPixelColor(Color.Green, width - 1, height - 1)
val insetLeft = width / 4 + 2
val insetTop = height / 4 + 2
val insetRight = width - width / 4 - 2
val insetBottom = height - height / 4 - 2
assertPixelColor(Color.Blue, insetLeft, insetTop)
assertPixelColor(Color.Blue, insetRight, insetTop)
assertPixelColor(Color.Blue, insetLeft, insetBottom)
assertPixelColor(Color.Blue, insetRight, insetBottom)
assertPixelColor(Color.Blue, width / 2, height / 2)
}
}
)
}
@Test
fun testRectOutlineClip() {
var layer: GraphicsLayer?
var left = 0
var top = 0
var right = 0
var bottom = 0
val bgColor = Color.Black
val targetColor = Color.Red
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(targetColor)
}
setRectOutline(this.size.center.toOffset(), (this.size / 2).toSize())
clip = true
}
drawRect(bgColor)
left = this.size.center.x.toInt()
top = this.size.center.y.toInt()
right = this.size.width.toInt()
bottom = this.size.height.toInt()
drawLayer(layer!!)
},
verify = { pixmap ->
with(pixmap) {
for (x in 0 until width) {
for (y in 0 until height) {
val expected = if (x in left until right &&
y in top until bottom) {
targetColor
} else {
bgColor
}
Assert.assertEquals(this[x, y], expected)
}
}
}
}
)
}
@Test
fun testPathOutlineClip() {
var layer: GraphicsLayer?
var left = 0
var top = 0
var right = 0
var bottom = 0
val bgColor = Color.Black
val targetColor = Color.Red
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(targetColor)
}
setPathOutline(Path().apply {
addRect(
Rect(
size.center.x.toFloat(),
size.center.y.toFloat(),
size.center.x + size.width.toFloat(),
size.center.y + size.height.toFloat()
)
)
})
clip = true
}
drawRect(bgColor)
left = this.size.center.x.toInt()
top = this.size.center.y.toInt()
right = this.size.width.toInt()
bottom = this.size.height.toInt()
drawLayer(layer!!)
},
verify = { pixmap ->
with(pixmap) {
for (x in 0 until width) {
for (y in 0 until height) {
val expected = if (x in left until right &&
y in top until bottom) {
targetColor
} else {
bgColor
}
Assert.assertEquals(this[x, y], expected)
}
}
}
}
)
}
@Test
fun testRoundRectOutlineClip() {
var layer: GraphicsLayer?
var left = 0
var top = 0
var right = 0
var bottom = 0
val radius = 50
val bgColor = Color.Black
val targetColor = Color.Red
graphicsLayerTest(
block = { graphicsContext ->
layer = graphicsContext.createGraphicsLayer().apply {
record {
drawRect(targetColor)
}
setRoundRectOutline(
this.size.center.toOffset(),
(this.size / 2).toSize(),
radius.toFloat()
)
clip = true
}
drawRect(bgColor)
left = this.size.center.x.toInt()
top = this.size.center.y.toInt()
right = (left + this.size.width / 2).toInt()
bottom = (top + this.size.height / 2).toInt()
drawLayer(layer!!)
},
verify = { pixmap ->
with(pixmap) {
val offset = 5
val startX = left + radius + offset
val startY = top + radius + offset
val endX = right - radius - offset
val endY = bottom - radius - offset
for (x in 0 until width) {
for (y in 0 until height) {
if (
x in startX until endX &&
y in startY until endY) {
assertEquals(targetColor, this[x, y])
}
}
}
Assert.assertEquals(bgColor, this[offset, offset])
Assert.assertEquals(bgColor, this[width - offset, offset])
Assert.assertEquals(bgColor, this[offset, height - offset])
Assert.assertEquals(bgColor, this[width - offset, height - offset])
}
}
)
}
@Test
fun setOutlineExtensionAppliesValuesCorrectly() {
graphicsLayerTest(
block = { graphicsContext ->
val layer = graphicsContext.createGraphicsLayer()
val rectangle = Outline.Rectangle(Rect(1f, 2f, 3f, 4f))
layer.setOutline(rectangle)
assertEquals(rectangle, layer.outline)
val rounded = Outline.Rounded(RoundRect(10f, 20f, 30f, 40f, 5f, 5f))
layer.setOutline(rounded)
assertEquals(rounded, layer.outline)
val path = Path().also { it.addOval(Rect(1f, 2f, 3f, 4f)) }
val generic = Outline.Generic(path)
layer.setOutline(generic)
// We wrap the path in a different Outline object from what we pass in, so compare
// the paths instead of the outline instances
assertEquals(generic.path, (layer.outline as Outline.Generic).path)
}
)
}
@Test
fun testSwitchingFromClipToBoundsToClipToOutline() {
val targetColor = Color.Red
val inset = 50f
graphicsLayerTest(
block = { graphicsContext ->
val fullSize = size
val layerSize = Size(
fullSize.width.roundToInt() - inset * 2,
fullSize.height.roundToInt() - inset * 2
).toIntSize()
val layer = graphicsContext.createGraphicsLayer().apply {
record(size = layerSize) {
inset(-inset) {
drawRect(targetColor)
}
}
// as no outline is provided yet, this command will enable clipToBounds
clip = true
// then with providing an outline we should disable clipToBounds and start
// using clipToOutline instead
setRectOutline(Offset(-inset, -inset), fullSize)
}
drawRect(Color.Black)
inset(inset) {
drawLayer(layer)
}
},
verify = { pixmap ->
with(pixmap) {
for (x in 0 until width) {
for (y in 0 until height) {
assertEquals(this[x, y], targetColor)
}
}
}
}
)
}
@Test
fun testEndRecordingAlwaysCalled() {
graphicsLayerTest(
block = { graphicsContext ->
val layer = graphicsContext.createGraphicsLayer()
try {
layer.record {
// Intentionally cause an exception to be thrown during recording
throw Exception()
}
} catch (_: Throwable) {
// NO-OP
}
// Attempts to record after an exception is thrown should still succeed
layer.record { drawRect(Color.Red) }
drawLayer(layer)
},
verify = { it.verifyQuadrants(Color.Red, Color.Red, Color.Red, Color.Red) }
)
}
private fun PixelMap.verifyQuadrants(
topLeft: Color,
topRight: Color,
bottomLeft: Color,
bottomRight: Color
) {
val left = this.width / 4
val right = this.width / 4 + this.width / 2
val top = this.height / 4
val bottom = this.height / 4 + this.height / 2
assertPixelColor(topLeft, left, top) { "$left, $top is incorrect color" }
assertPixelColor(topRight, right, top) { "$right, $top is incorrect color" }
assertPixelColor(bottomLeft, left, bottom) { "$left, $bottom is incorrect color" }
assertPixelColor(bottomRight, right, bottom) { "$right, $bottom is incorrect color" }
}
private fun graphicsLayerTest(
block: DrawScope.(GraphicsContext) -> Unit,
verify: (suspend (PixelMap) -> Unit)? = null,
entireScene: Boolean = false,
usePixelCopy: Boolean = false
) {
var scenario: ActivityScenario<TestActivity>? = null
try {
var container: ViewGroup? = null
var contentView: View? = null
var rootGraphicsLayer: GraphicsLayer? = null
var density = Density(1f)
scenario = ActivityScenario.launch(TestActivity::class.java)
.moveToState(Lifecycle.State.CREATED)
.onActivity {
container = FrameLayout(it).apply {
setBackgroundColor(Color.White.toArgb())
clipToPadding = false
clipChildren = false
}
val graphicsContext = GraphicsContext(container!!)
rootGraphicsLayer = graphicsContext.createGraphicsLayer()
density = Density(it)
val content = FrameLayout(it).apply {
setLayoutParams(
FrameLayout.LayoutParams(
TEST_WIDTH,
TEST_HEIGHT
)
)
setBackgroundColor(Color.Black.toArgb())
foreground = GraphicsContextHostDrawable(graphicsContext, block)
}
container!!.addView(content)
contentView = content
it.setContentView(container)
}
val resumed = CountDownLatch(1)
var testActivity: TestActivity? = null
scenario.moveToState(Lifecycle.State.RESUMED)
.onActivity { activity ->
testActivity = activity
activity.runOnUiThread {
resumed.countDown()
}
}
assertTrue(resumed.await(3000, TimeUnit.MILLISECONDS))
if (verify != null) {
val target = if (entireScene) {
container!!
} else {
contentView!!
}
val pixelMap = if (usePixelCopy && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
target.captureToImage().toPixelMap()
} else {
val recordLatch = CountDownLatch(1)
testActivity!!.runOnUiThread {
rootGraphicsLayer!!.record(
density,
Ltr,
IntSize(target.width, target.height)
) {
drawIntoCanvas { canvas ->
target.draw(canvas.nativeCanvas)
}
}
recordLatch.countDown()
}
assertTrue(recordLatch.await(3000, TimeUnit.MILLISECONDS))
val bitmap = runBlocking {
rootGraphicsLayer!!.toImageBitmap()
}
bitmap.toPixelMap()
}
runBlocking {
verify(pixelMap)
}
}
} finally {
scenario?.moveToState(Lifecycle.State.DESTROYED)
}
}
private class GraphicsContextHostDrawable(
val graphicsContext: GraphicsContext,
val block: DrawScope.(GraphicsContext) -> Unit
) : Drawable() {
var rootGraphicsLayer: GraphicsLayer? = null
override fun draw(canvas: Canvas) {
val bounds = getBounds()
val width = bounds.width().toFloat()
val height = bounds.height().toFloat()
var root = rootGraphicsLayer
if (root == null) {
root = graphicsContext.createGraphicsLayer()
root.record(Density(1f, 1f), Ltr, IntSize(width.toInt(), height.toInt())) {
block(graphicsContext)
}
rootGraphicsLayer = root
}
root.draw(androidx.compose.ui.graphics.Canvas(canvas), null)
}
override fun setAlpha(alpha: Int) {
// NO-OP
}
override fun setColorFilter(colorFilter: ColorFilter?) {
// NO-OP
}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
}
}