From 824dc56eb3e180acc4433e28362287963b9f59f8 Mon Sep 17 00:00:00 2001 From: Kyle Agronick Date: Tue, 9 Feb 2021 21:33:10 -0500 Subject: [PATCH 1/9] Kind of working --- .idea/compiler.xml | 6 + .idea/gradle.xml | 2 + .idea/misc.xml | 2 +- .../main/java/com/agronick/launcher/App.kt | 27 ++-- .../java/com/agronick/launcher/Container.kt | 11 +- .../com/agronick/launcher/MainActivity.kt | 10 ++ .../java/com/agronick/launcher/MainView.kt | 118 ++++++++++++------ .../java/com/agronick/launcher/Reorderer.kt | 79 ++++++++++++ .../com/agronick/launcher/StaticValues.kt | 11 ++ app/src/main/res/layout/activity_main.xml | 15 --- app/src/main/res/values/dimens.xml | 14 +-- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- 13 files changed, 212 insertions(+), 89 deletions(-) create mode 100644 .idea/compiler.xml create mode 100644 app/src/main/java/com/agronick/launcher/Reorderer.kt create mode 100644 app/src/main/java/com/agronick/launcher/StaticValues.kt delete mode 100644 app/src/main/res/layout/activity_main.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..61a9130 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index b9f8a5e..23a89bb 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,5 +1,6 @@ + diff --git a/.idea/misc.xml b/.idea/misc.xml index 7bfef59..d5d35ec 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/app/src/main/java/com/agronick/launcher/App.kt b/app/src/main/java/com/agronick/launcher/App.kt index d672af2..43eb019 100644 --- a/app/src/main/java/com/agronick/launcher/App.kt +++ b/app/src/main/java/com/agronick/launcher/App.kt @@ -2,7 +2,6 @@ package com.agronick.launcher import android.graphics.Canvas import android.graphics.Rect -import androidx.core.graphics.drawable.toBitmap import util.geometry.Circle import util.geometry.CircleCircleIntersection import util.geometry.Vector2 @@ -10,20 +9,20 @@ import util.geometry.Vector2 class App(val pkgInfo: PInfo, var size: Int) { var left = 0.0f var top = 0.0f - var bitmap = pkgInfo.icon.toBitmap(size * 2, size * 2) + var hidden = false private var lastCircle: Circle? = null - fun asOpenAnimator(endSize: Int): App { + fun copy(): App { val other = App(pkgInfo, size) other.left = left other.top = top - other.bitmap = pkgInfo.icon.toBitmap(endSize * 2, endSize * 2) return other } fun drawNormal(canvas: Canvas) { - if (lastCircle != null) { - val radius = lastCircle!!.r + if (!hidden) { + val radius = + if (lastCircle !== null) (lastCircle!!.r).coerceAtLeast(10f) else size.toFloat() draw(canvas, radius, left, top) } } @@ -33,17 +32,13 @@ class App(val pkgInfo: PInfo, var size: Int) { } fun draw(canvas: Canvas, radius: Float, x: Float, y: Float) { - canvas.drawBitmap( - bitmap, - null, - Rect( - (x - radius).toInt(), - (y - radius).toInt(), - (x + radius).toInt(), - (y + radius).toInt() - ), - null + pkgInfo.icon.bounds = Rect( + (x - radius).toInt(), + (y - radius).toInt(), + (x + radius).toInt(), + (y + radius).toInt() ) + pkgInfo.icon.draw(canvas) } fun getCircle(faceCircle: Circle): Circle? { diff --git a/app/src/main/java/com/agronick/launcher/Container.kt b/app/src/main/java/com/agronick/launcher/Container.kt index 16da716..0f34bdd 100644 --- a/app/src/main/java/com/agronick/launcher/Container.kt +++ b/app/src/main/java/com/agronick/launcher/Container.kt @@ -4,12 +4,13 @@ import android.graphics.Canvas import util.geometry.Circle import util.geometry.Vector2 import kotlin.math.ceil +import kotlin.math.roundToInt import kotlin.math.sqrt -class Container(appList: List) { +class Container(appList: List, density: Float) { private val rows: List> - val size = 48 - val margin = 2 + val size = (StaticValues.normalAppSize * density).roundToInt() + val margin = (StaticValues.margin * density).roundToInt() private val iterate: Sequence> var lastCircle: Circle? = null @@ -103,7 +104,7 @@ class Container(appList: List) { return pos } - fun getClickedPackage(x: Float, y: Float): App? { + fun getAppAtPoint(x: Float, y: Float): App? { return iterate.find { it.first.intersects(x, y) }?.first } @@ -120,7 +121,7 @@ class Container(appList: List) { } fun getLimit(x: Float, y: Float, size: Float): Pair { - val halfSize = size * 0.5f + val halfSize = (lastCircle?.r ?: size) * 0.75f var outX = x var outY = y if (x - halfSize < leftLimit) { diff --git a/app/src/main/java/com/agronick/launcher/MainActivity.kt b/app/src/main/java/com/agronick/launcher/MainActivity.kt index 6bf87c3..ccaa205 100644 --- a/app/src/main/java/com/agronick/launcher/MainActivity.kt +++ b/app/src/main/java/com/agronick/launcher/MainActivity.kt @@ -24,6 +24,11 @@ class MainActivity : Activity() { } mainView = MainView(this.baseContext, getInstalledApps()) + if (savedInstanceState != null) { + mainView.offsetLeft = savedInstanceState.getFloat("offsetLeft") + mainView.offsetTop = savedInstanceState.getFloat("offsetTop") + } + setContentView(mainView) mainView.onPackageClick = this::onPackageClick } @@ -56,4 +61,9 @@ class MainActivity : Activity() { } } + override fun onSaveInstanceState(outState: Bundle) { + outState.putFloat("offsetLeft", mainView.offsetLeft) + outState.putFloat("offsetTop", mainView.offsetTop) + super.onSaveInstanceState(outState) + } } diff --git a/app/src/main/java/com/agronick/launcher/MainView.kt b/app/src/main/java/com/agronick/launcher/MainView.kt index 3f3223d..cba98ad 100644 --- a/app/src/main/java/com/agronick/launcher/MainView.kt +++ b/app/src/main/java/com/agronick/launcher/MainView.kt @@ -8,20 +8,15 @@ import android.util.Log import android.view.MotionEvent import android.view.View import androidx.core.animation.doOnEnd +import java.util.* class MainView(context: Context, appList: List) : View(context) { - init { - Runnable { - container = Container(appList) - }.run() - } - + private var density: Float var onPackageClick: ((PInfo) -> Unit)? = null private lateinit var container: Container - private var offsetLeft = 0f - private var offsetTop = 0f - + var offsetLeft = 0f + var offsetTop = 0f private var previousX: Float = 0f private var previousY: Float = 0f private var hasMoved = false @@ -29,6 +24,16 @@ class MainView(context: Context, appList: List) : View(context) { private var openingApp: App? = null + init { + density = context.resources.displayMetrics.density + Runnable { + container = Container(appList, density) + }.run() + } + + var holdTimer = Timer() + var reorderer: Reorderer? = null + override fun onTouchEvent(event: MotionEvent?): Boolean { if (event != null) { when (event.action) { @@ -36,27 +41,38 @@ class MainView(context: Context, appList: List) : View(context) { previousX = event.x previousY = event.y hasMoved = false + holdTimer.schedule(object : TimerTask() { + override fun run() { + reorderer = Reorderer(container, ::prepareInvalidate) + val offset = getRelativePosition(Pair(event.x, event.y)) + post { + reorderer!!.onHoldDown(offset.first, offset.second) + } + } + }, 3000) return true } MotionEvent.ACTION_MOVE -> { - val newOffsetLeft = offsetLeft + event.x - previousX - val newOffsetTop = offsetTop + event.y - previousY - val limited = container.getLimit(newOffsetLeft, newOffsetTop, canvasSize) - offsetLeft = limited.first - offsetTop = limited.second - if (newOffsetLeft == limited.first) { + hasMoved = true + if (reorderer == null) { + offsetLeft += event.x - previousX + offsetTop += event.y - previousY previousX = event.x - } - if (newOffsetTop == limited.second) { previousY = event.y + } else { + val offset = getRelativePosition(Pair(event.x, event.y)) + reorderer!!.onMove(offset.first, offset.second) } - hasMoved = true prepareInvalidate() return true } MotionEvent.ACTION_UP -> { + holdTimer.cancel() + holdTimer = Timer() if (!hasMoved) { handleClick(event.x, event.y) + } else { + checkOverLimit() } } } @@ -64,16 +80,45 @@ class MainView(context: Context, appList: List) : View(context) { return super.onTouchEvent(event) } - fun handleClick(x: Float, y: Float) { - val offset = getOffset() - if (offset != null) { - val app = container.getClickedPackage(x - offset.first, y - offset.second) - if (app != null) { - setupOpenAnim(app.asOpenAnimator(canvasSize.toInt())) + fun checkOverLimit() { + val limited = container.getLimit(offsetLeft, offsetTop, canvasSize) + var animators = mutableListOf() + if (offsetLeft != limited.first) { + animators.add(ValueAnimator.ofFloat(offsetLeft, limited.first) + .apply { + addUpdateListener { animator -> + offsetLeft = animator.animatedValue as Float + } + } + ) + } + if (offsetTop != limited.second) { + animators.add(ValueAnimator.ofFloat(offsetTop, limited.second) + .apply { + addUpdateListener { animator -> + offsetTop = animator.animatedValue as Float + } + } + ) + } + if (animators.isNotEmpty()) { + animators[0].addUpdateListener { prepareInvalidate() } + AnimatorSet().apply { + duration = StaticValues.durationOpen + playTogether(*animators.toTypedArray()) + start() } } } + fun handleClick(x: Float, y: Float) { + val offset = getRelativePosition(Pair(x, y)) + val app = container.getAppAtPoint(offset.first, offset.second) + if (app != null) { + setupOpenAnim(app.copy()) + } + } + fun setupOpenAnim(app: App) { val face = container.lastCircle ?: return openingApp = app @@ -91,7 +136,7 @@ class MainView(context: Context, appList: List) : View(context) { } } val radiusAnimator = - ValueAnimator.ofFloat(app.size.toFloat(), canvasSize) + ValueAnimator.ofFloat(app.size.toFloat(), canvasSize * 0.5f) .apply { addUpdateListener { animator -> app.size = ((animator.animatedValue as Float).toInt()) @@ -120,9 +165,13 @@ class MainView(context: Context, appList: List) : View(context) { super.onSizeChanged(w, h, oldw, oldh) } - fun getOffset(): Pair? { + fun getRelativePosition(point: Pair? = null): Pair { val halfSize = canvasSize * 0.5f - return Pair(halfSize + offsetLeft, halfSize + offsetTop) + val pos = Pair(halfSize + offsetLeft, halfSize + offsetTop) + if (point != null) { + return Pair(point.first - pos.first, point.second - pos.second) + } + return pos } fun prepareInvalidate() { @@ -134,18 +183,13 @@ class MainView(context: Context, appList: List) : View(context) { } override fun onDraw(canvas: Canvas?) { - Log.d( - TAG, - "draw called $offsetLeft $offsetTop" - ) if (canvas == null) return Runnable { - val offset = getOffset() - if (offset != null) { - canvas.translate(offset.first, offset.second) - container.draw(canvas) - openingApp?.drawNormal(canvas) - } + val offset = getRelativePosition() + canvas.translate(offset.first, offset.second) + container.draw(canvas) + openingApp?.drawNormal(canvas) + reorderer?.draw(canvas) }.run() } } \ No newline at end of file diff --git a/app/src/main/java/com/agronick/launcher/Reorderer.kt b/app/src/main/java/com/agronick/launcher/Reorderer.kt new file mode 100644 index 0000000..9d94341 --- /dev/null +++ b/app/src/main/java/com/agronick/launcher/Reorderer.kt @@ -0,0 +1,79 @@ +package com.agronick.launcher + +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.graphics.Canvas +import androidx.core.animation.doOnEnd + +class Reorderer(private val container: Container, val invalidate: () -> Unit) { + private var activeAppCopy: App? = null + private var lastOverlap: App? = null + private var suppressedAppCopy: App? = null + private var lastPosition = HashMap>() + private var lastFreeSpace = Pair(0f, 0f) + + fun onHoldDown(x: Float, y: Float): Boolean { + val app = container.getAppAtPoint(x, y) + if (app != null) { + activeAppCopy = app.copy() + suppressedAppCopy = app + lastOverlap = suppressedAppCopy + suppressedAppCopy!!.hidden = true + lastPosition[app] = Pair(app.left, app.top) + lastFreeSpace = Pair(app.left, app.top) + ValueAnimator.ofInt(activeAppCopy!!.size, (activeAppCopy!!.size * 1.4).toInt()) + .apply { + duration = StaticValues.durationRise + addUpdateListener { animator -> + activeAppCopy!!.size = animator.animatedValue as Int + invalidate() + } + }.start() + return true + } + return false + } + + fun onMove(x: Float, y: Float) { + activeAppCopy!!.left = x + activeAppCopy!!.top = y + val app = container.getAppAtPoint(x, y) + if (app !== null && activeAppCopy !== null && app != activeAppCopy && app != lastOverlap) { + lastPosition[app] = Pair(app.left, app.top) + val positions = lastFreeSpace + lastFreeSpace = Pair(app.left, app.top) + animateAppPosition(app, positions.first, positions.second) + lastOverlap = app + } + } + + fun animateAppPosition(app: App, x: Float, y: Float) { + val xAnim = ValueAnimator.ofFloat(app.left, x).apply { + duration = StaticValues.durationSwap + addUpdateListener { animator -> + app.left = animator.animatedValue as Float + invalidate() + } + } + val yAnim = ValueAnimator.ofFloat(app.top, y).apply { + duration = StaticValues.durationSwap + addUpdateListener { animator -> + app.top = animator.animatedValue as Float + } + } + AnimatorSet().apply { + duration = StaticValues.durationOpen + playTogether(xAnim, yAnim) + doOnEnd { + lastOverlap = null + } + start() + } + } + + fun draw(canvas: Canvas) { + if (activeAppCopy !== null) { + activeAppCopy!!.drawNormal(canvas) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/agronick/launcher/StaticValues.kt b/app/src/main/java/com/agronick/launcher/StaticValues.kt new file mode 100644 index 0000000..31c3eb1 --- /dev/null +++ b/app/src/main/java/com/agronick/launcher/StaticValues.kt @@ -0,0 +1,11 @@ +package com.agronick.launcher + +class StaticValues { + companion object { + val margin = 1 + val normalAppSize = 24 + val durationRise = 300L + val durationSwap = 300L + val durationOpen = 600L + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 1b6b9b1..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e865b41..f1b9f95 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,15 +1,5 @@ - - 0dp - - - 5dp + 28 + 1 diff --git a/build.gradle b/build.gradle index 3023c19..ff44dab 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { jcenter() } dependencies { - classpath "com.android.tools.build:gradle:4.0.2" + classpath 'com.android.tools.build:gradle:4.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d5e04f7..f736d1b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Feb 06 14:50:12 EST 2021 +#Tue Feb 09 20:16:16 EST 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip From dc2824fc9b72a8a3f43501d4ab69a50b7ab0ca47 Mon Sep 17 00:00:00 2001 From: Kyle Agronick Date: Mon, 15 Feb 2021 22:40:57 -0500 Subject: [PATCH 2/9] Reordering working --- .../main/java/com/agronick/launcher/App.kt | 26 ++++---- .../java/com/agronick/launcher/Container.kt | 9 ++- .../java/com/agronick/launcher/MainView.kt | 23 +++++-- .../java/com/agronick/launcher/Reorderer.kt | 65 +++++++++---------- gradle/wrapper/gradle-wrapper.properties | 4 +- 5 files changed, 70 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/com/agronick/launcher/App.kt b/app/src/main/java/com/agronick/launcher/App.kt index 43eb019..a162a50 100644 --- a/app/src/main/java/com/agronick/launcher/App.kt +++ b/app/src/main/java/com/agronick/launcher/App.kt @@ -74,18 +74,20 @@ class App(val pkgInfo: PInfo, var size: Int) { } fun intersects(x: Float, y: Float): Boolean { - if (null == lastCircle) return false - val circle = getCircle(lastCircle!!) - val point = Circle( - Vector2( - x, - y - ), 3.0f - ) - return CircleCircleIntersection( - circle, - point - ).type == CircleCircleIntersection.Type.ECCENTRIC_CONTAINED + val circle = lastCircle + if (circle != null) { + val point = Circle( + Vector2( + x, + y + ), 3.0f + ) + return CircleCircleIntersection( + circle, + point + ).type == CircleCircleIntersection.Type.ECCENTRIC_CONTAINED + } + return false } } \ No newline at end of file diff --git a/app/src/main/java/com/agronick/launcher/Container.kt b/app/src/main/java/com/agronick/launcher/Container.kt index 0f34bdd..369c8c4 100644 --- a/app/src/main/java/com/agronick/launcher/Container.kt +++ b/app/src/main/java/com/agronick/launcher/Container.kt @@ -104,8 +104,13 @@ class Container(appList: List, density: Float) { return pos } - fun getAppAtPoint(x: Float, y: Float): App? { - return iterate.find { it.first.intersects(x, y) }?.first + fun getAppAtPoint(x: Float, y: Float, toIgnore: HashSet? = null): App? { + return iterate.find { + (if (toIgnore != null) !toIgnore.contains(it.first) else true) && it.first.intersects( + x, + y + ) + }?.first } fun prepare(offsetLeft: Float, offsetTop: Float, size: Float) { diff --git a/app/src/main/java/com/agronick/launcher/MainView.kt b/app/src/main/java/com/agronick/launcher/MainView.kt index cba98ad..1d200b4 100644 --- a/app/src/main/java/com/agronick/launcher/MainView.kt +++ b/app/src/main/java/com/agronick/launcher/MainView.kt @@ -32,6 +32,11 @@ class MainView(context: Context, appList: List) : View(context) { } var holdTimer = Timer() + var resetHold: () -> Unit = { + reorderer = null + holdTimer.cancel() + holdTimer = Timer() + } var reorderer: Reorderer? = null override fun onTouchEvent(event: MotionEvent?): Boolean { @@ -43,10 +48,12 @@ class MainView(context: Context, appList: List) : View(context) { hasMoved = false holdTimer.schedule(object : TimerTask() { override fun run() { - reorderer = Reorderer(container, ::prepareInvalidate) val offset = getRelativePosition(Pair(event.x, event.y)) - post { - reorderer!!.onHoldDown(offset.first, offset.second) + val app = container.getAppAtPoint(offset.first, offset.second) + if (app != null) { + post { + reorderer = Reorderer(container, app, ::prepareInvalidate) + } } } }, 3000) @@ -54,6 +61,7 @@ class MainView(context: Context, appList: List) : View(context) { } MotionEvent.ACTION_MOVE -> { hasMoved = true + holdTimer.cancel() if (reorderer == null) { offsetLeft += event.x - previousX offsetTop += event.y - previousY @@ -67,8 +75,11 @@ class MainView(context: Context, appList: List) : View(context) { return true } MotionEvent.ACTION_UP -> { - holdTimer.cancel() - holdTimer = Timer() + if (reorderer != null) { + reorderer!!.onStopReorder() + invalidate() + } + resetHold() if (!hasMoved) { handleClick(event.x, event.y) } else { @@ -82,7 +93,7 @@ class MainView(context: Context, appList: List) : View(context) { fun checkOverLimit() { val limited = container.getLimit(offsetLeft, offsetTop, canvasSize) - var animators = mutableListOf() + val animators = mutableListOf() if (offsetLeft != limited.first) { animators.add(ValueAnimator.ofFloat(offsetLeft, limited.first) .apply { diff --git a/app/src/main/java/com/agronick/launcher/Reorderer.kt b/app/src/main/java/com/agronick/launcher/Reorderer.kt index 9d94341..252418f 100644 --- a/app/src/main/java/com/agronick/launcher/Reorderer.kt +++ b/app/src/main/java/com/agronick/launcher/Reorderer.kt @@ -5,41 +5,32 @@ import android.animation.ValueAnimator import android.graphics.Canvas import androidx.core.animation.doOnEnd -class Reorderer(private val container: Container, val invalidate: () -> Unit) { - private var activeAppCopy: App? = null - private var lastOverlap: App? = null - private var suppressedAppCopy: App? = null - private var lastPosition = HashMap>() - private var lastFreeSpace = Pair(0f, 0f) +class Reorderer(private val container: Container, app: App, val invalidate: () -> Unit) { + private var activeAppCopy: App = app.copy() + private var lastOverlap: App? = app + private var suppressedAppCopy: App = app + private var lastPosition = HashSet() + private var lastFreeSpace = Pair(app.left, app.top) - fun onHoldDown(x: Float, y: Float): Boolean { - val app = container.getAppAtPoint(x, y) - if (app != null) { - activeAppCopy = app.copy() - suppressedAppCopy = app - lastOverlap = suppressedAppCopy - suppressedAppCopy!!.hidden = true - lastPosition[app] = Pair(app.left, app.top) - lastFreeSpace = Pair(app.left, app.top) - ValueAnimator.ofInt(activeAppCopy!!.size, (activeAppCopy!!.size * 1.4).toInt()) - .apply { - duration = StaticValues.durationRise - addUpdateListener { animator -> - activeAppCopy!!.size = animator.animatedValue as Int - invalidate() - } - }.start() - return true - } - return false + init { + lastPosition.add(app) + suppressedAppCopy.hidden = true + ValueAnimator.ofInt(activeAppCopy.size, (activeAppCopy.size * 1.4).toInt()) + .apply { + duration = StaticValues.durationRise + addUpdateListener { animator -> + activeAppCopy.size = animator.animatedValue as Int + invalidate() + } + }.start() } fun onMove(x: Float, y: Float) { - activeAppCopy!!.left = x - activeAppCopy!!.top = y - val app = container.getAppAtPoint(x, y) - if (app !== null && activeAppCopy !== null && app != activeAppCopy && app != lastOverlap) { - lastPosition[app] = Pair(app.left, app.top) + activeAppCopy.left = x + activeAppCopy.top = y + val app = container.getAppAtPoint(x, y, lastPosition) + if (app !== null) { + lastPosition.add(app) val positions = lastFreeSpace lastFreeSpace = Pair(app.left, app.top) animateAppPosition(app, positions.first, positions.second) @@ -65,15 +56,19 @@ class Reorderer(private val container: Container, val invalidate: () -> Unit) { duration = StaticValues.durationOpen playTogether(xAnim, yAnim) doOnEnd { - lastOverlap = null + lastPosition.remove(app) } start() } } + fun onStopReorder() { + suppressedAppCopy.left = lastFreeSpace.first + suppressedAppCopy.top = lastFreeSpace.second + suppressedAppCopy.hidden = false + } + fun draw(canvas: Canvas) { - if (activeAppCopy !== null) { - activeAppCopy!!.drawNormal(canvas) - } + activeAppCopy.drawNormal(canvas) } } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f736d1b..1601d34 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Feb 09 20:16:16 EST 2021 +#Sun Feb 14 15:30:22 EST 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip From 2194d0f2b50f28b0bb5a32f1832a1eb6343664d8 Mon Sep 17 00:00:00 2001 From: Kyle Agronick Date: Sun, 7 Mar 2021 15:58:47 -0500 Subject: [PATCH 3/9] Got calculated positioning working --- .../main/java/com/agronick/launcher/App.kt | 55 ++++++++++++------- app/src/main/java/util/geometry/Vector2.java | 16 ++++++ 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/agronick/launcher/App.kt b/app/src/main/java/com/agronick/launcher/App.kt index a162a50..1c86f8c 100644 --- a/app/src/main/java/com/agronick/launcher/App.kt +++ b/app/src/main/java/com/agronick/launcher/App.kt @@ -6,6 +6,7 @@ import util.geometry.Circle import util.geometry.CircleCircleIntersection import util.geometry.Vector2 + class App(val pkgInfo: PInfo, var size: Int) { var left = 0.0f var top = 0.0f @@ -20,15 +21,15 @@ class App(val pkgInfo: PInfo, var size: Int) { } fun drawNormal(canvas: Canvas) { - if (!hidden) { + if (!hidden && lastCircle != null) { val radius = if (lastCircle !== null) (lastCircle!!.r).coerceAtLeast(10f) else size.toFloat() - draw(canvas, radius, left, top) + draw(canvas, radius, lastCircle!!.c.x, lastCircle!!.c.y) } } fun prepare(faceCircle: Circle) { - lastCircle = getCircle(faceCircle) ?: return + lastCircle = getCircle(faceCircle) } fun draw(canvas: Canvas, radius: Float, x: Float, y: Float) { @@ -52,25 +53,41 @@ class App(val pkgInfo: PInfo, var size: Int) { val intersects = CircleCircleIntersection(faceCircle, appCircle) if (!intersects.type.isContained) { - if (intersects.type.intersectionPointCount == 0) return null - for (i in startSize.toLong() downTo 1) { - val testCircle = Circle( - Vector2( - left, - top - ), i.toFloat() - ) - val intersector = CircleCircleIntersection( - faceCircle, - testCircle + val arcMidpoint = getArcMidpoint(intersects) ?: return null + var opposite = getPointClosestToCenter(faceCircle, appCircle) + return Circle( + arcMidpoint.midpoint(opposite), + arcMidpoint.distance(opposite) * 0.5f + ) + } + return appCircle + } + + private fun getArcMidpoint(intersection: CircleCircleIntersection): Vector2? { + return when (intersection.type.intersectionPointCount) { + 1 -> intersection.intersectionPoint1 + 2 -> { + val AB = intersection.intersectionPoint2.sub(intersection.intersectionPoint1) + val lAB = kotlin.math.sqrt(AB.x * AB.x + AB.y * AB.y) + val uAB = Vector2(AB.x / lAB, AB.y / lAB) + val mAB = intersection.intersectionPoint1.add(intersection.intersectionPoint2).div( + 2f ) - if (intersector.type.intersectionPointCount == 0) { - return testCircle - } + val R = intersection.c1.r + val F = R - kotlin.math.sqrt(R * R - lAB * lAB / 4) + Vector2(mAB.x - uAB.y * F, mAB.y + uAB.x * F) } - return null + else -> null } - return appCircle + } + + private fun getPointClosestToCenter(faceCircle: Circle, appCircle: Circle): Vector2 { + val vX = faceCircle.c.x - appCircle.c.x + val vY = faceCircle.c.y - appCircle.c.y + val magV: Double = kotlin.math.sqrt((vX * vX + vY * vY).toDouble()) + val aX = appCircle.c.x + vX / magV * appCircle.r + val aY = appCircle.c.y + vY / magV * appCircle.r + return Vector2(aX.toFloat(), aY.toFloat()) } fun intersects(x: Float, y: Float): Boolean { diff --git a/app/src/main/java/util/geometry/Vector2.java b/app/src/main/java/util/geometry/Vector2.java index dd208a4..bb07fef 100644 --- a/app/src/main/java/util/geometry/Vector2.java +++ b/app/src/main/java/util/geometry/Vector2.java @@ -63,6 +63,22 @@ public float angle() { return (float) atan2(y, x); } + public Vector2 div(float a) { + return new Vector2(x / a, y / a); + } + + public Vector2 div(Vector2 a) { + return new Vector2(x / a.x, y / a.y); + } + + public Vector2 midpoint(Vector2 a) { + return new Vector2(this.x + a.x, this.y + a.y).div(2f); + } + + public float distance(Vector2 a) { + return (float) Math.sqrt(Math.pow((a.x - this.x), 2) + Math.pow((a.y - this.y), 2)); + } + @Override public int hashCode() { final int prime = 31; From e1c1ac6ca39868e3d9819ead4625dbc9a3c171e8 Mon Sep 17 00:00:00 2001 From: Kyle Agronick Date: Sun, 14 Mar 2021 16:08:27 -0400 Subject: [PATCH 4/9] Circle fill sort of working --- .../main/java/com/agronick/launcher/App.kt | 5 +- .../java/com/agronick/launcher/Container.kt | 66 +++++++++++++------ 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/agronick/launcher/App.kt b/app/src/main/java/com/agronick/launcher/App.kt index 1c86f8c..763046d 100644 --- a/app/src/main/java/com/agronick/launcher/App.kt +++ b/app/src/main/java/com/agronick/launcher/App.kt @@ -52,9 +52,12 @@ class App(val pkgInfo: PInfo, var size: Int) { ) val intersects = CircleCircleIntersection(faceCircle, appCircle) + if (intersects.type == CircleCircleIntersection.Type.SEPARATE) { + return null + } if (!intersects.type.isContained) { val arcMidpoint = getArcMidpoint(intersects) ?: return null - var opposite = getPointClosestToCenter(faceCircle, appCircle) + val opposite = getPointClosestToCenter(faceCircle, appCircle) return Circle( arcMidpoint.midpoint(opposite), arcMidpoint.distance(opposite) * 0.5f diff --git a/app/src/main/java/com/agronick/launcher/Container.kt b/app/src/main/java/com/agronick/launcher/Container.kt index 369c8c4..8ec55fb 100644 --- a/app/src/main/java/com/agronick/launcher/Container.kt +++ b/app/src/main/java/com/agronick/launcher/Container.kt @@ -1,14 +1,17 @@ package com.agronick.launcher import android.graphics.Canvas +import android.util.Log import util.geometry.Circle import util.geometry.Vector2 +import java.util.* import kotlin.math.ceil +import kotlin.math.floor import kotlin.math.roundToInt import kotlin.math.sqrt class Container(appList: List, density: Float) { - private val rows: List> + private val rows: List>> val size = (StaticValues.normalAppSize * density).roundToInt() val margin = (StaticValues.margin * density).roundToInt() private val iterate: Sequence> @@ -21,33 +24,55 @@ class Container(appList: List, density: Float) { private var equalizerOffset = 1.1f + companion object { + private const val tag = "Container" + } + init { - val squareSize = Math.ceil(sqrt(appList.size.toFloat()).toDouble()).toInt() val appIter = appList.iterator() - rows = 1.rangeTo(squareSize).mapNotNull outer@{ - val cols = 1.rangeTo(squareSize).mapNotNull inner@{ - if (appIter.hasNext()) { - return@inner App( - appIter.next(), - size - ) + // Area of a circle to radius + var appRadius = sqrt(appList.size.toFloat() / Math.PI) + val appDiam = appRadius * 2 + val appRadiusSquared = appRadius * appRadius + rows = 0.rangeTo(floor(appRadius).toInt()).mapNotNull outer@{ + // Pythagorean theorem - row length at each level + var rowDiam = + ((if (it == 0) appDiam else (sqrt(appRadiusSquared - (it * it)) * 2)) - 1).roundToInt() + val rowGroup = 0.rangeTo(if (it == 0) 0 else 1).mapNotNull middle@{ + val cols = 0.rangeTo(rowDiam.toInt()).mapNotNull inner@{ + if (appIter.hasNext()) { + return@inner App( + appIter.next(), + size + ) + } + return@inner null } - return@inner null + return@middle if (cols.isNotEmpty()) cols else null } - return@outer if (cols.isNotEmpty()) cols else null + return@outer if (rowGroup.isNotEmpty()) rowGroup else null } + iterate = sequence { - val iterator = this@Container.rows.iterator() + val iterator = rows.iterator() var rowCount = 0 while (iterator.hasNext()) { - val row = iterator.next() - val colIterator = row.iterator() + val rows = iterator.next() + val positive = rows[0].iterator() + val negative = if (rows.size > 1) rows[1].iterator() else null + var colCount = 0 - while (colIterator.hasNext()) { - val app = colIterator.next() + while (positive.hasNext()) { + val app = positive.next() yield(Triple(app, rowCount, colCount)) colCount++ } + colCount = 0 + while (negative != null && negative.hasNext()) { + val app = negative.next() + yield(Triple(app, rowCount * -1, colCount)) + colCount++ + } rowCount += 1 } } @@ -88,17 +113,20 @@ class Container(appList: List, density: Float) { private fun calcPositions(row: Int, col: Int): Pair { var left = calcPosition(col) * equalizerOffset - if (ceil(row * 0.5) % 2 == 1.0) { + if (kotlin.math.abs(row) % 2 == 1) { + // Add offset for haxagon shape left -= size + margin } - val top = calcPosition(row) + val top = (row * (size * 2) + row * margin).toFloat() + Log.d(tag, "${left} ${top}") return Pair(left, top) } private fun calcPosition(num: Int): Float { var pos = ceil(num * 0.5f) - pos = (pos * (size * 2) + pos * margin) + pos = pos * (size * 2) + pos * margin if (num % 2 == 0) { + // Position right, the left of center pos *= -1 } return pos From af64c6f3d55920c330a2f9ea66183c9570732516 Mon Sep 17 00:00:00 2001 From: Kyle Agronick Date: Sun, 14 Mar 2021 22:06:59 -0400 Subject: [PATCH 5/9] Edge move --- .../main/java/com/agronick/launcher/App.kt | 27 +++---- .../java/com/agronick/launcher/Container.kt | 21 ++++-- .../com/agronick/launcher/MainActivity.kt | 20 +++-- .../java/com/agronick/launcher/MainView.kt | 75 ++++++++++++++----- .../java/com/agronick/launcher/Reorderer.kt | 29 ++++++- app/src/main/java/util/geometry/Vector2.java | 10 +++ 6 files changed, 131 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/agronick/launcher/App.kt b/app/src/main/java/com/agronick/launcher/App.kt index 763046d..66f31fe 100644 --- a/app/src/main/java/com/agronick/launcher/App.kt +++ b/app/src/main/java/com/agronick/launcher/App.kt @@ -28,11 +28,11 @@ class App(val pkgInfo: PInfo, var size: Int) { } } - fun prepare(faceCircle: Circle) { - lastCircle = getCircle(faceCircle) + fun prepare(faceCircle: Circle, checkCollisions: Boolean = true) { + lastCircle = getCircle(faceCircle, checkCollisions) } - fun draw(canvas: Canvas, radius: Float, x: Float, y: Float) { + private fun draw(canvas: Canvas, radius: Float, x: Float, y: Float) { pkgInfo.icon.bounds = Rect( (x - radius).toInt(), (y - radius).toInt(), @@ -42,7 +42,7 @@ class App(val pkgInfo: PInfo, var size: Int) { pkgInfo.icon.draw(canvas) } - fun getCircle(faceCircle: Circle): Circle? { + private fun getCircle(faceCircle: Circle, checkCollisions: Boolean): Circle? { val startSize = size val appCircle = Circle( Vector2( @@ -50,6 +50,9 @@ class App(val pkgInfo: PInfo, var size: Int) { top ), startSize.toFloat() ) + if (!checkCollisions) { + return appCircle + } val intersects = CircleCircleIntersection(faceCircle, appCircle) if (intersects.type == CircleCircleIntersection.Type.SEPARATE) { @@ -77,7 +80,7 @@ class App(val pkgInfo: PInfo, var size: Int) { 2f ) val R = intersection.c1.r - val F = R - kotlin.math.sqrt(R * R - lAB * lAB / 4) + val F = R - kotlin.math.sqrt(R * R - lAB * lAB * 0.25f) Vector2(mAB.x - uAB.y * F, mAB.y + uAB.x * F) } else -> null @@ -87,24 +90,18 @@ class App(val pkgInfo: PInfo, var size: Int) { private fun getPointClosestToCenter(faceCircle: Circle, appCircle: Circle): Vector2 { val vX = faceCircle.c.x - appCircle.c.x val vY = faceCircle.c.y - appCircle.c.y - val magV: Double = kotlin.math.sqrt((vX * vX + vY * vY).toDouble()) + val magV = kotlin.math.sqrt((vX * vX + vY * vY).toDouble()).toFloat() val aX = appCircle.c.x + vX / magV * appCircle.r val aY = appCircle.c.y + vY / magV * appCircle.r - return Vector2(aX.toFloat(), aY.toFloat()) + return Vector2(aX, aY) } - fun intersects(x: Float, y: Float): Boolean { + fun intersects(point: Vector2): Boolean { val circle = lastCircle if (circle != null) { - val point = Circle( - Vector2( - x, - y - ), 3.0f - ) return CircleCircleIntersection( circle, - point + Circle(point, 3.0f) ).type == CircleCircleIntersection.Type.ECCENTRIC_CONTAINED } return false diff --git a/app/src/main/java/com/agronick/launcher/Container.kt b/app/src/main/java/com/agronick/launcher/Container.kt index 8ec55fb..c37d9b4 100644 --- a/app/src/main/java/com/agronick/launcher/Container.kt +++ b/app/src/main/java/com/agronick/launcher/Container.kt @@ -29,17 +29,25 @@ class Container(appList: List, density: Float) { } init { + /* + Tries to set up a rough circle shape + Rows are stored using the outer array index as y pos and inner array as x pos + The middle array holds up to 2 items, one to be drawn at positive y and one at negative y + rows = [ + [[row at y], [row at y index * -1]] + ] + */ val appIter = appList.iterator() // Area of a circle to radius - var appRadius = sqrt(appList.size.toFloat() / Math.PI) + val appRadius = sqrt(appList.size.toFloat() / Math.PI) val appDiam = appRadius * 2 val appRadiusSquared = appRadius * appRadius rows = 0.rangeTo(floor(appRadius).toInt()).mapNotNull outer@{ // Pythagorean theorem - row length at each level - var rowDiam = - ((if (it == 0) appDiam else (sqrt(appRadiusSquared - (it * it)) * 2)) - 1).roundToInt() + val rowDiam = + ((if (it == 0) appDiam else (sqrt(appRadiusSquared - (it * it)) * 2) - 1)).roundToInt() val rowGroup = 0.rangeTo(if (it == 0) 0 else 1).mapNotNull middle@{ - val cols = 0.rangeTo(rowDiam.toInt()).mapNotNull inner@{ + val cols = 0.rangeTo(rowDiam).mapNotNull inner@{ if (appIter.hasNext()) { return@inner App( appIter.next(), @@ -132,11 +140,10 @@ class Container(appList: List, density: Float) { return pos } - fun getAppAtPoint(x: Float, y: Float, toIgnore: HashSet? = null): App? { + fun getAppAtPoint(point: Vector2, toIgnore: HashSet? = null): App? { return iterate.find { (if (toIgnore != null) !toIgnore.contains(it.first) else true) && it.first.intersects( - x, - y + point ) }?.first } diff --git a/app/src/main/java/com/agronick/launcher/MainActivity.kt b/app/src/main/java/com/agronick/launcher/MainActivity.kt index ccaa205..cbe4f45 100644 --- a/app/src/main/java/com/agronick/launcher/MainActivity.kt +++ b/app/src/main/java/com/agronick/launcher/MainActivity.kt @@ -33,11 +33,6 @@ class MainActivity : Activity() { mainView.onPackageClick = this::onPackageClick } - override fun onPause() { - super.onPause() - mainView.invalidate() - } - fun onPackageClick(pkg: PInfo) { val name = ComponentName(pkg.pname, pkg.activityName) val i = Intent(Intent.ACTION_MAIN) @@ -66,4 +61,19 @@ class MainActivity : Activity() { outState.putFloat("offsetTop", mainView.offsetTop) super.onSaveInstanceState(outState) } + + override fun onPause() { + super.onPause() + mainView.openingApp = null + mainView.allHidden = true + mainView.invalidate() + } + + override fun onResume() { + super.onResume() + if (mainView.allHidden) { + mainView.allHidden = false + mainView.invalidate() + } + } } diff --git a/app/src/main/java/com/agronick/launcher/MainView.kt b/app/src/main/java/com/agronick/launcher/MainView.kt index 1d200b4..c0f88d1 100644 --- a/app/src/main/java/com/agronick/launcher/MainView.kt +++ b/app/src/main/java/com/agronick/launcher/MainView.kt @@ -4,14 +4,15 @@ import android.animation.AnimatorSet import android.animation.ValueAnimator import android.content.Context import android.graphics.Canvas -import android.util.Log +import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.View import androidx.core.animation.doOnEnd +import util.geometry.Vector2 import java.util.* class MainView(context: Context, appList: List) : View(context) { - private var density: Float + private var density: Float = context.resources.displayMetrics.density var onPackageClick: ((PInfo) -> Unit)? = null private lateinit var container: Container @@ -21,22 +22,28 @@ class MainView(context: Context, appList: List) : View(context) { private var previousY: Float = 0f private var hasMoved = false private var canvasSize: Float = 0f + private var edgeLimit = 100000f + var allHidden = false - private var openingApp: App? = null + var openingApp: App? = null init { - density = context.resources.displayMetrics.density Runnable { container = Container(appList, density) }.run() } var holdTimer = Timer() - var resetHold: () -> Unit = { + var edgeTimer = Timer() + val resetHold: () -> Unit = { reorderer = null holdTimer.cancel() holdTimer = Timer() } + val resetEdge: () -> Unit = { + edgeTimer.cancel() + edgeTimer = Timer() + } var reorderer: Reorderer? = null override fun onTouchEvent(event: MotionEvent?): Boolean { @@ -49,19 +56,21 @@ class MainView(context: Context, appList: List) : View(context) { holdTimer.schedule(object : TimerTask() { override fun run() { val offset = getRelativePosition(Pair(event.x, event.y)) - val app = container.getAppAtPoint(offset.first, offset.second) + val app = container.getAppAtPoint(Vector2(offset.x, offset.y)) if (app != null) { post { reorderer = Reorderer(container, app, ::prepareInvalidate) + performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) } } } - }, 3000) + }, 2000) return true } MotionEvent.ACTION_MOVE -> { hasMoved = true holdTimer.cancel() + resetEdge() if (reorderer == null) { offsetLeft += event.x - previousX offsetTop += event.y - previousY @@ -69,7 +78,11 @@ class MainView(context: Context, appList: List) : View(context) { previousY = event.y } else { val offset = getRelativePosition(Pair(event.x, event.y)) - reorderer!!.onMove(offset.first, offset.second) + reorderer!!.onMove(offset) + val newOffsets = reorderer!!.checkAtEdge(offset, container.lastCircle) + if (newOffsets != null) { + moveAtEdge(newOffsets.scale(density * 3)) + } } prepareInvalidate() return true @@ -83,7 +96,7 @@ class MainView(context: Context, appList: List) : View(context) { if (!hasMoved) { handleClick(event.x, event.y) } else { - checkOverLimit() + checkOverPanLimit() } } } @@ -91,7 +104,29 @@ class MainView(context: Context, appList: List) : View(context) { return super.onTouchEvent(event) } - fun checkOverLimit() { + fun moveAtEdge(newOffsets: Vector2) { + edgeTimer.schedule(object : TimerTask() { + override fun run() { + offsetLeft += newOffsets.x + offsetTop += newOffsets.y + val curReorderer = reorderer + if (curReorderer !== null) { + val appPos = curReorderer.getAppPos() + post { + curReorderer.onMove( + Vector2( + appPos.x - newOffsets.x, + appPos.y - newOffsets.y + ) + ) + } + } + prepareInvalidate() + } + }, 0, 33) + } + + fun checkOverPanLimit() { val limited = container.getLimit(offsetLeft, offsetTop, canvasSize) val animators = mutableListOf() if (offsetLeft != limited.first) { @@ -124,7 +159,7 @@ class MainView(context: Context, appList: List) : View(context) { fun handleClick(x: Float, y: Float) { val offset = getRelativePosition(Pair(x, y)) - val app = container.getAppAtPoint(offset.first, offset.second) + val app = container.getAppAtPoint(offset) if (app != null) { setupOpenAnim(app.copy()) } @@ -151,8 +186,7 @@ class MainView(context: Context, appList: List) : View(context) { .apply { addUpdateListener { animator -> app.size = ((animator.animatedValue as Float).toInt()) - Log.d(TAG, "At size ${app.size}") - container.lastCircle?.let { app.prepare(it) } + container.lastCircle?.let { app.prepare(it, false) } invalidate() } } @@ -162,7 +196,6 @@ class MainView(context: Context, appList: List) : View(context) { playTogether(xAnimator, yAnimator, radiusAnimator) doOnEnd { postDelayed({ - openingApp = null onPackageClick?.let { it1 -> it1(app.pkgInfo) } }, 200) } @@ -171,16 +204,17 @@ class MainView(context: Context, appList: List) : View(context) { } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { - canvasSize = Math.min(w, h).toFloat() + canvasSize = w.coerceAtMost(h).toFloat() + edgeLimit = canvasSize * 0.9f container.prepare(offsetLeft, offsetTop, w.toFloat()) super.onSizeChanged(w, h, oldw, oldh) } - fun getRelativePosition(point: Pair? = null): Pair { + fun getRelativePosition(point: Pair? = null): Vector2 { val halfSize = canvasSize * 0.5f - val pos = Pair(halfSize + offsetLeft, halfSize + offsetTop) + val pos = Vector2(halfSize + offsetLeft, halfSize + offsetTop) if (point != null) { - return Pair(point.first - pos.first, point.second - pos.second) + return Vector2(point.first - pos.x, point.second - pos.y) } return pos } @@ -189,15 +223,16 @@ class MainView(context: Context, appList: List) : View(context) { if (canvasSize == 0f) return Runnable { container.prepare(offsetLeft, offsetTop, canvasSize) + reorderer?.prepare() invalidate() }.run() } override fun onDraw(canvas: Canvas?) { - if (canvas == null) return + if (canvas == null || allHidden) return Runnable { val offset = getRelativePosition() - canvas.translate(offset.first, offset.second) + canvas.translate(offset.x, offset.y) container.draw(canvas) openingApp?.drawNormal(canvas) reorderer?.draw(canvas) diff --git a/app/src/main/java/com/agronick/launcher/Reorderer.kt b/app/src/main/java/com/agronick/launcher/Reorderer.kt index 252418f..402d628 100644 --- a/app/src/main/java/com/agronick/launcher/Reorderer.kt +++ b/app/src/main/java/com/agronick/launcher/Reorderer.kt @@ -4,6 +4,8 @@ import android.animation.AnimatorSet import android.animation.ValueAnimator import android.graphics.Canvas import androidx.core.animation.doOnEnd +import util.geometry.Circle +import util.geometry.Vector2 class Reorderer(private val container: Container, app: App, val invalidate: () -> Unit) { private var activeAppCopy: App = app.copy() @@ -25,10 +27,10 @@ class Reorderer(private val container: Container, app: App, val invalidate: () - }.start() } - fun onMove(x: Float, y: Float) { - activeAppCopy.left = x - activeAppCopy.top = y - val app = container.getAppAtPoint(x, y, lastPosition) + fun onMove(position: Vector2) { + activeAppCopy.left = position.x + activeAppCopy.top = position.y + val app = container.getAppAtPoint(position, lastPosition) if (app !== null) { lastPosition.add(app) val positions = lastFreeSpace @@ -38,6 +40,10 @@ class Reorderer(private val container: Container, app: App, val invalidate: () - } } + fun getAppPos(): Vector2 { + return Vector2(activeAppCopy.left, activeAppCopy.top) + } + fun animateAppPosition(app: App, x: Float, y: Float) { val xAnim = ValueAnimator.ofFloat(app.left, x).apply { duration = StaticValues.durationSwap @@ -66,9 +72,24 @@ class Reorderer(private val container: Container, app: App, val invalidate: () - suppressedAppCopy.left = lastFreeSpace.first suppressedAppCopy.top = lastFreeSpace.second suppressedAppCopy.hidden = false + container.lastCircle?.let { suppressedAppCopy.prepare(it) } + } + + fun prepare() { + container.lastCircle?.let { activeAppCopy.prepare(it, false) } } fun draw(canvas: Canvas) { activeAppCopy.drawNormal(canvas) } + + fun checkAtEdge(offsetVector: Vector2, lastCircle: Circle?): Vector2? { + if (lastCircle == null) return null + val maxDistance = lastCircle.r * 0.9 + if (offsetVector.distance(lastCircle.c) >= maxDistance) { + val angle = Math.toRadians(lastCircle.c.angleBetween(offsetVector)).toFloat() + return Vector2(kotlin.math.sin(angle) * -1, kotlin.math.cos(angle)) + } + return null + } } \ No newline at end of file diff --git a/app/src/main/java/util/geometry/Vector2.java b/app/src/main/java/util/geometry/Vector2.java index bb07fef..7b72319 100644 --- a/app/src/main/java/util/geometry/Vector2.java +++ b/app/src/main/java/util/geometry/Vector2.java @@ -79,6 +79,16 @@ public float distance(Vector2 a) { return (float) Math.sqrt(Math.pow((a.x - this.x), 2) + Math.pow((a.y - this.y), 2)); } + /** + * Returns the angle between the given two points + */ + public double angleBetween(Vector2 a) { + Vector2 diff = this.sub(a); + double angle = Math.atan2(diff.y, diff.x); + double degrees = 180 * angle / Math.PI; + return ((360 + Math.round(degrees)) - 90) % 360; + } + @Override public int hashCode() { final int prime = 31; From 63c1b5f99ee38211b871577636aea26291670df4 Mon Sep 17 00:00:00 2001 From: Kyle Agronick Date: Sun, 14 Aug 2022 19:59:20 -0400 Subject: [PATCH 6/9] Major refactor --- app/src/main/AndroidManifest.xml | 3 +- .../main/java/com/agronick/launcher/App.kt | 5 +- .../java/com/agronick/launcher/Container.kt | 14 +-- .../java/com/agronick/launcher/Launcher.kt | 16 +++ .../com/agronick/launcher/MainActivity.kt | 91 ++++++++++++--- .../java/com/agronick/launcher/MainView.kt | 107 +++++------------- .../main/java/com/agronick/launcher/PInfo.kt | 8 +- .../agronick/launcher/PreferenceManager.kt | 65 +++++++++++ .../java/com/agronick/launcher/Reorderer.kt | 64 +++++------ app/src/main/res/values/strings.xml | 2 +- 10 files changed, 236 insertions(+), 139 deletions(-) create mode 100644 app/src/main/java/com/agronick/launcher/Launcher.kt create mode 100644 app/src/main/java/com/agronick/launcher/PreferenceManager.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0fcd545..3895a8d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,7 +9,8 @@ , density: Float) { private val rows: List>> - val size = (StaticValues.normalAppSize * density).roundToInt() - val margin = (StaticValues.margin * density).roundToInt() + val appCircleSize = (StaticValues.normalAppSize * density).roundToInt() + val appCircleMargin = (StaticValues.margin * density).roundToInt() private val iterate: Sequence> var lastCircle: Circle? = null @@ -40,7 +40,7 @@ class Container(appList: List, density: Float) { val appIter = appList.iterator() // Area of a circle to radius val appRadius = sqrt(appList.size.toFloat() / Math.PI) - val appDiam = appRadius * 2 + val appDiam = PreferenceManager.getSetDiam(appRadius * 2) val appRadiusSquared = appRadius * appRadius rows = 0.rangeTo(floor(appRadius).toInt()).mapNotNull outer@{ // Pythagorean theorem - row length at each level @@ -51,7 +51,7 @@ class Container(appList: List, density: Float) { if (appIter.hasNext()) { return@inner App( appIter.next(), - size + appCircleSize ) } return@inner null @@ -123,16 +123,16 @@ class Container(appList: List, density: Float) { var left = calcPosition(col) * equalizerOffset if (kotlin.math.abs(row) % 2 == 1) { // Add offset for haxagon shape - left -= size + margin + left -= appCircleSize + appCircleMargin } - val top = (row * (size * 2) + row * margin).toFloat() + val top = (row * (appCircleSize * 2) + row * appCircleMargin).toFloat() Log.d(tag, "${left} ${top}") return Pair(left, top) } private fun calcPosition(num: Int): Float { var pos = ceil(num * 0.5f) - pos = pos * (size * 2) + pos * margin + pos = pos * (appCircleSize * 2) + pos * appCircleMargin if (num % 2 == 0) { // Position right, the left of center pos *= -1 diff --git a/app/src/main/java/com/agronick/launcher/Launcher.kt b/app/src/main/java/com/agronick/launcher/Launcher.kt new file mode 100644 index 0000000..5a9209f --- /dev/null +++ b/app/src/main/java/com/agronick/launcher/Launcher.kt @@ -0,0 +1,16 @@ +package com.agronick.launcher + +import android.app.Application +import android.content.Context + +class Launcher : Application() { + + override fun onCreate() { + super.onCreate() + Launcher.appContext = applicationContext + } + + companion object { + lateinit var appContext: Context + } +} \ No newline at end of file diff --git a/app/src/main/java/com/agronick/launcher/MainActivity.kt b/app/src/main/java/com/agronick/launcher/MainActivity.kt index cbe4f45..a1aa3f9 100644 --- a/app/src/main/java/com/agronick/launcher/MainActivity.kt +++ b/app/src/main/java/com/agronick/launcher/MainActivity.kt @@ -5,12 +5,16 @@ import android.content.ComponentName import android.content.Intent import android.os.Bundle import android.transition.Fade +import android.view.GestureDetector +import android.view.MotionEvent import android.view.Window +import androidx.core.view.GestureDetectorCompat var TAG = "main" -class MainActivity : Activity() { +class MainActivity : Activity(), GestureDetector.OnGestureListener { + private lateinit var mDetector: GestureDetectorCompat private lateinit var mainView: MainView override fun onCreate(savedInstanceState: Bundle?) { @@ -31,29 +35,39 @@ class MainActivity : Activity() { setContentView(mainView) mainView.onPackageClick = this::onPackageClick + + mDetector = GestureDetectorCompat(this, this) + mDetector.setIsLongpressEnabled(true) + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + return if (mDetector.onTouchEvent(event)) { + true + } else if (mainView.reorderer != null) { + mainView.handleLongPress(event) + return true + } else { + super.onTouchEvent(event) + } } + fun onPackageClick(pkg: PInfo) { - val name = ComponentName(pkg.pname, pkg.activityName) - val i = Intent(Intent.ACTION_MAIN) - i.addCategory(Intent.CATEGORY_LAUNCHER) - i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or - Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED - i.component = name - startActivity(i) + if (pkg.pname != null && pkg.activityName != null) { + val name = ComponentName(pkg.pname!!, pkg.activityName!!) + val i = Intent(Intent.ACTION_MAIN) + i.addCategory(Intent.CATEGORY_LAUNCHER) + i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + i.component = name + startActivity(i) + } } private fun getInstalledApps(): List { val mainIntent = Intent(Intent.ACTION_MAIN, null) mainIntent.addCategory(Intent.CATEGORY_LAUNCHER) - return packageManager.queryIntentActivities(mainIntent, 0).mapNotNull { - return@mapNotNull PInfo( - appname = it.activityInfo.packageName, - pname = it.activityInfo.packageName, - icon = it.activityInfo.loadIcon(packageManager), - activityName = it.activityInfo.name - ) - } + return PreferenceManager.getAppList(packageManager, mainIntent) } override fun onSaveInstanceState(outState: Bundle) { @@ -76,4 +90,49 @@ class MainActivity : Activity() { mainView.invalidate() } } + + override fun onDown(e: MotionEvent?): Boolean { + return false + } + + override fun onShowPress(e: MotionEvent?) { + + } + + override fun onSingleTapUp(e: MotionEvent?): Boolean { + if (e != null) { + mainView.handleClick(e.x, e.y) + } + return true + } + + override fun onScroll( + e1: MotionEvent?, + e2: MotionEvent?, + distanceX: Float, + distanceY: Float + ): Boolean { + mainView.offsetLeft -= distanceX + mainView.offsetTop -= distanceY + mainView.prepareInvalidate() + if (e2 != null && e2.action == MotionEvent.ACTION_UP) { + mainView.checkOverPanLimit() + } + return true + } + + override fun onLongPress(e: MotionEvent?) { + if (e != null) { + mainView.handleLongPress(e) + } + } + + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent?, + velocityX: Float, + velocityY: Float + ): Boolean { + return false + } } diff --git a/app/src/main/java/com/agronick/launcher/MainView.kt b/app/src/main/java/com/agronick/launcher/MainView.kt index c0f88d1..dcd833f 100644 --- a/app/src/main/java/com/agronick/launcher/MainView.kt +++ b/app/src/main/java/com/agronick/launcher/MainView.kt @@ -18,9 +18,6 @@ class MainView(context: Context, appList: List) : View(context) { private lateinit var container: Container var offsetLeft = 0f var offsetTop = 0f - private var previousX: Float = 0f - private var previousY: Float = 0f - private var hasMoved = false private var canvasSize: Float = 0f private var edgeLimit = 100000f var allHidden = false @@ -33,99 +30,52 @@ class MainView(context: Context, appList: List) : View(context) { }.run() } - var holdTimer = Timer() var edgeTimer = Timer() - val resetHold: () -> Unit = { - reorderer = null - holdTimer.cancel() - holdTimer = Timer() - } val resetEdge: () -> Unit = { edgeTimer.cancel() edgeTimer = Timer() } var reorderer: Reorderer? = null + fun handleLongPress(event: MotionEvent) { + val offset = getRelativePosition(Pair(event.x, event.y)) + if (reorderer != null) { + if (event.action == MotionEvent.ACTION_UP) { + reorderer!!.onStopReorder(container.getAppAtPoint(Vector2(offset.x, offset.y))) + reorderer = null + } else { + reorderer!!.onMove(offset) + val newOffsets = + reorderer!!.checkAtEdge(offset, container.lastCircle, container.appCircleSize) + if (newOffsets != null) { + offsetLeft += newOffsets.x + offsetTop += newOffsets.y + } + prepareInvalidate() + } + } else { + val app = container.getAppAtPoint(Vector2(offset.x, offset.y)) + if (app != null) { + post { + reorderer = Reorderer(container, app, ::prepareInvalidate) + performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) + } + } + } + } + override fun onTouchEvent(event: MotionEvent?): Boolean { if (event != null) { when (event.action) { - MotionEvent.ACTION_DOWN -> { - previousX = event.x - previousY = event.y - hasMoved = false - holdTimer.schedule(object : TimerTask() { - override fun run() { - val offset = getRelativePosition(Pair(event.x, event.y)) - val app = container.getAppAtPoint(Vector2(offset.x, offset.y)) - if (app != null) { - post { - reorderer = Reorderer(container, app, ::prepareInvalidate) - performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) - } - } - } - }, 2000) - return true - } MotionEvent.ACTION_MOVE -> { - hasMoved = true - holdTimer.cancel() resetEdge() - if (reorderer == null) { - offsetLeft += event.x - previousX - offsetTop += event.y - previousY - previousX = event.x - previousY = event.y - } else { - val offset = getRelativePosition(Pair(event.x, event.y)) - reorderer!!.onMove(offset) - val newOffsets = reorderer!!.checkAtEdge(offset, container.lastCircle) - if (newOffsets != null) { - moveAtEdge(newOffsets.scale(density * 3)) - } - } - prepareInvalidate() return true } - MotionEvent.ACTION_UP -> { - if (reorderer != null) { - reorderer!!.onStopReorder() - invalidate() - } - resetHold() - if (!hasMoved) { - handleClick(event.x, event.y) - } else { - checkOverPanLimit() - } - } } } return super.onTouchEvent(event) } - fun moveAtEdge(newOffsets: Vector2) { - edgeTimer.schedule(object : TimerTask() { - override fun run() { - offsetLeft += newOffsets.x - offsetTop += newOffsets.y - val curReorderer = reorderer - if (curReorderer !== null) { - val appPos = curReorderer.getAppPos() - post { - curReorderer.onMove( - Vector2( - appPos.x - newOffsets.x, - appPos.y - newOffsets.y - ) - ) - } - } - prepareInvalidate() - } - }, 0, 33) - } - fun checkOverPanLimit() { val limited = container.getLimit(offsetLeft, offsetTop, canvasSize) val animators = mutableListOf() @@ -160,7 +110,7 @@ class MainView(context: Context, appList: List) : View(context) { fun handleClick(x: Float, y: Float) { val offset = getRelativePosition(Pair(x, y)) val app = container.getAppAtPoint(offset) - if (app != null) { + if (app != null && app.pkgInfo.activityName != null) { setupOpenAnim(app.copy()) } } @@ -235,7 +185,6 @@ class MainView(context: Context, appList: List) : View(context) { canvas.translate(offset.x, offset.y) container.draw(canvas) openingApp?.drawNormal(canvas) - reorderer?.draw(canvas) }.run() } } \ No newline at end of file diff --git a/app/src/main/java/com/agronick/launcher/PInfo.kt b/app/src/main/java/com/agronick/launcher/PInfo.kt index 76f55fc..8ab3c27 100644 --- a/app/src/main/java/com/agronick/launcher/PInfo.kt +++ b/app/src/main/java/com/agronick/launcher/PInfo.kt @@ -2,4 +2,10 @@ package com.agronick.launcher import android.graphics.drawable.Drawable -class PInfo(var appname: String, var pname: String, var icon: Drawable, var activityName: String) \ No newline at end of file +class PInfo( + var appname: String?, + var pname: String?, + var icon: Drawable?, + var activityName: String?, + var order: Int? +) \ No newline at end of file diff --git a/app/src/main/java/com/agronick/launcher/PreferenceManager.kt b/app/src/main/java/com/agronick/launcher/PreferenceManager.kt new file mode 100644 index 0000000..480ef04 --- /dev/null +++ b/app/src/main/java/com/agronick/launcher/PreferenceManager.kt @@ -0,0 +1,65 @@ +package com.agronick.launcher + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager + +object PreferenceManager { + private var preferences = + Launcher.appContext.getSharedPreferences("prefs", Context.MODE_PRIVATE) + private var editor = preferences.edit() + private var appOrder = preferences.getString("app_order", "")?.split(";")?.toTypedArray()!! + .toCollection(ArrayList()) + private var sep = ";" + + fun getSetDiam(value: Double): Double { + val cur = preferences.getFloat("max_diam", 0f) + if (value > cur) { + editor.putFloat("max_diam", value.toFloat()) + editor.apply() + return value + } + return cur.toDouble() + } + + fun getAppList(packageManager: PackageManager, intent: Intent): List { + val asMapp = packageManager.queryIntentActivities(intent, 0).map { + it.activityInfo.packageName to PInfo( + appname = it.activityInfo.packageName, + pname = it.activityInfo.packageName, + icon = it.activityInfo.loadIcon(packageManager), + activityName = it.activityInfo.name, + order = null + ) + }.toMap().toMutableMap() + val prefList = preferences.getString("app_order", null) ?: return asMapp.values.toList() + val blankPinfo = PInfo( + appname = null, + pname = null, + icon = null, + activityName = null, + order = null + ) + val matchedItems = + prefList.split(sep).toTypedArray().map { asMapp.remove(it) ?: blankPinfo } + .toMutableList() + matchedItems.addAll(asMapp.values) + matchedItems.mapIndexed { order, pInfo -> pInfo.order = order } + return matchedItems + } + + fun swap(pinfo1: PInfo, pinfo2: PInfo) { + // Swap in array + appOrder[pinfo1.order!!] = pinfo2.pname!! + appOrder[pinfo2.order!!] = pinfo1.pname!! + + // Swap on objects + val temp = pinfo1.order + pinfo1.order = pinfo2.order + pinfo2.order = temp + + // Create new list + editor.putString("app_order", appOrder.joinToString(separator = sep)) + editor.apply() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/agronick/launcher/Reorderer.kt b/app/src/main/java/com/agronick/launcher/Reorderer.kt index 402d628..baf9193 100644 --- a/app/src/main/java/com/agronick/launcher/Reorderer.kt +++ b/app/src/main/java/com/agronick/launcher/Reorderer.kt @@ -2,46 +2,39 @@ package com.agronick.launcher import android.animation.AnimatorSet import android.animation.ValueAnimator -import android.graphics.Canvas +import android.util.Log import androidx.core.animation.doOnEnd import util.geometry.Circle import util.geometry.Vector2 -class Reorderer(private val container: Container, app: App, val invalidate: () -> Unit) { - private var activeAppCopy: App = app.copy() - private var lastOverlap: App? = app - private var suppressedAppCopy: App = app +class Reorderer( + private val container: Container, + private var app: App, + val invalidate: () -> Unit +) { + private var suppressedAppCopy: App = app.copy() private var lastPosition = HashSet() - private var lastFreeSpace = Pair(app.left, app.top) init { lastPosition.add(app) suppressedAppCopy.hidden = true - ValueAnimator.ofInt(activeAppCopy.size, (activeAppCopy.size * 1.4).toInt()) + ValueAnimator.ofInt(app.size, (app.size * 1.4).toInt()) .apply { duration = StaticValues.durationRise addUpdateListener { animator -> - activeAppCopy.size = animator.animatedValue as Int + app.size = animator.animatedValue as Int invalidate() } }.start() } fun onMove(position: Vector2) { - activeAppCopy.left = position.x - activeAppCopy.top = position.y - val app = container.getAppAtPoint(position, lastPosition) - if (app !== null) { - lastPosition.add(app) - val positions = lastFreeSpace - lastFreeSpace = Pair(app.left, app.top) - animateAppPosition(app, positions.first, positions.second) - lastOverlap = app - } + app.left = position.x + app.top = position.y } fun getAppPos(): Vector2 { - return Vector2(activeAppCopy.left, activeAppCopy.top) + return Vector2(app.left, app.top) } fun animateAppPosition(app: App, x: Float, y: Float) { @@ -68,27 +61,34 @@ class Reorderer(private val container: Container, app: App, val invalidate: () - } } - fun onStopReorder() { - suppressedAppCopy.left = lastFreeSpace.first - suppressedAppCopy.top = lastFreeSpace.second - suppressedAppCopy.hidden = false - container.lastCircle?.let { suppressedAppCopy.prepare(it) } + fun onStopReorder(overApp: App?) { + if (overApp == null) { + animateAppPosition(app, suppressedAppCopy.left, suppressedAppCopy.top) + } else { + animateAppPosition(overApp, suppressedAppCopy.left, suppressedAppCopy.top) + animateAppPosition(app, overApp.left, overApp.top) + } + ValueAnimator.ofInt((suppressedAppCopy.size * 1.4).toInt(), suppressedAppCopy.size) + .apply { + duration = StaticValues.durationRise + addUpdateListener { animator -> + app.size = animator.animatedValue as Int + } + }.start() } fun prepare() { - container.lastCircle?.let { activeAppCopy.prepare(it, false) } + container.lastCircle?.let { app.prepare(it, false) } } - fun draw(canvas: Canvas) { - activeAppCopy.drawNormal(canvas) - } - fun checkAtEdge(offsetVector: Vector2, lastCircle: Circle?): Vector2? { + fun checkAtEdge(offsetVector: Vector2, lastCircle: Circle?, radius: Int): Vector2? { if (lastCircle == null) return null - val maxDistance = lastCircle.r * 0.9 - if (offsetVector.distance(lastCircle.c) >= maxDistance) { + val maxDistance = lastCircle.r + Log.d("offset", "${offsetVector.distance(lastCircle.c)} >= ${maxDistance}") + if (offsetVector.distance(lastCircle.c) >= maxDistance - radius) { val angle = Math.toRadians(lastCircle.c.angleBetween(offsetVector)).toFloat() - return Vector2(kotlin.math.sin(angle) * -1, kotlin.math.cos(angle)) + return Vector2(kotlin.math.sin(angle) * -2, kotlin.math.cos(angle) * 2) } return null } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 41d30d5..dbf2642 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,5 +4,5 @@ This string is used for square devices and overridden by hello_world in values-round/strings.xml for round devices. --> - Hello Square World! + Max rows \ No newline at end of file From a0e0a693435a9d221451e956edc0585898fa0257 Mon Sep 17 00:00:00 2001 From: Kyle Agronick Date: Mon, 15 Aug 2022 00:32:58 -0400 Subject: [PATCH 7/9] Moving seems to be working --- .../com/agronick/launcher/MainActivity.kt | 8 ++- .../java/com/agronick/launcher/MainView.kt | 64 +++++++++++++------ .../agronick/launcher/PreferenceManager.kt | 20 ++++-- .../java/com/agronick/launcher/Reorderer.kt | 9 ++- 4 files changed, 68 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/agronick/launcher/MainActivity.kt b/app/src/main/java/com/agronick/launcher/MainActivity.kt index a1aa3f9..7473a4a 100644 --- a/app/src/main/java/com/agronick/launcher/MainActivity.kt +++ b/app/src/main/java/com/agronick/launcher/MainActivity.kt @@ -14,6 +14,7 @@ import androidx.core.view.GestureDetectorCompat var TAG = "main" class MainActivity : Activity(), GestureDetector.OnGestureListener { + private var wasScrolling = false private lateinit var mDetector: GestureDetectorCompat private lateinit var mainView: MainView @@ -46,6 +47,9 @@ class MainActivity : Activity(), GestureDetector.OnGestureListener { } else if (mainView.reorderer != null) { mainView.handleLongPress(event) return true + } else if (wasScrolling && event.action == MotionEvent.ACTION_UP) { + mainView.checkOverPanLimit() + return true } else { super.onTouchEvent(event) } @@ -115,9 +119,7 @@ class MainActivity : Activity(), GestureDetector.OnGestureListener { mainView.offsetLeft -= distanceX mainView.offsetTop -= distanceY mainView.prepareInvalidate() - if (e2 != null && e2.action == MotionEvent.ACTION_UP) { - mainView.checkOverPanLimit() - } + wasScrolling = true return true } diff --git a/app/src/main/java/com/agronick/launcher/MainView.kt b/app/src/main/java/com/agronick/launcher/MainView.kt index dcd833f..ab5f647 100644 --- a/app/src/main/java/com/agronick/launcher/MainView.kt +++ b/app/src/main/java/com/agronick/launcher/MainView.kt @@ -12,6 +12,7 @@ import util.geometry.Vector2 import java.util.* class MainView(context: Context, appList: List) : View(context) { + private var edgeTimer: Timer? = null private var density: Float = context.resources.displayMetrics.density var onPackageClick: ((PInfo) -> Unit)? = null @@ -30,26 +31,61 @@ class MainView(context: Context, appList: List) : View(context) { }.run() } - var edgeTimer = Timer() - val resetEdge: () -> Unit = { - edgeTimer.cancel() + var reorderer: Reorderer? = null + + fun resetReorderEdgeTimer() { + edgeTimer?.cancel() + } + + fun reorderAtEdge(newOffsets: Vector2) { + resetReorderEdgeTimer() edgeTimer = Timer() + edgeTimer?.schedule(object : TimerTask() { + override fun run() { + offsetLeft += newOffsets.x + offsetTop += newOffsets.y + val curReorderer = reorderer + if (curReorderer !== null) { + val appPos = curReorderer.getAppPos() + post { + curReorderer.onMove( + Vector2( + appPos.x - newOffsets.x, + appPos.y - newOffsets.y + ) + ) + } + } + prepareInvalidate() + } + }, 0, 33) } - var reorderer: Reorderer? = null fun handleLongPress(event: MotionEvent) { val offset = getRelativePosition(Pair(event.x, event.y)) if (reorderer != null) { if (event.action == MotionEvent.ACTION_UP) { - reorderer!!.onStopReorder(container.getAppAtPoint(Vector2(offset.x, offset.y))) + reorderer!!.onStopReorder( + when (container.getLimit(offsetLeft, offsetTop, canvasSize)) { + Pair(offsetLeft, offsetTop) -> container.getAppAtPoint( + Vector2( + offset.x, + offset.y + ) + ) + else -> null + } + ) reorderer = null + resetReorderEdgeTimer() } else { reorderer!!.onMove(offset) val newOffsets = - reorderer!!.checkAtEdge(offset, container.lastCircle, container.appCircleSize) + reorderer!!.checkAtEdge(offset, container.lastCircle, density * 0.3f) if (newOffsets != null) { - offsetLeft += newOffsets.x - offsetTop += newOffsets.y + reorderAtEdge(newOffsets.scale(density * 3)) + } else { + resetReorderEdgeTimer() } prepareInvalidate() } @@ -64,18 +100,6 @@ class MainView(context: Context, appList: List) : View(context) { } } - override fun onTouchEvent(event: MotionEvent?): Boolean { - if (event != null) { - when (event.action) { - MotionEvent.ACTION_MOVE -> { - resetEdge() - return true - } - } - } - return super.onTouchEvent(event) - } - fun checkOverPanLimit() { val limited = container.getLimit(offsetLeft, offsetTop, canvasSize) val animators = mutableListOf() diff --git a/app/src/main/java/com/agronick/launcher/PreferenceManager.kt b/app/src/main/java/com/agronick/launcher/PreferenceManager.kt index 480ef04..6b0f19f 100644 --- a/app/src/main/java/com/agronick/launcher/PreferenceManager.kt +++ b/app/src/main/java/com/agronick/launcher/PreferenceManager.kt @@ -3,13 +3,14 @@ package com.agronick.launcher import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import java.util.* +import kotlin.collections.ArrayList object PreferenceManager { private var preferences = Launcher.appContext.getSharedPreferences("prefs", Context.MODE_PRIVATE) private var editor = preferences.edit() - private var appOrder = preferences.getString("app_order", "")?.split(";")?.toTypedArray()!! - .toCollection(ArrayList()) + private lateinit var appOrder: ArrayList private var sep = ";" fun getSetDiam(value: Double): Double { @@ -32,7 +33,7 @@ object PreferenceManager { order = null ) }.toMap().toMutableMap() - val prefList = preferences.getString("app_order", null) ?: return asMapp.values.toList() + val prefList = preferences.getString("app_order", "") val blankPinfo = PInfo( appname = null, pname = null, @@ -41,10 +42,19 @@ object PreferenceManager { order = null ) val matchedItems = - prefList.split(sep).toTypedArray().map { asMapp.remove(it) ?: blankPinfo } + prefList!!.split(sep).toTypedArray().map { asMapp.remove(it) ?: blankPinfo } .toMutableList() + if (matchedItems.size == 1 && matchedItems.first().appname == null) { + matchedItems.clear() + } matchedItems.addAll(asMapp.values) - matchedItems.mapIndexed { order, pInfo -> pInfo.order = order } + matchedItems.removeIf { it.pname?.startsWith("com.agronick.launcher") ?: false } + val orderedItems = LinkedList() + matchedItems.forEachIndexed { order, pInfo -> + pInfo.order = order + orderedItems.add(pInfo.pname ?: "") + } + appOrder = orderedItems.toCollection(ArrayList()) return matchedItems } diff --git a/app/src/main/java/com/agronick/launcher/Reorderer.kt b/app/src/main/java/com/agronick/launcher/Reorderer.kt index baf9193..10d6d53 100644 --- a/app/src/main/java/com/agronick/launcher/Reorderer.kt +++ b/app/src/main/java/com/agronick/launcher/Reorderer.kt @@ -2,7 +2,6 @@ package com.agronick.launcher import android.animation.AnimatorSet import android.animation.ValueAnimator -import android.util.Log import androidx.core.animation.doOnEnd import util.geometry.Circle import util.geometry.Vector2 @@ -67,6 +66,7 @@ class Reorderer( } else { animateAppPosition(overApp, suppressedAppCopy.left, suppressedAppCopy.top) animateAppPosition(app, overApp.left, overApp.top) + PreferenceManager.swap(app.pkgInfo, overApp.pkgInfo) } ValueAnimator.ofInt((suppressedAppCopy.size * 1.4).toInt(), suppressedAppCopy.size) .apply { @@ -82,11 +82,10 @@ class Reorderer( } - fun checkAtEdge(offsetVector: Vector2, lastCircle: Circle?, radius: Int): Vector2? { + fun checkAtEdge(offsetVector: Vector2, lastCircle: Circle?, density: Float): Vector2? { if (lastCircle == null) return null - val maxDistance = lastCircle.r - Log.d("offset", "${offsetVector.distance(lastCircle.c)} >= ${maxDistance}") - if (offsetVector.distance(lastCircle.c) >= maxDistance - radius) { + val maxDistance = lastCircle.r * density + if (offsetVector.distance(lastCircle.c) >= maxDistance) { val angle = Math.toRadians(lastCircle.c.angleBetween(offsetVector)).toFloat() return Vector2(kotlin.math.sin(angle) * -2, kotlin.math.cos(angle) * 2) } From d62970f4de3f98ac989758bf32ace1d4953592ac Mon Sep 17 00:00:00 2001 From: Kyle Agronick Date: Mon, 15 Aug 2022 12:45:07 -0400 Subject: [PATCH 8/9] Added broken functionality for scrollEndFn --- app/src/main/AndroidManifest.xml | 2 +- .../java/com/agronick/launcher/Container.kt | 2 - .../com/agronick/launcher/MainActivity.kt | 10 +-- .../java/com/agronick/launcher/MainView.kt | 78 +++++++++++++++---- .../agronick/launcher/PreferenceManager.kt | 4 +- .../java/com/agronick/launcher/Reorderer.kt | 5 +- .../com/agronick/launcher/StaticValues.kt | 1 + 7 files changed, 73 insertions(+), 29 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3895a8d..39e52fc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,10 +30,10 @@ - diff --git a/app/src/main/java/com/agronick/launcher/Container.kt b/app/src/main/java/com/agronick/launcher/Container.kt index c23febf..ed3ff30 100644 --- a/app/src/main/java/com/agronick/launcher/Container.kt +++ b/app/src/main/java/com/agronick/launcher/Container.kt @@ -1,7 +1,6 @@ package com.agronick.launcher import android.graphics.Canvas -import android.util.Log import util.geometry.Circle import util.geometry.Vector2 import java.util.* @@ -126,7 +125,6 @@ class Container(appList: List, density: Float) { left -= appCircleSize + appCircleMargin } val top = (row * (appCircleSize * 2) + row * appCircleMargin).toFloat() - Log.d(tag, "${left} ${top}") return Pair(left, top) } diff --git a/app/src/main/java/com/agronick/launcher/MainActivity.kt b/app/src/main/java/com/agronick/launcher/MainActivity.kt index 7473a4a..5cba71f 100644 --- a/app/src/main/java/com/agronick/launcher/MainActivity.kt +++ b/app/src/main/java/com/agronick/launcher/MainActivity.kt @@ -11,10 +11,8 @@ import android.view.Window import androidx.core.view.GestureDetectorCompat -var TAG = "main" class MainActivity : Activity(), GestureDetector.OnGestureListener { - private var wasScrolling = false private lateinit var mDetector: GestureDetectorCompat private lateinit var mainView: MainView @@ -47,7 +45,7 @@ class MainActivity : Activity(), GestureDetector.OnGestureListener { } else if (mainView.reorderer != null) { mainView.handleLongPress(event) return true - } else if (wasScrolling && event.action == MotionEvent.ACTION_UP) { + } else if (mainView.wasScrolling && event.action == MotionEvent.ACTION_UP) { mainView.checkOverPanLimit() return true } else { @@ -116,10 +114,8 @@ class MainActivity : Activity(), GestureDetector.OnGestureListener { distanceX: Float, distanceY: Float ): Boolean { - mainView.offsetLeft -= distanceX - mainView.offsetTop -= distanceY - mainView.prepareInvalidate() - wasScrolling = true + mainView.handleScroll(distanceX, distanceY) + mainView.wasScrolling = true return true } diff --git a/app/src/main/java/com/agronick/launcher/MainView.kt b/app/src/main/java/com/agronick/launcher/MainView.kt index ab5f647..9e9626a 100644 --- a/app/src/main/java/com/agronick/launcher/MainView.kt +++ b/app/src/main/java/com/agronick/launcher/MainView.kt @@ -4,6 +4,7 @@ import android.animation.AnimatorSet import android.animation.ValueAnimator import android.content.Context import android.graphics.Canvas +import android.util.Log import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.View @@ -23,6 +24,29 @@ class MainView(context: Context, appList: List) : View(context) { private var edgeLimit = 100000f var allHidden = false + var scrollEndFn: (() -> Unit)? = null + var wasScrolling = false + set(value: Boolean) { + if (!wasScrolling) { + Log.e(StaticValues.tag, "ScrollEndFn without scroll") + } + if (scrollEndFn != null) { + scrollEndFn!!() + scrollEndFn = null + } + field = value + } + + private val STATE_NONE = 0 + private val STATE_REORDERING = 1 + private val STATE_OPENING = 2 + + private fun getActiveState(): Int { + if (reorderer != null) return STATE_REORDERING + if (openingApp != null) return STATE_OPENING + return STATE_NONE + } + var openingApp: App? = null init { @@ -33,6 +57,13 @@ class MainView(context: Context, appList: List) : View(context) { var reorderer: Reorderer? = null + fun handleScroll(distanceX: Float, distanceY: Float) { + if (getActiveState() != STATE_NONE) return + offsetLeft -= distanceX + offsetTop -= distanceY + prepareInvalidate() + } + fun resetReorderEdgeTimer() { edgeTimer?.cancel() } @@ -62,22 +93,31 @@ class MainView(context: Context, appList: List) : View(context) { } fun handleLongPress(event: MotionEvent) { + val state = getActiveState() + if (state == STATE_OPENING) return val offset = getRelativePosition(Pair(event.x, event.y)) - if (reorderer != null) { + if (state == STATE_REORDERING) { if (event.action == MotionEvent.ACTION_UP) { - reorderer!!.onStopReorder( - when (container.getLimit(offsetLeft, offsetTop, canvasSize)) { - Pair(offsetLeft, offsetTop) -> container.getAppAtPoint( - Vector2( - offset.x, - offset.y + val scrollEndEvent = { + reorderer!!.onStopReorder( + when (container.getLimit(offsetLeft, offsetTop, canvasSize)) { + Pair(offsetLeft, offsetTop) -> container.getAppAtPoint( + Vector2( + offset.x, + offset.y + ) ) - ) - else -> null - } - ) - reorderer = null - resetReorderEdgeTimer() + else -> null + } + ) + reorderer = null + resetReorderEdgeTimer() + } + if (wasScrolling) { + scrollEndFn = scrollEndEvent + } else { + scrollEndEvent() + } } else { reorderer!!.onMove(offset) val newOffsets = @@ -89,11 +129,12 @@ class MainView(context: Context, appList: List) : View(context) { } prepareInvalidate() } - } else { + } else if (state == STATE_NONE) { val app = container.getAppAtPoint(Vector2(offset.x, offset.y)) if (app != null) { post { - reorderer = Reorderer(container, app, ::prepareInvalidate) + reorderer = + Reorderer(container, app, ::prepareInvalidate, container.appCircleSize) performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) } } @@ -127,11 +168,15 @@ class MainView(context: Context, appList: List) : View(context) { duration = StaticValues.durationOpen playTogether(*animators.toTypedArray()) start() + doOnEnd { wasScrolling = false } } + } else { + wasScrolling = false } } fun handleClick(x: Float, y: Float) { + if (getActiveState() != STATE_NONE) return val offset = getRelativePosition(Pair(x, y)) val app = container.getAppAtPoint(offset) if (app != null && app.pkgInfo.activityName != null) { @@ -140,6 +185,8 @@ class MainView(context: Context, appList: List) : View(context) { } fun setupOpenAnim(app: App) { + if (getActiveState() != STATE_NONE) return + val face = container.lastCircle ?: return openingApp = app @@ -171,6 +218,7 @@ class MainView(context: Context, appList: List) : View(context) { doOnEnd { postDelayed({ onPackageClick?.let { it1 -> it1(app.pkgInfo) } + openingApp = null }, 200) } start() diff --git a/app/src/main/java/com/agronick/launcher/PreferenceManager.kt b/app/src/main/java/com/agronick/launcher/PreferenceManager.kt index 6b0f19f..b4c1aa4 100644 --- a/app/src/main/java/com/agronick/launcher/PreferenceManager.kt +++ b/app/src/main/java/com/agronick/launcher/PreferenceManager.kt @@ -60,8 +60,8 @@ object PreferenceManager { fun swap(pinfo1: PInfo, pinfo2: PInfo) { // Swap in array - appOrder[pinfo1.order!!] = pinfo2.pname!! - appOrder[pinfo2.order!!] = pinfo1.pname!! + appOrder[pinfo1.order!!] = pinfo2.pname ?: "" + appOrder[pinfo2.order!!] = pinfo1.pname ?: "" // Swap on objects val temp = pinfo1.order diff --git a/app/src/main/java/com/agronick/launcher/Reorderer.kt b/app/src/main/java/com/agronick/launcher/Reorderer.kt index 10d6d53..90fc63d 100644 --- a/app/src/main/java/com/agronick/launcher/Reorderer.kt +++ b/app/src/main/java/com/agronick/launcher/Reorderer.kt @@ -9,7 +9,8 @@ import util.geometry.Vector2 class Reorderer( private val container: Container, private var app: App, - val invalidate: () -> Unit + val invalidate: () -> Unit, + val defaultCircleSize: Int ) { private var suppressedAppCopy: App = app.copy() private var lastPosition = HashSet() @@ -68,7 +69,7 @@ class Reorderer( animateAppPosition(app, overApp.left, overApp.top) PreferenceManager.swap(app.pkgInfo, overApp.pkgInfo) } - ValueAnimator.ofInt((suppressedAppCopy.size * 1.4).toInt(), suppressedAppCopy.size) + ValueAnimator.ofInt((suppressedAppCopy.size * 1.4).toInt(), defaultCircleSize) .apply { duration = StaticValues.durationRise addUpdateListener { animator -> diff --git a/app/src/main/java/com/agronick/launcher/StaticValues.kt b/app/src/main/java/com/agronick/launcher/StaticValues.kt index 31c3eb1..f4769c3 100644 --- a/app/src/main/java/com/agronick/launcher/StaticValues.kt +++ b/app/src/main/java/com/agronick/launcher/StaticValues.kt @@ -7,5 +7,6 @@ class StaticValues { val durationRise = 300L val durationSwap = 300L val durationOpen = 600L + val tag = "Flattery" } } \ No newline at end of file From c85beff107f5589ef19b1eb3ea9a66a1a07aee16 Mon Sep 17 00:00:00 2001 From: Kyle Agronick Date: Mon, 15 Aug 2022 12:48:07 -0400 Subject: [PATCH 9/9] Reverted back to old scroll --- .../com/agronick/launcher/MainActivity.kt | 11 +++-- .../java/com/agronick/launcher/MainView.kt | 48 +++++-------------- 2 files changed, 19 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/agronick/launcher/MainActivity.kt b/app/src/main/java/com/agronick/launcher/MainActivity.kt index 5cba71f..267c0ea 100644 --- a/app/src/main/java/com/agronick/launcher/MainActivity.kt +++ b/app/src/main/java/com/agronick/launcher/MainActivity.kt @@ -11,8 +11,8 @@ import android.view.Window import androidx.core.view.GestureDetectorCompat - class MainActivity : Activity(), GestureDetector.OnGestureListener { + private var wasScrolling = false private lateinit var mDetector: GestureDetectorCompat private lateinit var mainView: MainView @@ -45,8 +45,9 @@ class MainActivity : Activity(), GestureDetector.OnGestureListener { } else if (mainView.reorderer != null) { mainView.handleLongPress(event) return true - } else if (mainView.wasScrolling && event.action == MotionEvent.ACTION_UP) { + } else if (wasScrolling && event.action == MotionEvent.ACTION_UP) { mainView.checkOverPanLimit() + wasScrolling = false return true } else { super.onTouchEvent(event) @@ -114,8 +115,10 @@ class MainActivity : Activity(), GestureDetector.OnGestureListener { distanceX: Float, distanceY: Float ): Boolean { - mainView.handleScroll(distanceX, distanceY) - mainView.wasScrolling = true + mainView.offsetLeft -= distanceX + mainView.offsetTop -= distanceY + mainView.prepareInvalidate() + wasScrolling = true return true } diff --git a/app/src/main/java/com/agronick/launcher/MainView.kt b/app/src/main/java/com/agronick/launcher/MainView.kt index 9e9626a..8ebb382 100644 --- a/app/src/main/java/com/agronick/launcher/MainView.kt +++ b/app/src/main/java/com/agronick/launcher/MainView.kt @@ -4,7 +4,6 @@ import android.animation.AnimatorSet import android.animation.ValueAnimator import android.content.Context import android.graphics.Canvas -import android.util.Log import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.View @@ -24,19 +23,6 @@ class MainView(context: Context, appList: List) : View(context) { private var edgeLimit = 100000f var allHidden = false - var scrollEndFn: (() -> Unit)? = null - var wasScrolling = false - set(value: Boolean) { - if (!wasScrolling) { - Log.e(StaticValues.tag, "ScrollEndFn without scroll") - } - if (scrollEndFn != null) { - scrollEndFn!!() - scrollEndFn = null - } - field = value - } - private val STATE_NONE = 0 private val STATE_REORDERING = 1 private val STATE_OPENING = 2 @@ -98,26 +84,19 @@ class MainView(context: Context, appList: List) : View(context) { val offset = getRelativePosition(Pair(event.x, event.y)) if (state == STATE_REORDERING) { if (event.action == MotionEvent.ACTION_UP) { - val scrollEndEvent = { - reorderer!!.onStopReorder( - when (container.getLimit(offsetLeft, offsetTop, canvasSize)) { - Pair(offsetLeft, offsetTop) -> container.getAppAtPoint( - Vector2( - offset.x, - offset.y - ) + reorderer!!.onStopReorder( + when (container.getLimit(offsetLeft, offsetTop, canvasSize)) { + Pair(offsetLeft, offsetTop) -> container.getAppAtPoint( + Vector2( + offset.x, + offset.y ) - else -> null - } - ) - reorderer = null - resetReorderEdgeTimer() - } - if (wasScrolling) { - scrollEndFn = scrollEndEvent - } else { - scrollEndEvent() - } + ) + else -> null + } + ) + reorderer = null + resetReorderEdgeTimer() } else { reorderer!!.onMove(offset) val newOffsets = @@ -168,10 +147,7 @@ class MainView(context: Context, appList: List) : View(context) { duration = StaticValues.durationOpen playTogether(*animators.toTypedArray()) start() - doOnEnd { wasScrolling = false } } - } else { - wasScrolling = false } }