| /* |
| * 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.vector |
| |
| import android.app.Application |
| import android.content.ComponentCallbacks2 |
| import android.content.pm.ActivityInfo |
| import android.content.res.Configuration |
| import android.content.res.Resources |
| import android.graphics.Bitmap |
| import android.os.Build |
| import androidx.activity.ComponentActivity |
| import androidx.annotation.RequiresApi |
| import androidx.compose.foundation.Image |
| import androidx.compose.foundation.background |
| import androidx.compose.foundation.clickable |
| import androidx.compose.foundation.layout.Box |
| import androidx.compose.foundation.layout.height |
| import androidx.compose.foundation.layout.size |
| import androidx.compose.foundation.layout.width |
| import androidx.compose.foundation.layout.wrapContentSize |
| import androidx.compose.runtime.Composable |
| import androidx.compose.runtime.CompositionLocalProvider |
| import androidx.compose.runtime.getValue |
| import androidx.compose.runtime.mutableIntStateOf |
| import androidx.compose.runtime.mutableStateOf |
| import androidx.compose.runtime.remember |
| import androidx.compose.runtime.setValue |
| import androidx.compose.testutils.assertPixels |
| import androidx.compose.ui.Alignment |
| import androidx.compose.ui.AtLeastSize |
| import androidx.compose.ui.Modifier |
| import androidx.compose.ui.background |
| import androidx.compose.ui.draw.drawBehind |
| import androidx.compose.ui.draw.paint |
| import androidx.compose.ui.geometry.Size |
| import androidx.compose.ui.graphics.BlendMode |
| import androidx.compose.ui.graphics.Brush |
| import androidx.compose.ui.graphics.Canvas |
| import androidx.compose.ui.graphics.Color |
| import androidx.compose.ui.graphics.ColorFilter |
| import androidx.compose.ui.graphics.CompositingStrategy |
| import androidx.compose.ui.graphics.ImageBitmap |
| import androidx.compose.ui.graphics.ImageBitmapConfig |
| import androidx.compose.ui.graphics.SolidColor |
| import androidx.compose.ui.graphics.asAndroidBitmap |
| import androidx.compose.ui.graphics.drawscope.CanvasDrawScope |
| import androidx.compose.ui.graphics.graphicsLayer |
| import androidx.compose.ui.graphics.painter.BitmapPainter |
| import androidx.compose.ui.graphics.toArgb |
| import androidx.compose.ui.graphics.toPixelMap |
| import androidx.compose.ui.layout.ContentScale |
| import androidx.compose.ui.platform.LocalContext |
| import androidx.compose.ui.platform.LocalDensity |
| import androidx.compose.ui.platform.LocalImageVectorCache |
| import androidx.compose.ui.platform.LocalLayoutDirection |
| import androidx.compose.ui.platform.testTag |
| import androidx.compose.ui.res.ImageVectorCache |
| import androidx.compose.ui.res.painterResource |
| import androidx.compose.ui.test.captureToImage |
| import androidx.compose.ui.test.junit4.createAndroidComposeRule |
| import androidx.compose.ui.test.onNodeWithTag |
| import androidx.compose.ui.test.onRoot |
| import androidx.compose.ui.test.performClick |
| import androidx.compose.ui.tests.R |
| import androidx.compose.ui.unit.Density |
| import androidx.compose.ui.unit.LayoutDirection |
| import androidx.compose.ui.unit.dp |
| import androidx.test.ext.junit.runners.AndroidJUnit4 |
| import androidx.test.filters.MediumTest |
| import androidx.test.filters.SdkSuppress |
| import java.util.concurrent.CountDownLatch |
| import java.util.concurrent.TimeUnit |
| import org.junit.Assert |
| import org.junit.Assert.assertArrayEquals |
| import org.junit.Assert.assertEquals |
| import org.junit.Assert.assertTrue |
| import org.junit.Assert.fail |
| import org.junit.Rule |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| |
| @MediumTest |
| @RunWith(AndroidJUnit4::class) |
| class VectorTest { |
| |
| @get:Rule |
| val rule = createAndroidComposeRule<ComponentActivity>() |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testVectorTint() { |
| rule.setContent { |
| VectorTint() |
| } |
| |
| takeScreenShot(200).apply { |
| assertEquals(getPixel(100, 100), Color.Cyan.toArgb()) |
| } |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testVectorIntrinsicTint() { |
| rule.setContent { |
| val background = Modifier.paint( |
| createTestVectorPainter(200, Color.Magenta), |
| alignment = Alignment.Center |
| ) |
| AtLeastSize(size = 200, modifier = background) { |
| } |
| } |
| takeScreenShot(200).apply { |
| assertEquals(getPixel(100, 100), Color.Magenta.toArgb()) |
| } |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testVectorIntrinsicTintFirstFrame() { |
| var vector: VectorPainter? = null |
| rule.setContent { |
| vector = createTestVectorPainter(200, Color.Magenta) |
| |
| val bitmap = remember { |
| val bitmap = ImageBitmap(200, 200) |
| val canvas = Canvas(bitmap) |
| val bitmapSize = Size(200f, 200f) |
| CanvasDrawScope().draw( |
| Density(1f), |
| LayoutDirection.Ltr, |
| canvas, |
| bitmapSize |
| ) { |
| with(vector!!) { |
| draw(bitmapSize) |
| } |
| } |
| bitmap |
| } |
| |
| val background = Modifier.paint(BitmapPainter(bitmap)) |
| |
| AtLeastSize(size = 200, modifier = background) { |
| } |
| } |
| takeScreenShot(200).apply { |
| assertEquals(getPixel(100, 100), Color.Magenta.toArgb()) |
| } |
| assertEquals(ImageBitmapConfig.Alpha8, vector!!.bitmapConfig) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testVectorAlignment() { |
| rule.setContent { |
| VectorTint(minimumSize = 450, alignment = Alignment.BottomEnd) |
| } |
| |
| takeScreenShot(450).apply { |
| assertEquals(getPixel(430, 430), Color.Cyan.toArgb()) |
| } |
| } |
| |
| @Test |
| fun testVectorSkipsRecompositionOnNoChange() { |
| val state = mutableIntStateOf(0) |
| var composeCount = 0 |
| var vectorComposeCount = 0 |
| |
| val composeVector: @Composable @VectorComposable (Float, Float) -> Unit = { |
| viewportWidth, viewportHeight -> |
| |
| vectorComposeCount++ |
| Path( |
| fill = SolidColor(Color.Blue), |
| pathData = PathData { |
| lineTo(viewportWidth, 0f) |
| lineTo(viewportWidth, viewportHeight) |
| lineTo(0f, viewportHeight) |
| close() |
| } |
| ) |
| } |
| |
| rule.setContent { |
| composeCount++ |
| // Arbitrary read to force composition here and verify the subcomposition below skips |
| state.value |
| val vectorPainter = rememberVectorPainter( |
| defaultWidth = 10.dp, |
| defaultHeight = 10.dp, |
| autoMirror = false, |
| content = composeVector |
| ) |
| Image( |
| vectorPainter, |
| null, |
| modifier = Modifier.size(20.dp) |
| ) |
| } |
| |
| state.value = 1 |
| rule.waitForIdle() |
| assertEquals(2, composeCount) // Arbitrary state read should compose twice |
| assertEquals(1, vectorComposeCount) // Vector is identical so should compose once |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testVectorInvalidation() { |
| val testCase = VectorInvalidationTestCase() |
| rule.setContent { |
| testCase.TestVector() |
| } |
| |
| rule.waitUntil { testCase.measured } |
| val size = testCase.vectorSize |
| takeScreenShot(size).apply { |
| assertEquals(Color.Blue.toArgb(), getPixel(5, size - 5)) |
| assertEquals(Color.White.toArgb(), getPixel(size - 5, 5)) |
| } |
| |
| testCase.measured = false |
| rule.runOnUiThread { |
| testCase.toggle() |
| } |
| |
| rule.waitUntil { testCase.measured } |
| |
| takeScreenShot(size).apply { |
| assertEquals(Color.White.toArgb(), getPixel(5, size - 5)) |
| assertEquals(Color.Red.toArgb(), getPixel(size - 5, 5)) |
| } |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testVectorRendersOnceOnFirstFrame() { |
| var drawCount = 0 |
| val testTag = "TestTag" |
| rule.setContent { |
| Box(modifier = Modifier |
| .wrapContentSize() |
| .drawBehind { |
| drawCount++ |
| } |
| .paint(painterResource(R.drawable.ic_triangle2)) |
| .testTag(testTag)) |
| } |
| |
| rule.onNodeWithTag(testTag).captureToImage().toPixelMap().apply { |
| assertEquals(1, drawCount) |
| } |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testVectorClipPath() { |
| rule.setContent { |
| VectorClip() |
| } |
| |
| takeScreenShot(200).apply { |
| assertEquals(getPixel(100, 50), Color.Cyan.toArgb()) |
| assertEquals(getPixel(100, 150), Color.Black.toArgb()) |
| } |
| } |
| |
| @Test |
| fun testVectorZeroSizeDoesNotCrash() { |
| // Make sure that if we are given the size of zero we should not crash and instead |
| // act as a no-op |
| rule.setContent { |
| Box(modifier = Modifier.size(0.dp).paint(createTestVectorPainter())) |
| } |
| } |
| |
| @Test |
| fun testVectorZeroWidthDoesNotCrash() { |
| rule.setContent { |
| Box( |
| modifier = Modifier.width(0.dp).height(100.dp).paint |
| (createTestVectorPainter()) |
| ) |
| } |
| } |
| |
| @Test |
| fun testVectorZeroHeightDoesNotCrash() { |
| rule.setContent { |
| Box( |
| modifier = Modifier.width(50.dp).height(0.dp).paint( |
| createTestVectorPainter() |
| ) |
| ) |
| } |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testVectorTrimPath() { |
| rule.setContent { |
| VectorTrim() |
| } |
| |
| takeScreenShot(200).apply { |
| assertEquals(Color.Yellow.toArgb(), getPixel(25, 100)) |
| assertEquals(Color.Blue.toArgb(), getPixel(100, 100)) |
| assertEquals(Color.Yellow.toArgb(), getPixel(175, 100)) |
| } |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testImageVectorChangeOnStateChange() { |
| val defaultWidth = 48.dp |
| val defaultHeight = 48.dp |
| val viewportWidth = 24f |
| val viewportHeight = 24f |
| |
| val icon1 = ImageVector.Builder( |
| defaultWidth = defaultWidth, |
| defaultHeight = defaultHeight, |
| viewportWidth = viewportWidth, |
| viewportHeight = viewportHeight |
| ) |
| .addPath( |
| fill = SolidColor(Color.Black), |
| pathData = PathData { |
| lineTo(viewportWidth, 0f) |
| lineTo(viewportWidth, viewportHeight) |
| lineTo(0f, 0f) |
| close() |
| } |
| ).build() |
| |
| val icon2 = ImageVector.Builder( |
| defaultWidth = defaultWidth, |
| defaultHeight = defaultHeight, |
| viewportWidth = viewportWidth, |
| viewportHeight = viewportHeight |
| ) |
| .addPath( |
| fill = SolidColor(Color.Black), |
| pathData = PathData { |
| lineTo(0f, viewportHeight) |
| lineTo(viewportWidth, viewportHeight) |
| lineTo(0f, 0f) |
| close() |
| } |
| ).build() |
| |
| val testTag = "iconClick" |
| rule.setContent { |
| val clickState = remember { mutableStateOf(false) } |
| Image( |
| imageVector = if (clickState.value) icon1 else icon2, |
| contentDescription = null, |
| modifier = Modifier |
| .testTag(testTag) |
| .size(icon1.defaultWidth, icon1.defaultHeight) |
| .background(Color.Red) |
| .clickable { clickState.value = !clickState.value }, |
| alignment = Alignment.TopStart, |
| contentScale = ContentScale.FillHeight |
| ) |
| } |
| |
| rule.onNodeWithTag(testTag).apply { |
| captureToImage().asAndroidBitmap().apply { |
| assertEquals(Color.Red.toArgb(), getPixel(width - 2, 0)) |
| assertEquals(Color.Red.toArgb(), getPixel(2, 0)) |
| assertEquals(Color.Red.toArgb(), getPixel(width - 1, height - 4)) |
| |
| assertEquals(Color.Black.toArgb(), getPixel(0, 2)) |
| assertEquals(Color.Black.toArgb(), getPixel(0, height - 2)) |
| assertEquals(Color.Black.toArgb(), getPixel(width - 4, height - 2)) |
| } |
| performClick() |
| } |
| |
| rule.waitForIdle() |
| |
| rule.onNodeWithTag(testTag).captureToImage().asAndroidBitmap().apply { |
| assertEquals(Color.Black.toArgb(), getPixel(width - 2, 0)) |
| assertEquals(Color.Black.toArgb(), getPixel(2, 0)) |
| assertEquals(Color.Black.toArgb(), getPixel(width - 1, height - 4)) |
| |
| assertEquals(Color.Red.toArgb(), getPixel(0, 2)) |
| assertEquals(Color.Red.toArgb(), getPixel(0, height - 2)) |
| assertEquals(Color.Red.toArgb(), getPixel(width - 4, height - 2)) |
| } |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testDrawWithoutColorFilterAfterPreviouslyConfigured() { |
| val defaultWidth = 24.dp |
| val defaultHeight = 24.dp |
| val testTag = "testTag" |
| var vectorPainter: VectorPainter? = null |
| |
| var tint: ColorFilter? by mutableStateOf(ColorFilter.tint(Color.Green)) |
| rule.setContent { |
| vectorPainter = rememberVectorPainter( |
| defaultWidth = defaultWidth, |
| defaultHeight = defaultHeight, |
| autoMirror = false |
| ) { viewportWidth, viewportHeight -> |
| Path( |
| fill = SolidColor(Color.Blue), |
| pathData = PathData { |
| lineTo(viewportWidth, 0f) |
| lineTo(viewportWidth, viewportHeight) |
| lineTo(0f, viewportHeight) |
| close() |
| } |
| ) |
| } |
| Image( |
| painter = vectorPainter!!, |
| contentDescription = null, |
| modifier = Modifier |
| .testTag(testTag) |
| .background(Color.Red), |
| contentScale = ContentScale.FillBounds, |
| colorFilter = tint |
| ) |
| } |
| |
| rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Green } |
| |
| tint = null |
| rule.waitForIdle() |
| rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue } |
| assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testDrawWithColorFilterAfterNotPreviouslyConfigured() { |
| val defaultWidth = 24.dp |
| val defaultHeight = 24.dp |
| val testTag = "testTag" |
| var vectorPainter: VectorPainter? = null |
| |
| var tint: ColorFilter? by mutableStateOf(null) |
| rule.setContent { |
| vectorPainter = rememberVectorPainter( |
| defaultWidth = defaultWidth, |
| defaultHeight = defaultHeight, |
| autoMirror = false |
| ) { viewportWidth, viewportHeight -> |
| Path( |
| fill = SolidColor(Color.Blue), |
| pathData = PathData { |
| lineTo(viewportWidth, 0f) |
| lineTo(viewportWidth, viewportHeight) |
| lineTo(0f, viewportHeight) |
| close() |
| } |
| ) |
| } |
| Image( |
| painter = vectorPainter!!, |
| contentDescription = null, |
| modifier = Modifier |
| .testTag(testTag) |
| .background(Color.Red), |
| contentScale = ContentScale.FillBounds, |
| colorFilter = tint |
| ) |
| } |
| |
| rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue } |
| |
| tint = ColorFilter.tint(Color.Green) |
| rule.waitForIdle() |
| rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Green } |
| assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicClearBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Clear) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicSrcBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Src) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicDstBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Dst) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicSrcOverBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.SrcOver, expectedConfig = ImageBitmapConfig.Alpha8) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicDstOverBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.DstOver) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicSrcInBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.SrcIn, expectedConfig = ImageBitmapConfig.Alpha8) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicDstInBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.DstIn) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicSrcOutBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.SrcOut) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicDstOutBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.DstOut) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicSrcAtopBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.SrcAtop) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicDstAtopBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.DstAtop) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicXorBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Xor) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicPlusBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Plus) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicModulateBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Modulate) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicScreenBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Screen) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicOverlayBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Overlay) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicDarkenBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Darken) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicLightenBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Lighten) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicColorDodgeBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.ColorDodge) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicColorBurnBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.ColorBurn) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicHardlightBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Hardlight) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicSoftLightBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Softlight) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicDifferenceBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Difference) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicExclusionBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Exclusion) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicMultiplyBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Multiply) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicHueBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Hue) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicSaturationBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Saturation) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicColorBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Color) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithIntrinsicLuminosityBlendMode() { |
| verifyAlphaMaskWithBlendModes(BlendMode.Luminosity) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawClearBlendMode() { |
| verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Clear)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawSrcBlendMode() { |
| verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Src)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawDstBlendMode() { |
| verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Dst)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawSrcOverBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.SrcOver), |
| expectedConfig = ImageBitmapConfig.Alpha8 |
| ) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawDstOverBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.DstOver)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawSrcInBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.SrcIn), |
| expectedConfig = ImageBitmapConfig.Alpha8 |
| ) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawDstInBlendMode() { |
| verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.DstIn)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawSrcOutBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.SrcOut)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawDstOutBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.DstOut)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawSrcAtopBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.SrcAtop)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawDstAtopBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.DstAtop)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawXorBlendMode() { |
| verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Xor)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawPlusBlendMode() { |
| verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Plus)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawModulateBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Modulate)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawScreenBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Screen)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawOverlayBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Overlay)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawDarkenBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Darken)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawLightenBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Lighten)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawColorDodgeBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.ColorDodge)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawColorBurnBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.ColorBurn)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawHardlightBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Hardlight)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawSoftLightBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Softlight)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawDifferenceBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Difference)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawExclusionBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Exclusion)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawMultiplyBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Multiply)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawHueBlendMode() { |
| verifyAlphaMaskWithBlendModes(colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Hue)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawSaturationBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Saturation)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawColorBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Color)) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testAlphaMaskWithDrawLuminosityBlendMode() { |
| verifyAlphaMaskWithBlendModes( |
| colorFilter = ColorFilter.tint(Color.Yellow, BlendMode.Luminosity)) |
| } |
| |
| @RequiresApi(Build.VERSION_CODES.O) |
| private fun verifyAlphaMaskWithBlendModes( |
| intrinsicBlendMode: BlendMode = BlendMode.SrcIn, |
| colorFilter: ColorFilter? = null, |
| expectedConfig: ImageBitmapConfig? = null, |
| ) { |
| val defaultWidth = 24.dp |
| val defaultHeight = 24.dp |
| val testTag = "testTag" |
| var vectorPainter: VectorPainter? = null |
| |
| // Create a gradient of the same color as a solid in order to verify behavior |
| // of intrinsic color filter usage both with and without the optimization to tint |
| // use a tinted alpha channel bitmap instead of a ARGB8888 |
| val solidBlueGradient = Brush.horizontalGradient(listOf(Color.Blue, Color.Blue)) |
| val solidBlueColor = SolidColor(Color.Blue) |
| var targetBrush: Brush by mutableStateOf(solidBlueColor) |
| rule.setContent { |
| vectorPainter = rememberVectorPainter( |
| defaultWidth = defaultWidth, |
| defaultHeight = defaultHeight, |
| tintColor = Color.Cyan, |
| tintBlendMode = intrinsicBlendMode, |
| autoMirror = false |
| ) { viewportWidth, viewportHeight -> |
| Path( |
| fill = targetBrush, |
| pathData = PathData { |
| lineTo(viewportWidth, 0f) |
| lineTo(viewportWidth, viewportHeight) |
| lineTo(0f, viewportHeight) |
| close() |
| } |
| ) |
| } |
| Image( |
| painter = vectorPainter!!, |
| contentDescription = null, |
| modifier = Modifier |
| .testTag(testTag) |
| .background( |
| Brush.horizontalGradient( |
| listOf(Color.Transparent, Color.Yellow, Color.Transparent) |
| ) |
| ) |
| .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }, |
| contentScale = ContentScale.FillBounds, |
| colorFilter = colorFilter |
| ) |
| } |
| |
| rule.waitForIdle() |
| |
| val solidBrushImage = rule.onNodeWithTag(testTag).captureToImage() |
| if (expectedConfig != null) { |
| assertEquals(expectedConfig, vectorPainter!!.bitmapConfig) |
| } |
| |
| targetBrush = solidBlueGradient |
| rule.waitForIdle() |
| |
| val gradientBrushImage = rule.onNodeWithTag(testTag).captureToImage() |
| |
| assertArrayEquals( |
| "Optimized vector does not match expected for $intrinsicBlendMode", |
| gradientBrushImage.toPixelMap().buffer, |
| solidBrushImage.toPixelMap().buffer |
| ) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testPathColorChangeUpdatesBitmapConfig() { |
| val defaultWidth = 24.dp |
| val defaultHeight = 24.dp |
| val testTag = "testTag" |
| var vectorPainter: VectorPainter? = null |
| var brush: Brush by mutableStateOf(SolidColor(Color.Blue)) |
| rule.setContent { |
| vectorPainter = rememberVectorPainter( |
| defaultWidth = defaultWidth, |
| defaultHeight = defaultHeight, |
| autoMirror = false |
| ) { viewportWidth, viewportHeight -> |
| Path( |
| fill = brush, |
| pathData = PathData { |
| lineTo(viewportWidth, 0f) |
| lineTo(viewportWidth, viewportHeight) |
| lineTo(0f, viewportHeight) |
| close() |
| } |
| ) |
| } |
| Image( |
| painter = vectorPainter!!, |
| contentDescription = null, |
| modifier = Modifier |
| .testTag(testTag) |
| .size(defaultWidth * 8, defaultHeight * 2) |
| .background(Color.Red), |
| contentScale = ContentScale.FillBounds |
| ) |
| } |
| |
| rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue } |
| assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig) |
| |
| brush = Brush.horizontalGradient(listOf(Color.Blue, Color.Blue)) |
| rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue } |
| assertEquals(ImageBitmapConfig.Argb8888, vectorPainter!!.bitmapConfig) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testGroupPathColorChangeUpdatesBitmapConfig() { |
| val defaultWidth = 24.dp |
| val defaultHeight = 24.dp |
| val testTag = "testTag" |
| var vectorPainter: VectorPainter? = null |
| var brush: Brush by mutableStateOf(SolidColor(Color.Blue)) |
| rule.setContent { |
| vectorPainter = rememberVectorPainter( |
| defaultWidth = defaultWidth, |
| defaultHeight = defaultHeight, |
| autoMirror = false |
| ) { viewportWidth, viewportHeight -> |
| Group { |
| Path( |
| fill = brush, |
| pathData = PathData { |
| lineTo(viewportWidth, 0f) |
| lineTo(viewportWidth, viewportHeight) |
| lineTo(0f, viewportHeight) |
| close() |
| } |
| ) |
| } |
| } |
| Image( |
| painter = vectorPainter!!, |
| contentDescription = null, |
| modifier = Modifier |
| .testTag(testTag) |
| .size(defaultWidth * 8, defaultHeight * 2) |
| .background(Color.Red), |
| contentScale = ContentScale.FillBounds |
| ) |
| } |
| |
| rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue } |
| assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig) |
| |
| brush = Brush.horizontalGradient(listOf(Color.Blue, Color.Blue)) |
| rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue } |
| assertEquals(ImageBitmapConfig.Argb8888, vectorPainter!!.bitmapConfig) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testVectorScaleNonUniformly() { |
| val defaultWidth = 24.dp |
| val defaultHeight = 24.dp |
| val testTag = "testTag" |
| var vectorPainter: VectorPainter? = null |
| rule.setContent { |
| vectorPainter = rememberVectorPainter( |
| defaultWidth = defaultWidth, |
| defaultHeight = defaultHeight, |
| autoMirror = false |
| ) { viewportWidth, viewportHeight -> |
| Path( |
| fill = SolidColor(Color.Blue), |
| pathData = PathData { |
| lineTo(viewportWidth, 0f) |
| lineTo(viewportWidth, viewportHeight) |
| lineTo(0f, viewportHeight) |
| close() |
| } |
| ) |
| } |
| Image( |
| painter = vectorPainter!!, |
| contentDescription = null, |
| modifier = Modifier |
| .testTag(testTag) |
| .size(defaultWidth * 8, defaultHeight * 2) |
| .background(Color.Red), |
| contentScale = ContentScale.FillBounds |
| ) |
| } |
| |
| rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Blue } |
| assertEquals(ImageBitmapConfig.Alpha8, vectorPainter!!.bitmapConfig) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testVectorChangeSize() { |
| val size = mutableStateOf(200) |
| val color = mutableStateOf(Color.Magenta) |
| |
| rule.setContent { |
| val background = Modifier.background(Color.Red).paint( |
| createTestVectorPainter(size.value, color.value), |
| alignment = Alignment.TopStart |
| ) |
| AtLeastSize(size = 400, modifier = background) { |
| } |
| } |
| |
| takeScreenShot(400).apply { |
| assertEquals(getPixel(100, 100), Color.Magenta.toArgb()) |
| assertEquals(getPixel(300, 300), Color.Red.toArgb()) |
| } |
| |
| size.value = 400 |
| color.value = Color.Cyan |
| |
| takeScreenShot(400).apply { |
| assertEquals(getPixel(100, 100), Color.Cyan.toArgb()) |
| assertEquals(getPixel(300, 300), Color.Cyan.toArgb()) |
| } |
| |
| size.value = 50 |
| color.value = Color.Yellow |
| |
| takeScreenShot(400).apply { |
| assertEquals(getPixel(10, 10), Color.Yellow.toArgb()) |
| assertEquals(getPixel(100, 100), Color.Red.toArgb()) |
| assertEquals(getPixel(300, 300), Color.Red.toArgb()) |
| } |
| } |
| |
| @Test |
| fun testImageVectorCacheHit() { |
| var vectorInCache = false |
| rule.setContent { |
| val theme = LocalContext.current.theme |
| val density = LocalDensity.current |
| val imageVectorCache = LocalImageVectorCache.current |
| imageVectorCache.clear() |
| Image( |
| painterResource(R.drawable.ic_triangle), |
| contentDescription = null |
| ) |
| |
| val key = ImageVectorCache.Key(theme, R.drawable.ic_triangle, density) |
| vectorInCache = imageVectorCache[key] != null |
| } |
| |
| assertTrue(vectorInCache) |
| } |
| |
| @Test |
| fun testVectorPainterCacheHit() { |
| var vectorInCache = false |
| rule.setContent { |
| // obtaining the same painter resource should return the same instance root |
| // GroupComponent |
| val painter1 = painterResource(R.drawable.ic_triangle) as VectorPainter |
| val painter2 = painterResource(R.drawable.ic_triangle) as VectorPainter |
| vectorInCache = painter1.vector.root === painter2.vector.root |
| } |
| |
| assertTrue(vectorInCache) |
| } |
| |
| @Test |
| fun testImageVectorCacheCleared() { |
| var vectorInCache = false |
| var application: Application? = null |
| var theme: Resources.Theme? = null |
| var vectorCache: ImageVectorCache? = null |
| var density: Density? = null |
| rule.setContent { |
| application = LocalContext.current.applicationContext as Application |
| density = LocalDensity.current |
| theme = LocalContext.current.theme |
| val imageVectorCache = LocalImageVectorCache.current |
| imageVectorCache.clear() |
| Image( |
| painterResource(R.drawable.ic_triangle), |
| contentDescription = null |
| ) |
| |
| val key = ImageVectorCache.Key(theme!!, R.drawable.ic_triangle, density!!) |
| vectorInCache = imageVectorCache[key] != null |
| |
| vectorCache = imageVectorCache |
| } |
| |
| application?.onTrimMemory(0) |
| |
| val cacheCleared = vectorCache?.let { |
| it[ImageVectorCache.Key(theme!!, R.drawable.ic_triangle, density!!)] == null |
| } ?: false |
| |
| assertTrue("Vector was not inserted in cache after initial creation", vectorInCache) |
| assertTrue("Cache was not cleared after trim memory call", cacheCleared) |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testImageVectorConfigChange() { |
| val tag = "testTag" |
| rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE |
| |
| val latch = CountDownLatch(1) |
| |
| rule.activity.application.registerComponentCallbacks(object : ComponentCallbacks2 { |
| override fun onConfigurationChanged(p0: Configuration) { |
| latch.countDown() |
| } |
| |
| override fun onLowMemory() { |
| // NO-OP |
| } |
| |
| override fun onTrimMemory(p0: Int) { |
| // NO-OP |
| } |
| }) |
| |
| try { |
| latch.await(1500, TimeUnit.MILLISECONDS) |
| rule.setContent { |
| Image( |
| painterResource(R.drawable.ic_triangle_config), |
| contentDescription = null, |
| modifier = Modifier.testTag(tag) |
| ) |
| } |
| rule.onNodeWithTag(tag).captureToImage().apply { |
| assertEquals(Color.Blue, toPixelMap()[width - 5, 5]) |
| } |
| } catch (e: InterruptedException) { |
| fail("Unable to verify vector asset in landscape orientation") |
| } finally { |
| rule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT |
| } |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testVectorMirror() { |
| val tag = "mirroredVector" |
| rule.setContent { |
| CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { |
| Image( |
| painter = VectorMirror(20), |
| contentDescription = null, |
| modifier = Modifier.testTag(tag) |
| ) |
| } |
| } |
| rule.onNodeWithTag(tag).captureToImage().toPixelMap().apply { |
| assertEquals(Color.Blue, this[2, 2]) |
| assertEquals(Color.Blue, this[2, height - 3]) |
| assertEquals(Color.Blue, this[width / 2 - 3, 2]) |
| assertEquals(Color.Blue, this[width / 2 - 3, height - 3]) |
| |
| assertEquals(Color.Red, this[width - 3, 2]) |
| assertEquals(Color.Red, this[width - 3, height - 3]) |
| assertEquals(Color.Red, this[width / 2 + 3, 2]) |
| assertEquals(Color.Red, this[width / 2 + 3, height - 3]) |
| } |
| } |
| |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) |
| @Test |
| fun testVectorStrokeWidth() { |
| val strokeWidth = mutableStateOf(100) |
| rule.setContent { |
| VectorStroke(strokeWidth = strokeWidth.value) |
| } |
| takeScreenShot(200).apply { |
| assertEquals(Color.Yellow.toArgb(), getPixel(100, 25)) |
| assertEquals(Color.Blue.toArgb(), getPixel(100, 75)) |
| } |
| rule.runOnUiThread { strokeWidth.value = 200 } |
| rule.waitForIdle() |
| takeScreenShot(200).apply { |
| assertEquals(Color.Yellow.toArgb(), getPixel(100, 25)) |
| assertEquals(Color.Yellow.toArgb(), getPixel(100, 75)) |
| } |
| } |
| |
| @Composable |
| private fun VectorTint( |
| size: Int = 200, |
| minimumSize: Int = size, |
| alignment: Alignment = Alignment.Center |
| ) { |
| val background = Modifier.paint( |
| createTestVectorPainter(size), |
| colorFilter = ColorFilter.tint(Color.Cyan), |
| alignment = alignment |
| ) |
| AtLeastSize(size = minimumSize, modifier = background) { |
| } |
| } |
| |
| @Composable |
| private fun createTestVectorPainter( |
| size: Int = 200, |
| tintColor: Color = Color.Unspecified |
| ): VectorPainter { |
| val sizePx = size.toFloat() |
| val sizeDp = (size / LocalDensity.current.density).dp |
| return rememberVectorPainter( |
| defaultWidth = sizeDp, |
| defaultHeight = sizeDp, |
| autoMirror = false, |
| content = { _, _ -> |
| Path( |
| pathData = PathData { |
| lineTo(sizePx, 0.0f) |
| lineTo(sizePx, sizePx) |
| lineTo(0.0f, sizePx) |
| close() |
| }, |
| fill = SolidColor(Color.Black) |
| ) |
| }, |
| tintColor = tintColor |
| ) |
| } |
| |
| @Composable |
| private fun VectorClip( |
| size: Int = 200, |
| minimumSize: Int = size, |
| alignment: Alignment = Alignment.Center |
| ) { |
| val sizePx = size.toFloat() |
| val sizeDp = (size / LocalDensity.current.density).dp |
| val background = Modifier.paint( |
| rememberVectorPainter( |
| defaultWidth = sizeDp, |
| defaultHeight = sizeDp, |
| autoMirror = false |
| ) { _, _ -> |
| Path( |
| // Cyan background. |
| pathData = PathData { |
| lineTo(sizePx, 0.0f) |
| lineTo(sizePx, sizePx) |
| lineTo(0.0f, sizePx) |
| close() |
| }, |
| fill = SolidColor(Color.Cyan) |
| ) |
| Group( |
| // Only show the top half... |
| clipPathData = PathData { |
| lineTo(sizePx, 0.0f) |
| lineTo(sizePx, sizePx / 2) |
| lineTo(0.0f, sizePx / 2) |
| close() |
| }, |
| // And rotate it, resulting in the bottom half being black. |
| pivotX = sizePx / 2, |
| pivotY = sizePx / 2, |
| rotation = 180f |
| ) { |
| Path( |
| pathData = PathData { |
| lineTo(sizePx, 0.0f) |
| lineTo(sizePx, sizePx) |
| lineTo(0.0f, sizePx) |
| close() |
| }, |
| fill = SolidColor(Color.Black) |
| ) |
| } |
| }, |
| alignment = alignment |
| ) |
| AtLeastSize(size = minimumSize, modifier = background) { |
| } |
| } |
| |
| @Composable |
| private fun VectorTrim( |
| size: Int = 200, |
| minimumSize: Int = size, |
| alignment: Alignment = Alignment.Center |
| ) { |
| val sizePx = size.toFloat() |
| val sizeDp = (size / LocalDensity.current.density).dp |
| val background = Modifier.paint( |
| rememberVectorPainter( |
| defaultWidth = sizeDp, |
| defaultHeight = sizeDp, |
| autoMirror = false |
| ) { _, _ -> |
| Path( |
| pathData = PathData { |
| lineTo(sizePx, 0.0f) |
| lineTo(sizePx, sizePx) |
| lineTo(0.0f, sizePx) |
| close() |
| }, |
| fill = SolidColor(Color.Blue) |
| ) |
| // A thick stroke |
| Path( |
| pathData = PathData { |
| moveTo(0.0f, sizePx / 2) |
| lineTo(sizePx, sizePx / 2) |
| }, |
| stroke = SolidColor(Color.Yellow), |
| strokeLineWidth = sizePx / 2, |
| trimPathStart = 0.25f, |
| trimPathEnd = 0.75f, |
| trimPathOffset = 0.5f |
| ) |
| }, |
| alignment = alignment |
| ) |
| AtLeastSize(size = minimumSize, modifier = background) { |
| } |
| } |
| |
| @Composable |
| private fun VectorStroke( |
| size: Int = 200, |
| strokeWidth: Int = 100, |
| minimumSize: Int = size, |
| alignment: Alignment = Alignment.Center |
| ) { |
| val sizePx = size.toFloat() |
| val sizeDp = (size / LocalDensity.current.density).dp |
| val strokeWidthPx = strokeWidth.toFloat() |
| val background = Modifier.paint( |
| rememberVectorPainter( |
| defaultWidth = sizeDp, |
| defaultHeight = sizeDp, |
| autoMirror = false |
| ) { _, _ -> |
| Path( |
| pathData = PathData { |
| lineTo(sizePx, 0.0f) |
| lineTo(sizePx, sizePx) |
| lineTo(0.0f, sizePx) |
| close() |
| }, |
| fill = SolidColor(Color.Blue) |
| ) |
| // A thick stroke |
| Path( |
| pathData = PathData { |
| moveTo(0.0f, 0.0f) |
| lineTo(sizePx, 0.0f) |
| }, |
| stroke = SolidColor(Color.Yellow), |
| strokeLineWidth = strokeWidthPx, |
| ) |
| }, |
| alignment = alignment |
| ) |
| AtLeastSize(size = minimumSize, modifier = background) { |
| } |
| } |
| |
| @Composable |
| private fun VectorMirror(size: Int): VectorPainter { |
| val sizePx = size.toFloat() |
| val sizeDp = (size / LocalDensity.current.density).dp |
| return rememberVectorPainter( |
| defaultWidth = sizeDp, |
| defaultHeight = sizeDp, |
| autoMirror = true |
| ) { _, _ -> |
| Path( |
| pathData = PathData { |
| lineTo(sizePx / 2, 0f) |
| lineTo(sizePx / 2, sizePx) |
| lineTo(0f, sizePx) |
| close() |
| }, |
| fill = SolidColor(Color.Red) |
| ) |
| |
| Path( |
| pathData = PathData { |
| moveTo(sizePx / 2, 0f) |
| lineTo(sizePx, 0f) |
| lineTo(sizePx, sizePx) |
| lineTo(sizePx / 2, sizePx) |
| close() |
| }, |
| fill = SolidColor(Color.Blue) |
| ) |
| } |
| } |
| |
| // captureToImage() requires API level 26 |
| @RequiresApi(Build.VERSION_CODES.O) |
| private fun takeScreenShot(width: Int, height: Int = width): Bitmap { |
| val bitmap = rule.onRoot().captureToImage().asAndroidBitmap() |
| Assert.assertEquals(width, bitmap.width) |
| Assert.assertEquals(height, bitmap.height) |
| return bitmap |
| } |
| } |