1 /* 2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.graphics 18 19 import android.graphics.Matrix 20 import android.graphics.Matrix.ScaleToFit.FILL 21 import android.graphics.Path 22 import android.graphics.Path.Direction 23 import android.graphics.Rect 24 import android.graphics.RectF 25 import android.graphics.Region 26 import android.platform.uiautomatorhelpers.DeviceHelpers.context 27 import android.view.View 28 import androidx.core.graphics.PathParser 29 import androidx.test.ext.junit.runners.AndroidJUnit4 30 import androidx.test.filters.SmallTest 31 import com.android.launcher3.graphics.ShapeDelegate.Circle 32 import com.android.launcher3.graphics.ShapeDelegate.Companion.AREA_CALC_SIZE 33 import com.android.launcher3.graphics.ShapeDelegate.Companion.AREA_DIFF_THRESHOLD 34 import com.android.launcher3.graphics.ShapeDelegate.Companion.areaDiffCalculator 35 import com.android.launcher3.graphics.ShapeDelegate.Companion.pickBestShape 36 import com.android.launcher3.graphics.ShapeDelegate.GenericPathShape 37 import com.android.launcher3.graphics.ShapeDelegate.RoundedSquare 38 import com.android.launcher3.icons.GraphicsUtils 39 import com.android.launcher3.views.ClipPathView 40 import com.google.common.truth.Truth.assertThat 41 import org.junit.Test 42 import org.junit.runner.RunWith 43 44 @SmallTest 45 @RunWith(AndroidJUnit4::class) 46 class ShapeDelegateTest { 47 48 @Test areaDiffCalculator increases with outwards shapenull49 fun `areaDiffCalculator increases with outwards shape`() { 50 val diffCalculator = 51 areaDiffCalculator( 52 Path().apply { 53 addCircle( 54 AREA_CALC_SIZE / 2f, 55 AREA_CALC_SIZE / 2f, 56 AREA_CALC_SIZE / 2f, 57 Direction.CW, 58 ) 59 } 60 ) 61 assertThat(diffCalculator(Circle())).isLessThan(AREA_DIFF_THRESHOLD) 62 assertThat(diffCalculator(Circle())).isLessThan(diffCalculator(RoundedSquare(.9f))) 63 assertThat(diffCalculator(RoundedSquare(.9f))) 64 .isLessThan(diffCalculator(RoundedSquare(.8f))) 65 assertThat(diffCalculator(RoundedSquare(.8f))) 66 .isLessThan(diffCalculator(RoundedSquare(.7f))) 67 assertThat(diffCalculator(RoundedSquare(.7f))) 68 .isLessThan(diffCalculator(RoundedSquare(.6f))) 69 assertThat(diffCalculator(RoundedSquare(.6f))) 70 .isLessThan(diffCalculator(RoundedSquare(.5f))) 71 } 72 73 @Test areaDiffCalculator increases with inwards shapenull74 fun `areaDiffCalculator increases with inwards shape`() { 75 val diffCalculator = areaDiffCalculator(roundedRectPath(0.5f)) 76 assertThat(diffCalculator(RoundedSquare(.5f))).isLessThan(AREA_DIFF_THRESHOLD) 77 assertThat(diffCalculator(RoundedSquare(.5f))) 78 .isLessThan(diffCalculator(RoundedSquare(.6f))) 79 assertThat(diffCalculator(RoundedSquare(.5f))) 80 .isLessThan(diffCalculator(RoundedSquare(.4f))) 81 } 82 83 @Test pickBestShape picks circlenull84 fun `pickBestShape picks circle`() { 85 val r = AREA_CALC_SIZE / 2 86 val pathStr = "M 50 0 a 50 50 0 0 1 0 100 a 50 50 0 0 1 0 -100" 87 val path = Path().apply { addCircle(r.toFloat(), r.toFloat(), r.toFloat(), Direction.CW) } 88 assertThat(pickBestShape(path, pathStr)).isInstanceOf(Circle::class.java) 89 } 90 91 @Test pickBestShape picks rounded rectnull92 fun `pickBestShape picks rounded rect`() { 93 val factor = 0.5f 94 var shape = pickBestShape(roundedRectPath(factor), roundedRectString(factor)) 95 assertThat(shape).isInstanceOf(RoundedSquare::class.java) 96 assertThat((shape as RoundedSquare).radiusRatio).isEqualTo(factor) 97 98 val factor2 = 0.2f 99 shape = pickBestShape(roundedRectPath(factor2), roundedRectString(factor2)) 100 assertThat(shape).isInstanceOf(RoundedSquare::class.java) 101 assertThat((shape as RoundedSquare).radiusRatio).isEqualTo(factor2) 102 } 103 104 @Test pickBestShape picks generic shapenull105 fun `pickBestShape picks generic shape`() { 106 val path = cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)) 107 val pathStr = FOUR_SIDED_COOKIE 108 val shape = pickBestShape(path, pathStr) 109 assertThat(shape).isInstanceOf(GenericPathShape::class.java) 110 111 val diffCalculator = areaDiffCalculator(path) 112 assertThat(diffCalculator(shape)).isLessThan(AREA_DIFF_THRESHOLD) 113 } 114 115 @Test generic shape creates smooth animationnull116 fun `generic shape creates smooth animation`() { 117 val shape = GenericPathShape(FOUR_SIDED_COOKIE) 118 val target = TestClipView() 119 val anim = 120 shape.createRevealAnimator( 121 target, 122 Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE), 123 Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE), 124 AREA_CALC_SIZE * .25f, 125 false, 126 ) 127 128 // Verify that the start rect is similar to initial path 129 anim.setCurrentFraction(0f) 130 assertThat( 131 getAreaDiff( 132 target.currentClip!!, 133 cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)), 134 ) 135 ) 136 .isLessThan(AREA_CALC_SIZE) 137 138 // Verify that end rect is similar to end path 139 anim.setCurrentFraction(1f) 140 assertThat(getAreaDiff(target.currentClip!!, roundedRectPath(0.5f))) 141 .isLessThan(AREA_CALC_SIZE) 142 143 // Ensure that when running animation, area increases smoothly. We run the animation over 144 // [steps] and verify increase of max 5 times the linear diff increase 145 val steps = 1000 146 val incrementalDiff = 147 getAreaDiff( 148 cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)), 149 roundedRectPath(0.5f), 150 ) * 5 / steps 151 var lastPath = cookiePath(Rect(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE)) 152 for (progress in 1..steps) { 153 anim.setCurrentFraction(progress / 1000f) 154 val currentPath = Path(target.currentClip!!) 155 assertThat(getAreaDiff(lastPath, currentPath)).isLessThan(incrementalDiff) 156 lastPath = currentPath 157 } 158 assertThat(getAreaDiff(lastPath, roundedRectPath(0.5f))).isLessThan(AREA_CALC_SIZE) 159 } 160 roundedRectPathnull161 private fun roundedRectPath(factor: Float) = 162 Path().apply { 163 val r = factor * AREA_CALC_SIZE / 2 164 addRoundRect( 165 0f, 166 0f, 167 AREA_CALC_SIZE.toFloat(), 168 AREA_CALC_SIZE.toFloat(), 169 r, 170 r, 171 Direction.CW, 172 ) 173 } 174 roundedRectStringnull175 private fun roundedRectString(factor: Float): String { 176 val s = 100f 177 val r = (factor * s / 2) 178 val t = s - r 179 return "M $r 0 " + 180 "L $t 0 " + 181 "A $r $r 0 0 1 $s $r " + 182 "L $s $t " + 183 "A $r $r 0 0 1 $t $s " + 184 "L $r $s " + 185 "A $r $r 0 0 1 0 $t " + 186 "L 0 $r " + 187 "A $r $r 0 0 1 $r 0 Z" 188 } 189 getAreaDiffnull190 private fun getAreaDiff(p1: Path, p2: Path): Int { 191 val fullRegion = Region(0, 0, AREA_CALC_SIZE, AREA_CALC_SIZE) 192 val iconRegion = Region().apply { setPath(p1, fullRegion) } 193 val shapeRegion = Region().apply { setPath(p2, fullRegion) } 194 shapeRegion.op(iconRegion, Region.Op.XOR) 195 return GraphicsUtils.getArea(shapeRegion) 196 } 197 198 class TestClipView : View(context), ClipPathView { 199 200 var currentClip: Path? = null 201 setClipPathnull202 override fun setClipPath(clipPath: Path?) { 203 currentClip = clipPath 204 } 205 } 206 207 companion object { 208 const val FOUR_SIDED_COOKIE = 209 "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" 210 cookiePathnull211 private fun cookiePath(bounds: Rect) = 212 PathParser.createPathFromPathData(FOUR_SIDED_COOKIE).apply { 213 transform( 214 Matrix().apply { setRectToRect(RectF(0f, 0f, 100f, 100f), RectF(bounds), FILL) } 215 ) 216 } 217 } 218 } 219