mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-03-06 02:46:57 +00:00
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
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.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) }
|
|
)
|
|
}
|
|
}
|
|
}
|