Files
lawnchair/tests/multivalentTests/src/com/android/launcher3/graphics/ShapeDelegateTest.kt
Sunny Goyal 29f9c3d231 Merging IconShape with ThemeManager
The order of initialization for IconShape is closely tied to ThemeManager
which makes it difficult for change listeners to work. Merging these keeps
all the theming information at one place

Bug: 381897614
Test: Updated tests
Flag: EXEMPT refactor
Change-Id: Ie29efa20c3308a24be3e284c1c93ed7444b68d58
2025-02-13 22:38:01 -08:00

219 lines
8.5 KiB
Kotlin

/*
* Copyright (C) 2025 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 com.android.launcher3.graphics
import android.graphics.Matrix
import android.graphics.Matrix.ScaleToFit.FILL
import android.graphics.Path
import android.graphics.Path.Direction
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Region
import android.platform.uiautomatorhelpers.DeviceHelpers.context
import android.view.View
import androidx.core.graphics.PathParser
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.graphics.ShapeDelegate.Circle
import com.android.launcher3.graphics.ShapeDelegate.Companion.AREA_CALC_SIZE
import com.android.launcher3.graphics.ShapeDelegate.Companion.AREA_DIFF_THRESHOLD
import com.android.launcher3.graphics.ShapeDelegate.Companion.areaDiffCalculator
import com.android.launcher3.graphics.ShapeDelegate.Companion.pickBestShape
import com.android.launcher3.graphics.ShapeDelegate.GenericPathShape
import com.android.launcher3.graphics.ShapeDelegate.RoundedSquare
import com.android.launcher3.icons.GraphicsUtils
import com.android.launcher3.views.ClipPathView
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class ShapeDelegateTest {
@Test
fun `areaDiffCalculator increases with outwards shape`() {
val diffCalculator =
areaDiffCalculator(
Path().apply {
addCircle(
AREA_CALC_SIZE / 2f,
AREA_CALC_SIZE / 2f,
AREA_CALC_SIZE / 2f,
Direction.CW,
)
}
)
assertThat(diffCalculator(Circle())).isLessThan(AREA_DIFF_THRESHOLD)
assertThat(diffCalculator(Circle())).isLessThan(diffCalculator(RoundedSquare(.9f)))
assertThat(diffCalculator(RoundedSquare(.9f)))
.isLessThan(diffCalculator(RoundedSquare(.8f)))
assertThat(diffCalculator(RoundedSquare(.8f)))
.isLessThan(diffCalculator(RoundedSquare(.7f)))
assertThat(diffCalculator(RoundedSquare(.7f)))
.isLessThan(diffCalculator(RoundedSquare(.6f)))
assertThat(diffCalculator(RoundedSquare(.6f)))
.isLessThan(diffCalculator(RoundedSquare(.5f)))
}
@Test
fun `areaDiffCalculator increases with inwards shape`() {
val diffCalculator = areaDiffCalculator(roundedRectPath(0.5f))
assertThat(diffCalculator(RoundedSquare(.5f))).isLessThan(AREA_DIFF_THRESHOLD)
assertThat(diffCalculator(RoundedSquare(.5f)))
.isLessThan(diffCalculator(RoundedSquare(.6f)))
assertThat(diffCalculator(RoundedSquare(.5f)))
.isLessThan(diffCalculator(RoundedSquare(.4f)))
}
@Test
fun `pickBestShape picks circle`() {
val r = AREA_CALC_SIZE / 2
val pathStr = "M 50 0 a 50 50 0 0 1 0 100 a 50 50 0 0 1 0 -100"
val path = Path().apply { addCircle(r.toFloat(), r.toFloat(), r.toFloat(), Direction.CW) }
assertThat(pickBestShape(path, pathStr)).isInstanceOf(Circle::class.java)
}
@Test
fun `pickBestShape picks rounded rect`() {
val factor = 0.5f
var shape = pickBestShape(roundedRectPath(factor), roundedRectString(factor))
assertThat(shape).isInstanceOf(RoundedSquare::class.java)
assertThat((shape as RoundedSquare).radiusRatio).isEqualTo(factor)
val factor2 = 0.2f
shape = pickBestShape(roundedRectPath(factor2), roundedRectString(factor2))
assertThat(shape).isInstanceOf(RoundedSquare::class.java)
assertThat((shape as RoundedSquare).radiusRatio).isEqualTo(factor2)
}
@Test
fun `pickBestShape picks generic shape`() {
val path = cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE))
val pathStr = FOUR_SIDED_COOKIE
val shape = pickBestShape(path, pathStr)
assertThat(shape).isInstanceOf(GenericPathShape::class.java)
val diffCalculator = areaDiffCalculator(path)
assertThat(diffCalculator(shape)).isLessThan(AREA_DIFF_THRESHOLD)
}
@Test
fun `generic shape creates smooth animation`() {
val shape = GenericPathShape(FOUR_SIDED_COOKIE)
val target = TestClipView()
val anim =
shape.createRevealAnimator(
target,
Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE),
Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE),
AREA_CALC_SIZE * .25f,
false,
)
// Verify that the start rect is similar to initial path
anim.setCurrentFraction(0f)
assertThat(
getAreaDiff(
target.currentClip!!,
cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)),
)
)
.isLessThan(AREA_CALC_SIZE)
// Verify that end rect is similar to end path
anim.setCurrentFraction(1f)
assertThat(getAreaDiff(target.currentClip!!, roundedRectPath(0.5f)))
.isLessThan(AREA_CALC_SIZE)
// Ensure that when running animation, area increases smoothly. We run the animation over
// [steps] and verify increase of max 5 times the linear diff increase
val steps = 1000
val incrementalDiff =
getAreaDiff(
cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)),
roundedRectPath(0.5f),
) * 5 / steps
var lastPath = cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE))
for (progress in 1..steps) {
anim.setCurrentFraction(progress / 1000f)
val currentPath = Path(target.currentClip!!)
assertThat(getAreaDiff(lastPath, currentPath)).isLessThan(incrementalDiff)
lastPath = currentPath
}
assertThat(getAreaDiff(lastPath, roundedRectPath(0.5f))).isLessThan(AREA_CALC_SIZE)
}
private fun roundedRectPath(factor: Float) =
Path().apply {
val r = factor * AREA_CALC_SIZE / 2
addRoundRect(
0f,
0f,
AREA_CALC_SIZE.toFloat(),
AREA_CALC_SIZE.toFloat(),
r,
r,
Direction.CW,
)
}
private fun roundedRectString(factor: Float): String {
val s = 100f
val r = (factor * s / 2)
val t = s - r
return "M $r 0 " +
"L $t 0 " +
"A $r $r 0 0 1 $s $r " +
"L $s $t " +
"A $r $r 0 0 1 $t $s " +
"L $r $s " +
"A $r $r 0 0 1 0 $t " +
"L 0 $r " +
"A $r $r 0 0 1 $r 0 Z"
}
private fun getAreaDiff(p1: Path, p2: Path): Int {
val fullRegion = Region(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)
val iconRegion = Region().apply { setPath(p1, fullRegion) }
val shapeRegion = Region().apply { setPath(p2, fullRegion) }
shapeRegion.op(iconRegion, Region.Op.XOR)
return GraphicsUtils.getArea(shapeRegion)
}
class TestClipView : View(context), ClipPathView {
var currentClip: Path? = null
override fun setClipPath(clipPath: Path?) {
currentClip = clipPath
}
}
companion object {
const val FOUR_SIDED_COOKIE =
"M63.605 3C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C -6.176 15.268 15.267 -6.176 36.395 3L39.888 4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3Z"
private fun cookiePath(bounds: Rect) =
PathParser.createPathFromPathData(FOUR_SIDED_COOKIE).apply {
transform(
Matrix().apply { setRectToRect(RectF(0f, 0f, 100f, 100f), RectF(bounds), FILL) }
)
}
}
}