Loading src/com/android/launcher3/Utilities.java +53 −9 Original line number Diff line number Diff line Loading @@ -106,8 +106,6 @@ import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Various utilities shared amongst the Launcher's classes. Loading @@ -116,8 +114,7 @@ public final class Utilities { private static final String TAG = "Launcher.Utilities"; private static final Pattern sTrimPattern = Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); private static final String TRIM_PATTERN = "(^\\h+|\\h+$)"; private static final Matrix sMatrix = new Matrix(); private static final Matrix sInverseMatrix = new Matrix(); Loading Loading @@ -445,10 +442,7 @@ public final class Utilities { if (s == null) { return ""; } // Just strip any sequence of whitespace or java space characters from the beginning and end Matcher m = sTrimPattern.matcher(s); return m.replaceAll("$1"); return s.toString().replaceAll(TRIM_PATTERN, "").trim(); } /** Loading Loading @@ -722,14 +716,59 @@ public final class Utilities { } /** * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CCW. Parent * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CW. Parent * sizes represent the "space" that will rotate carrying inOutBounds along with it to determine * the final bounds. * * As an example if this is the input: * +-------------+ * | +-----+ | * | | | | * | +-----+ | * | | * | | * | | * +-------------+ * This would be case delta % 4 == 0: * +-------------+ * | +-----+ | * | | | | * | +-----+ | * | | * | | * | | * +-------------+ * This would be case delta % 4 == 1: * +----------------+ * | +--+ | * | | | | * | | | | * | +--+ | * | | * +----------------+ * This would be case delta % 4 == 2: * +-------------+ * | | * | | * | | * | +-----+ | * | | | | * | +-----+ | * +-------------+ * This would be case delta % 4 == 3: * +----------------+ * | +--+ | * | | | | * | | | | * | +--+ | * | | * +----------------+ */ public static void rotateBounds(Rect inOutBounds, int parentWidth, int parentHeight, int delta) { int rdelta = ((delta % 4) + 4) % 4; int origLeft = inOutBounds.left; int origTop = inOutBounds.top; switch (rdelta) { case 0: return; Loading @@ -741,6 +780,8 @@ public final class Utilities { return; case 2: inOutBounds.left = parentWidth - inOutBounds.right; inOutBounds.top = parentHeight - inOutBounds.bottom; inOutBounds.bottom = parentHeight - origTop; inOutBounds.right = parentWidth - origLeft; return; case 3: Loading Loading @@ -830,6 +871,9 @@ public final class Utilities { @NonNull Rect inclusionBounds, @NonNull Rect exclusionBounds, @AdjustmentDirection int adjustmentDirection) { if (!Rect.intersects(targetViewBounds, exclusionBounds)) { return; } switch (adjustmentDirection) { case TRANSLATE_RIGHT: targetView.setTranslationX(Math.min( Loading tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt +291 −1 Original line number Diff line number Diff line Loading @@ -17,12 +17,19 @@ package com.android.launcher3 import android.content.Context import android.content.ContextWrapper import android.graphics.Rect import android.graphics.RectF import android.view.View import android.view.ViewGroup import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.util.ActivityContextWrapper import org.junit.Assert.* import kotlin.random.Random import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith Loading @@ -30,6 +37,10 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class UtilitiesTest { companion object { const val SEED = 827 } private lateinit var mContext: Context @Before Loading Loading @@ -94,4 +105,283 @@ class UtilitiesTest { assertTrue(Utilities.pointInView(view, -5f, -5f, 10f)) // Inside slop assertFalse(Utilities.pointInView(view, 115f, 115f, 10f)) // Outside slop } @Test fun testNumberBounding() { assertEquals(887.99f, Utilities.boundToRange(887.99f, 0f, 1000f)) assertEquals(2.777f, Utilities.boundToRange(887.99f, 0f, 2.777f)) assertEquals(900f, Utilities.boundToRange(887.99f, 900f, 1000f)) assertEquals(9383667L, Utilities.boundToRange(9383667L, -999L, 9999999L)) assertEquals(9383668L, Utilities.boundToRange(9383667L, 9383668L, 9999999L)) assertEquals(42L, Utilities.boundToRange(9383667L, -999L, 42L)) assertEquals(345, Utilities.boundToRange(345, 2, 500)) assertEquals(400, Utilities.boundToRange(345, 400, 500)) assertEquals(300, Utilities.boundToRange(345, 2, 300)) val random = Random(SEED) for (i in 1..300) { val value = random.nextFloat() val lowerBound = random.nextFloat() val higherBound = lowerBound + random.nextFloat() assertEquals( "Utilities.boundToRange doesn't match Kotlin coerceIn", value.coerceIn(lowerBound, higherBound), Utilities.boundToRange(value, lowerBound, higherBound) ) assertEquals( "Utilities.boundToRange doesn't match Kotlin coerceIn", value.toInt().coerceIn(lowerBound.toInt(), higherBound.toInt()), Utilities.boundToRange(value.toInt(), lowerBound.toInt(), higherBound.toInt()) ) assertEquals( "Utilities.boundToRange doesn't match Kotlin coerceIn", value.toLong().coerceIn(lowerBound.toLong(), higherBound.toLong()), Utilities.boundToRange(value.toLong(), lowerBound.toLong(), higherBound.toLong()) ) assertEquals( "If the lower bound is higher than lower bound, it should return the lower bound", higherBound, Utilities.boundToRange(value, higherBound, lowerBound) ) } } @Test fun testTranslateOverlappingView() { testConcentricOverlap() leftDownCornerOverlap() noOverlap() } /* Test Case: Rectangle Contained Within Another Rectangle +-------------+ <-- exclusionBounds | | | +-----+ | | | | | <-- targetViewBounds | | | | | +-----+ | | | +-------------+ */ private fun testConcentricOverlap() { val targetView = View(ContextWrapper(getApplicationContext())) val targetViewBounds = Rect(40, 40, 60, 60) val inclusionBounds = Rect(0, 0, 100, 100) val exclusionBounds = Rect(30, 30, 70, 70) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_RIGHT ) assertEquals(30f, targetView.translationX) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_LEFT ) assertEquals(-30f, targetView.translationX) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_DOWN ) assertEquals(30f, targetView.translationY) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_UP ) assertEquals(-30f, targetView.translationY) } /* Test Case: Non-Overlapping Rectangles +-----------------+ <-- targetViewBounds | | | | +-----------------+ +-----------+ <-- exclusionBounds | | | | +-----------+ */ private fun noOverlap() { val targetView = View(ContextWrapper(getApplicationContext())) val targetViewBounds = Rect(10, 10, 20, 20) val inclusionBounds = Rect(0, 0, 100, 100) val exclusionBounds = Rect(30, 30, 40, 40) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_RIGHT ) assertEquals(0f, targetView.translationX) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_LEFT ) assertEquals(0f, targetView.translationX) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_DOWN ) assertEquals(0f, targetView.translationY) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_UP ) assertEquals(0f, targetView.translationY) } /* Test Case: Rectangles Overlapping at Corners +------------+ <-- exclusionBounds | | +-------+ | | | | | <-- targetViewBounds | +------------+ | | +-------+ */ private fun leftDownCornerOverlap() { val targetView = View(ContextWrapper(getApplicationContext())) val targetViewBounds = Rect(20, 20, 30, 30) val inclusionBounds = Rect(0, 0, 100, 100) val exclusionBounds = Rect(25, 25, 35, 35) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_RIGHT ) assertEquals(15f, targetView.translationX) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_LEFT ) assertEquals(-5f, targetView.translationX) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_DOWN ) assertEquals(15f, targetView.translationY) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_UP ) assertEquals(-5f, targetView.translationY) } @Test fun trim() { val expectedString = "Hello World" assertEquals(expectedString, Utilities.trim("Hello World ")) // Basic trimming assertEquals(expectedString, Utilities.trim(" Hello World ")) assertEquals(expectedString, Utilities.trim(" Hello World")) // Non-breaking whitespace assertEquals("Hello World", Utilities.trim("\u00A0\u00A0Hello World\u00A0\u00A0")) // Whitespace combinations assertEquals(expectedString, Utilities.trim("\t \r\n Hello World \n\r")) assertEquals(expectedString, Utilities.trim("\nHello World ")) // Null input assertEquals("", Utilities.trim(null)) // Empty String assertEquals("", Utilities.trim("")) } @Test fun getProgress() { // Basic test assertEquals(0.5f, Utilities.getProgress(50f, 0f, 100f), 0.001f) // Negative values assertEquals(0.5f, Utilities.getProgress(-20f, -50f, 10f), 0.001f) // Outside of range assertEquals(1.2f, Utilities.getProgress(120f, 0f, 100f), 0.001f) } @Test fun scaleRectFAboutPivot() { // Enlarge var rectF = RectF(10f, 20f, 50f, 80f) Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 1.5f) assertEquals(RectF(0f, 5f, 60f, 95f), rectF) // Shrink rectF = RectF(10f, 20f, 50f, 80f) Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 0.5f) assertEquals(RectF(20f, 35f, 40f, 65f), rectF) // No scale rectF = RectF(10f, 20f, 50f, 80f) Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 1.0f) assertEquals(RectF(10f, 20f, 50f, 80f), rectF) } @Test fun rotateBounds() { var rect = Rect(20, 70, 60, 80) Utilities.rotateBounds(rect, 100, 100, 0) assertEquals(Rect(20, 70, 60, 80), rect) rect = Rect(20, 70, 60, 80) Utilities.rotateBounds(rect, 100, 100, 1) assertEquals(Rect(70, 40, 80, 80), rect) rect = Rect(20, 70, 60, 80) Utilities.rotateBounds(rect, 100, 100, 2) assertEquals(Rect(40, 20, 80, 30), rect) rect = Rect(20, 70, 60, 80) Utilities.rotateBounds(rect, 100, 100, 3) assertEquals(Rect(20, 20, 30, 60), rect) } } Loading
src/com/android/launcher3/Utilities.java +53 −9 Original line number Diff line number Diff line Loading @@ -106,8 +106,6 @@ import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Various utilities shared amongst the Launcher's classes. Loading @@ -116,8 +114,7 @@ public final class Utilities { private static final String TAG = "Launcher.Utilities"; private static final Pattern sTrimPattern = Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); private static final String TRIM_PATTERN = "(^\\h+|\\h+$)"; private static final Matrix sMatrix = new Matrix(); private static final Matrix sInverseMatrix = new Matrix(); Loading Loading @@ -445,10 +442,7 @@ public final class Utilities { if (s == null) { return ""; } // Just strip any sequence of whitespace or java space characters from the beginning and end Matcher m = sTrimPattern.matcher(s); return m.replaceAll("$1"); return s.toString().replaceAll(TRIM_PATTERN, "").trim(); } /** Loading Loading @@ -722,14 +716,59 @@ public final class Utilities { } /** * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CCW. Parent * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CW. Parent * sizes represent the "space" that will rotate carrying inOutBounds along with it to determine * the final bounds. * * As an example if this is the input: * +-------------+ * | +-----+ | * | | | | * | +-----+ | * | | * | | * | | * +-------------+ * This would be case delta % 4 == 0: * +-------------+ * | +-----+ | * | | | | * | +-----+ | * | | * | | * | | * +-------------+ * This would be case delta % 4 == 1: * +----------------+ * | +--+ | * | | | | * | | | | * | +--+ | * | | * +----------------+ * This would be case delta % 4 == 2: * +-------------+ * | | * | | * | | * | +-----+ | * | | | | * | +-----+ | * +-------------+ * This would be case delta % 4 == 3: * +----------------+ * | +--+ | * | | | | * | | | | * | +--+ | * | | * +----------------+ */ public static void rotateBounds(Rect inOutBounds, int parentWidth, int parentHeight, int delta) { int rdelta = ((delta % 4) + 4) % 4; int origLeft = inOutBounds.left; int origTop = inOutBounds.top; switch (rdelta) { case 0: return; Loading @@ -741,6 +780,8 @@ public final class Utilities { return; case 2: inOutBounds.left = parentWidth - inOutBounds.right; inOutBounds.top = parentHeight - inOutBounds.bottom; inOutBounds.bottom = parentHeight - origTop; inOutBounds.right = parentWidth - origLeft; return; case 3: Loading Loading @@ -830,6 +871,9 @@ public final class Utilities { @NonNull Rect inclusionBounds, @NonNull Rect exclusionBounds, @AdjustmentDirection int adjustmentDirection) { if (!Rect.intersects(targetViewBounds, exclusionBounds)) { return; } switch (adjustmentDirection) { case TRANSLATE_RIGHT: targetView.setTranslationX(Math.min( Loading
tests/multivalentTests/src/com/android/launcher3/UtilitiesTest.kt +291 −1 Original line number Diff line number Diff line Loading @@ -17,12 +17,19 @@ package com.android.launcher3 import android.content.Context import android.content.ContextWrapper import android.graphics.Rect import android.graphics.RectF import android.view.View import android.view.ViewGroup import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.util.ActivityContextWrapper import org.junit.Assert.* import kotlin.random.Random import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith Loading @@ -30,6 +37,10 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class UtilitiesTest { companion object { const val SEED = 827 } private lateinit var mContext: Context @Before Loading Loading @@ -94,4 +105,283 @@ class UtilitiesTest { assertTrue(Utilities.pointInView(view, -5f, -5f, 10f)) // Inside slop assertFalse(Utilities.pointInView(view, 115f, 115f, 10f)) // Outside slop } @Test fun testNumberBounding() { assertEquals(887.99f, Utilities.boundToRange(887.99f, 0f, 1000f)) assertEquals(2.777f, Utilities.boundToRange(887.99f, 0f, 2.777f)) assertEquals(900f, Utilities.boundToRange(887.99f, 900f, 1000f)) assertEquals(9383667L, Utilities.boundToRange(9383667L, -999L, 9999999L)) assertEquals(9383668L, Utilities.boundToRange(9383667L, 9383668L, 9999999L)) assertEquals(42L, Utilities.boundToRange(9383667L, -999L, 42L)) assertEquals(345, Utilities.boundToRange(345, 2, 500)) assertEquals(400, Utilities.boundToRange(345, 400, 500)) assertEquals(300, Utilities.boundToRange(345, 2, 300)) val random = Random(SEED) for (i in 1..300) { val value = random.nextFloat() val lowerBound = random.nextFloat() val higherBound = lowerBound + random.nextFloat() assertEquals( "Utilities.boundToRange doesn't match Kotlin coerceIn", value.coerceIn(lowerBound, higherBound), Utilities.boundToRange(value, lowerBound, higherBound) ) assertEquals( "Utilities.boundToRange doesn't match Kotlin coerceIn", value.toInt().coerceIn(lowerBound.toInt(), higherBound.toInt()), Utilities.boundToRange(value.toInt(), lowerBound.toInt(), higherBound.toInt()) ) assertEquals( "Utilities.boundToRange doesn't match Kotlin coerceIn", value.toLong().coerceIn(lowerBound.toLong(), higherBound.toLong()), Utilities.boundToRange(value.toLong(), lowerBound.toLong(), higherBound.toLong()) ) assertEquals( "If the lower bound is higher than lower bound, it should return the lower bound", higherBound, Utilities.boundToRange(value, higherBound, lowerBound) ) } } @Test fun testTranslateOverlappingView() { testConcentricOverlap() leftDownCornerOverlap() noOverlap() } /* Test Case: Rectangle Contained Within Another Rectangle +-------------+ <-- exclusionBounds | | | +-----+ | | | | | <-- targetViewBounds | | | | | +-----+ | | | +-------------+ */ private fun testConcentricOverlap() { val targetView = View(ContextWrapper(getApplicationContext())) val targetViewBounds = Rect(40, 40, 60, 60) val inclusionBounds = Rect(0, 0, 100, 100) val exclusionBounds = Rect(30, 30, 70, 70) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_RIGHT ) assertEquals(30f, targetView.translationX) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_LEFT ) assertEquals(-30f, targetView.translationX) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_DOWN ) assertEquals(30f, targetView.translationY) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_UP ) assertEquals(-30f, targetView.translationY) } /* Test Case: Non-Overlapping Rectangles +-----------------+ <-- targetViewBounds | | | | +-----------------+ +-----------+ <-- exclusionBounds | | | | +-----------+ */ private fun noOverlap() { val targetView = View(ContextWrapper(getApplicationContext())) val targetViewBounds = Rect(10, 10, 20, 20) val inclusionBounds = Rect(0, 0, 100, 100) val exclusionBounds = Rect(30, 30, 40, 40) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_RIGHT ) assertEquals(0f, targetView.translationX) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_LEFT ) assertEquals(0f, targetView.translationX) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_DOWN ) assertEquals(0f, targetView.translationY) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_UP ) assertEquals(0f, targetView.translationY) } /* Test Case: Rectangles Overlapping at Corners +------------+ <-- exclusionBounds | | +-------+ | | | | | <-- targetViewBounds | +------------+ | | +-------+ */ private fun leftDownCornerOverlap() { val targetView = View(ContextWrapper(getApplicationContext())) val targetViewBounds = Rect(20, 20, 30, 30) val inclusionBounds = Rect(0, 0, 100, 100) val exclusionBounds = Rect(25, 25, 35, 35) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_RIGHT ) assertEquals(15f, targetView.translationX) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_LEFT ) assertEquals(-5f, targetView.translationX) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_DOWN ) assertEquals(15f, targetView.translationY) Utilities.translateOverlappingView( targetView, targetViewBounds, inclusionBounds, exclusionBounds, Utilities.TRANSLATE_UP ) assertEquals(-5f, targetView.translationY) } @Test fun trim() { val expectedString = "Hello World" assertEquals(expectedString, Utilities.trim("Hello World ")) // Basic trimming assertEquals(expectedString, Utilities.trim(" Hello World ")) assertEquals(expectedString, Utilities.trim(" Hello World")) // Non-breaking whitespace assertEquals("Hello World", Utilities.trim("\u00A0\u00A0Hello World\u00A0\u00A0")) // Whitespace combinations assertEquals(expectedString, Utilities.trim("\t \r\n Hello World \n\r")) assertEquals(expectedString, Utilities.trim("\nHello World ")) // Null input assertEquals("", Utilities.trim(null)) // Empty String assertEquals("", Utilities.trim("")) } @Test fun getProgress() { // Basic test assertEquals(0.5f, Utilities.getProgress(50f, 0f, 100f), 0.001f) // Negative values assertEquals(0.5f, Utilities.getProgress(-20f, -50f, 10f), 0.001f) // Outside of range assertEquals(1.2f, Utilities.getProgress(120f, 0f, 100f), 0.001f) } @Test fun scaleRectFAboutPivot() { // Enlarge var rectF = RectF(10f, 20f, 50f, 80f) Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 1.5f) assertEquals(RectF(0f, 5f, 60f, 95f), rectF) // Shrink rectF = RectF(10f, 20f, 50f, 80f) Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 0.5f) assertEquals(RectF(20f, 35f, 40f, 65f), rectF) // No scale rectF = RectF(10f, 20f, 50f, 80f) Utilities.scaleRectFAboutPivot(rectF, 30f, 50f, 1.0f) assertEquals(RectF(10f, 20f, 50f, 80f), rectF) } @Test fun rotateBounds() { var rect = Rect(20, 70, 60, 80) Utilities.rotateBounds(rect, 100, 100, 0) assertEquals(Rect(20, 70, 60, 80), rect) rect = Rect(20, 70, 60, 80) Utilities.rotateBounds(rect, 100, 100, 1) assertEquals(Rect(70, 40, 80, 80), rect) rect = Rect(20, 70, 60, 80) Utilities.rotateBounds(rect, 100, 100, 2) assertEquals(Rect(40, 20, 80, 30), rect) rect = Rect(20, 70, 60, 80) Utilities.rotateBounds(rect, 100, 100, 3) assertEquals(Rect(20, 20, 30, 60), rect) } }