1138f5fe59
Removing various custom shapes and replacing it with a generic shape implementation. This works with any valid svg path. Bug: 366237794 Flag: EXEMPT refactor Test: atest IconShapeTest Change-Id: I4665bc657b8bffc4d4efa30cebe00341b3140c55
219 lines
8.5 KiB
Kotlin
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.IconShape.Circle
|
|
import com.android.launcher3.graphics.IconShape.Companion.AREA_CALC_SIZE
|
|
import com.android.launcher3.graphics.IconShape.Companion.AREA_DIFF_THRESHOLD
|
|
import com.android.launcher3.graphics.IconShape.Companion.areaDiffCalculator
|
|
import com.android.launcher3.graphics.IconShape.Companion.pickBestShape
|
|
import com.android.launcher3.graphics.IconShape.GenericPathShape
|
|
import com.android.launcher3.graphics.IconShape.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 IconShapeTest {
|
|
|
|
@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) }
|
|
)
|
|
}
|
|
}
|
|
}
|