mirror of
https://github.com/jrpie/Launcher.git
synced 2025-04-12 15:24:31 +02:00
Compare commits
3 commits
012f13c827
...
47ae0bf35f
Author | SHA1 | Date | |
---|---|---|---|
47ae0bf35f | |||
5669279c64 | |||
0c0d90a357 |
8 changed files with 420 additions and 155 deletions
71
README.md
71
README.md
|
@ -14,12 +14,6 @@
|
||||||
µLauncher is an Android home screen that lets you launch apps using swipe gestures and button presses.
|
µLauncher is an Android home screen that lets you launch apps using swipe gestures and button presses.
|
||||||
It is *minimal, efficient and free of distraction*.
|
It is *minimal, efficient and free of distraction*.
|
||||||
|
|
||||||
Your home screen only displays the date, time and a wallpaper.
|
|
||||||
Pressing back or swiping up (this can be configured) opens a list
|
|
||||||
of all installed apps, which can be searched efficiently.
|
|
||||||
|
|
||||||
This is a fork of [finnmglas's app Launcher][original-repo].
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://f-droid.org/packages/de.jrpie.android.launcher/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80"></a>
|
<a href="https://f-droid.org/packages/de.jrpie.android.launcher/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80"></a>
|
||||||
<a href="https://accrescent.app/app/de.jrpie.android.launcher.accrescent"><img alt="Get it on Accrescent" src="https://accrescent.app/badges/get-it-on.png" height="80"></a>
|
<a href="https://accrescent.app/app/de.jrpie.android.launcher.accrescent"><img alt="Get it on Accrescent" src="https://accrescent.app/badges/get-it-on.png" height="80"></a>
|
||||||
|
@ -51,6 +45,45 @@ You can also [get it on Google Play](https://play.google.com/store/apps/details?
|
||||||
height="400">
|
height="400">
|
||||||
|
|
||||||
|
|
||||||
|
µLauncher is a fork of [finnmglas's app Launcher][original-repo].
|
||||||
|
An incomplete list of changes can be found [here](docs/launcher.md).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
µLauncher only displays the date, time and a wallpaper.
|
||||||
|
Pressing back or swiping up (this can be configured) opens a list
|
||||||
|
of all installed apps, which can be searched efficiently.
|
||||||
|
|
||||||
|
The following gestures are available:
|
||||||
|
- volume up / down,
|
||||||
|
- swipe up / down / left / right,
|
||||||
|
- swipe with two fingers,
|
||||||
|
- swipe on the left / right resp. top / bottom edge,
|
||||||
|
- draw < / > / V / Λ
|
||||||
|
- click on date / time,
|
||||||
|
- double click,
|
||||||
|
- long click,
|
||||||
|
- back button.
|
||||||
|
|
||||||
|
To every gesture you can bind one of the following actions:
|
||||||
|
- launch an app,
|
||||||
|
- open a list of all / favorite / private apps,
|
||||||
|
- open µLauncher settings,
|
||||||
|
- toggle private space lock,
|
||||||
|
- lock the screen,
|
||||||
|
- toggle the torch,
|
||||||
|
- volume up / down,
|
||||||
|
- go to previous / next audio track.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
µLauncher is compatible with [work profile](https://www.android.com/enterprise/work-profile/),
|
||||||
|
so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used.
|
||||||
|
|
||||||
|
By default the font is set to [Hack][hack-font], but other fonts can be selected.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
There are several ways to contribute to this app:
|
There are several ways to contribute to this app:
|
||||||
|
@ -63,34 +96,10 @@ There are several ways to contribute to this app:
|
||||||
- Open a new pull request.
|
- Open a new pull request.
|
||||||
|
|
||||||
|
|
||||||
See [BUILD.md](BUILD.md) for instructions how to build this project.
|
See [build.md](docs/build.md) for instructions how to build this project.
|
||||||
The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds.
|
The [CI pipeline](https://github.com/jrpie/Launcher/actions) automatically creates debug builds.
|
||||||
Note that those are not signed.
|
Note that those are not signed.
|
||||||
|
|
||||||
## Notable changes compared to [Finn's Launcher][original-repo]:
|
|
||||||
|
|
||||||
* Edge gestures: There is a setting to allow distinguishing swiping at the edges of the screen from swiping in the center.
|
|
||||||
* Compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used.
|
|
||||||
* The home button now works as expected.
|
|
||||||
|
|
||||||
### Visual
|
|
||||||
* This app uses the system wallpaper instead of a custom solution.
|
|
||||||
* The font has been changed to [Hack][hack-font].
|
|
||||||
* Font Awesome Icons were replaced by Material icons.
|
|
||||||
* The gear button on the home screen was removed. Instead pressing back opens the list of applications and the app settings are accessible from there.
|
|
||||||
|
|
||||||
|
|
||||||
### Search
|
|
||||||
* The search algorithm was modified to prefer matches at the beginning of the app name, i.e. when searching for `"te"`, `"termux"` is sorted before `"notes"`.
|
|
||||||
* The search bar was moved to the bottom of the screen.
|
|
||||||
|
|
||||||
### Technical
|
|
||||||
* Small improvements to the gesture detection.
|
|
||||||
* Different apps set as default.
|
|
||||||
* Package name was changed to `de.jrpie.android.launcher` to avoid clashing with the original app.
|
|
||||||
* Dropped support for API < 21 (i.e. pre Lollypop)
|
|
||||||
* Some refactoring
|
|
||||||
---
|
|
||||||
---
|
---
|
||||||
[hack-font]: https://sourcefoundry.org/hack/
|
[hack-font]: https://sourcefoundry.org/hack/
|
||||||
[original-repo]: https://github.com/finnmglas/Launcher
|
[original-repo]: https://github.com/finnmglas/Launcher
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package de.jrpie.android.launcher.actions
|
package de.jrpie.android.launcher.actions
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import de.jrpie.android.launcher.R
|
import de.jrpie.android.launcher.R
|
||||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||||
|
|
||||||
|
@ -169,6 +170,54 @@ enum class Gesture(
|
||||||
R.array.default_double_right,
|
R.array.default_double_right,
|
||||||
R.anim.left_right
|
R.anim.left_right
|
||||||
),
|
),
|
||||||
|
SWIPE_LARGER(
|
||||||
|
"action.larger",
|
||||||
|
R.string.settings_gesture_swipe_larger,
|
||||||
|
R.string.settings_gesture_description_swipe_larger,
|
||||||
|
R.array.no_default
|
||||||
|
),
|
||||||
|
SWIPE_LARGER_REVERSE(
|
||||||
|
"action.larger_reverse",
|
||||||
|
R.string.settings_gesture_swipe_larger_reverse,
|
||||||
|
R.string.settings_gesture_description_swipe_larger_reverse,
|
||||||
|
R.array.no_default
|
||||||
|
),
|
||||||
|
SWIPE_SMALLER(
|
||||||
|
"action.smaller",
|
||||||
|
R.string.settings_gesture_swipe_smaller,
|
||||||
|
R.string.settings_gesture_description_swipe_smaller,
|
||||||
|
R.array.no_default
|
||||||
|
),
|
||||||
|
SWIPE_SMALLER_REVERSE(
|
||||||
|
"action.smaller_reverse",
|
||||||
|
R.string.settings_gesture_swipe_smaller_reverse,
|
||||||
|
R.string.settings_gesture_description_swipe_smaller_reverse,
|
||||||
|
R.array.no_default
|
||||||
|
),
|
||||||
|
SWIPE_LAMBDA(
|
||||||
|
"action.lambda",
|
||||||
|
R.string.settings_gesture_swipe_lambda,
|
||||||
|
R.string.settings_gesture_description_swipe_lambda,
|
||||||
|
R.array.no_default
|
||||||
|
),
|
||||||
|
SWIPE_LAMBDA_REVERSE(
|
||||||
|
"action.lambda_reverse",
|
||||||
|
R.string.settings_gesture_swipe_lambda_reverse,
|
||||||
|
R.string.settings_gesture_description_swipe_lambda_reverse,
|
||||||
|
R.array.no_default
|
||||||
|
),
|
||||||
|
SWIPE_V(
|
||||||
|
"action.v",
|
||||||
|
R.string.settings_gesture_swipe_v,
|
||||||
|
R.string.settings_gesture_description_swipe_v,
|
||||||
|
R.array.no_default
|
||||||
|
),
|
||||||
|
SWIPE_V_REVERSE(
|
||||||
|
"action.v_reverse",
|
||||||
|
R.string.settings_gesture_swipe_v_reverse,
|
||||||
|
R.string.settings_gesture_description_swipe_v_reverse,
|
||||||
|
R.array.no_default
|
||||||
|
),
|
||||||
BACK(
|
BACK(
|
||||||
"action.back",
|
"action.back",
|
||||||
R.string.settings_gesture_back,
|
R.string.settings_gesture_back,
|
||||||
|
@ -267,6 +316,7 @@ enum class Gesture(
|
||||||
}
|
}
|
||||||
|
|
||||||
operator fun invoke(context: Context) {
|
operator fun invoke(context: Context) {
|
||||||
|
Log.i("Launcher", "Detected gesture: $this")
|
||||||
val action = Action.forGesture(this)
|
val action = Action.forGesture(this)
|
||||||
Action.launch(action, context, this.animationIn, this.animationOut)
|
Action.launch(action, context, this.animationIn, this.animationOut)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,11 @@ import android.content.res.Resources
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import android.view.GestureDetector
|
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewConfiguration
|
|
||||||
import android.window.OnBackInvokedDispatcher
|
import android.window.OnBackInvokedDispatcher
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.GestureDetectorCompat
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import de.jrpie.android.launcher.R
|
import de.jrpie.android.launcher.R
|
||||||
import de.jrpie.android.launcher.actions.Action
|
import de.jrpie.android.launcher.actions.Action
|
||||||
|
@ -23,13 +20,6 @@ import de.jrpie.android.launcher.databinding.HomeBinding
|
||||||
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||||
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
|
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.Timer
|
|
||||||
import kotlin.concurrent.fixedRateTimer
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
import kotlin.math.tan
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [HomeActivity] is the actual application Launcher,
|
* [HomeActivity] is the actual application Launcher,
|
||||||
|
@ -43,10 +33,10 @@ import kotlin.math.tan
|
||||||
* - Setting global variables (preferences etc.)
|
* - Setting global variables (preferences etc.)
|
||||||
* - Opening the [TutorialActivity] on new installations
|
* - Opening the [TutorialActivity] on new installations
|
||||||
*/
|
*/
|
||||||
class HomeActivity : UIObject, AppCompatActivity(),
|
class HomeActivity : UIObject, AppCompatActivity() {
|
||||||
GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
|
|
||||||
|
|
||||||
private lateinit var binding: HomeBinding
|
private lateinit var binding: HomeBinding
|
||||||
|
private lateinit var touchGestureDetector: TouchGestureDetector
|
||||||
|
|
||||||
private var sharedPreferencesListener =
|
private var sharedPreferencesListener =
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
|
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
|
||||||
|
@ -61,22 +51,29 @@ class HomeActivity : UIObject, AppCompatActivity(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var edgeWidth = 0.15f
|
|
||||||
|
|
||||||
private var bufferedPointerCount = 1 // how many fingers on screen
|
|
||||||
private var pointerBufferTimer = Timer()
|
|
||||||
|
|
||||||
private lateinit var mDetector: GestureDetectorCompat
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super<AppCompatActivity>.onCreate(savedInstanceState)
|
super<AppCompatActivity>.onCreate(savedInstanceState)
|
||||||
super<UIObject>.onCreate()
|
super<UIObject>.onCreate()
|
||||||
|
|
||||||
|
|
||||||
|
val displayMetrics = DisplayMetrics()
|
||||||
|
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||||
|
|
||||||
|
val width = displayMetrics.widthPixels
|
||||||
|
val height = displayMetrics.heightPixels
|
||||||
|
|
||||||
|
touchGestureDetector = TouchGestureDetector(
|
||||||
|
this,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
|
||||||
|
)
|
||||||
|
|
||||||
// Initialise layout
|
// Initialise layout
|
||||||
binding = HomeBinding.inflate(layoutInflater)
|
binding = HomeBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|
||||||
// Handle back key / gesture on Android 13+, cf. onKeyDown()
|
// Handle back key / gesture on Android 13+, cf. onKeyDown()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
||||||
|
@ -95,9 +92,6 @@ class HomeActivity : UIObject, AppCompatActivity(),
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super<AppCompatActivity>.onStart()
|
super<AppCompatActivity>.onStart()
|
||||||
|
|
||||||
mDetector = GestureDetectorCompat(this, this)
|
|
||||||
mDetector.setOnDoubleTapListener(this)
|
|
||||||
|
|
||||||
super<UIObject>.onStart()
|
super<UIObject>.onStart()
|
||||||
|
|
||||||
LauncherPreferences.getSharedPreferences()
|
LauncherPreferences.getSharedPreferences()
|
||||||
|
@ -172,7 +166,8 @@ class HomeActivity : UIObject, AppCompatActivity(),
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
edgeWidth = LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
|
touchGestureDetector.edgeWidth =
|
||||||
|
LauncherPreferences.enabled_gestures().edgeSwipeEdgeWidth() / 100f
|
||||||
|
|
||||||
initClock()
|
initClock()
|
||||||
updateSettingsFallbackButtonVisibility()
|
updateSettingsFallbackButtonVisibility()
|
||||||
|
@ -211,95 +206,8 @@ class HomeActivity : UIObject, AppCompatActivity(),
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFling(e1: MotionEvent?, e2: MotionEvent, dX: Float, dY: Float): Boolean {
|
|
||||||
|
|
||||||
if (e1 == null) return false
|
|
||||||
|
|
||||||
|
|
||||||
val displayMetrics = DisplayMetrics()
|
|
||||||
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
|
||||||
|
|
||||||
val width = displayMetrics.widthPixels
|
|
||||||
val height = displayMetrics.heightPixels
|
|
||||||
|
|
||||||
val diffX = e1.x - e2.x
|
|
||||||
val diffY = e1.y - e2.y
|
|
||||||
|
|
||||||
val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe()
|
|
||||||
val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe()
|
|
||||||
|
|
||||||
val threshold = ViewConfiguration.get(this).scaledTouchSlop
|
|
||||||
val angularThreshold = tan(Math.PI / 6)
|
|
||||||
|
|
||||||
var gesture = if (angularThreshold * abs(diffX) > abs(diffY)) { // horizontal swipe
|
|
||||||
if (diffX > threshold)
|
|
||||||
Gesture.SWIPE_LEFT
|
|
||||||
else if (diffX < -threshold)
|
|
||||||
Gesture.SWIPE_RIGHT
|
|
||||||
else null
|
|
||||||
} else if (angularThreshold * abs(diffY) > abs(diffX)) { // vertical swipe
|
|
||||||
// Only open if the swipe was not from the phones top edge
|
|
||||||
// TODO: replace 100px by sensible dp value (e.g. twice the height of the status bar)
|
|
||||||
if (diffY < -threshold && e1.y > 100)
|
|
||||||
Gesture.SWIPE_DOWN
|
|
||||||
else if (diffY > threshold)
|
|
||||||
Gesture.SWIPE_UP
|
|
||||||
else null
|
|
||||||
} else null
|
|
||||||
|
|
||||||
if (doubleActions && bufferedPointerCount > 1) {
|
|
||||||
gesture = gesture?.let(Gesture::getDoubleVariant)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (edgeActions) {
|
|
||||||
if (max(e1.x, e2.x) < edgeWidth * width) {
|
|
||||||
gesture = gesture?.getEdgeVariant(Gesture.Edge.LEFT)
|
|
||||||
} else if (min(e1.x, e2.x) > (1 - edgeWidth) * width) {
|
|
||||||
gesture = gesture?.getEdgeVariant(Gesture.Edge.RIGHT)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (max(e1.y, e2.y) < edgeWidth * height) {
|
|
||||||
gesture = gesture?.getEdgeVariant(Gesture.Edge.TOP)
|
|
||||||
} else if (min(e1.y, e2.y) > (1 - edgeWidth) * height) {
|
|
||||||
gesture = gesture?.getEdgeVariant(Gesture.Edge.BOTTOM)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gesture?.invoke(this)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongPress(event: MotionEvent) {
|
|
||||||
Gesture.LONG_CLICK(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDoubleTap(event: MotionEvent): Boolean {
|
|
||||||
Gesture.DOUBLE_CLICK(this)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tooltip
|
|
||||||
override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
return touchGestureDetector.onTouchEvent(event) || super.onTouchEvent(event)
|
||||||
// Buffer / Debounce the pointer count
|
|
||||||
if (event.pointerCount > bufferedPointerCount) {
|
|
||||||
bufferedPointerCount = event.pointerCount
|
|
||||||
pointerBufferTimer = fixedRateTimer("pointerBufferTimer", true, 300, 1000) {
|
|
||||||
bufferedPointerCount = 1
|
|
||||||
this.cancel() // a non-recurring timer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (mDetector.onTouchEvent(event)) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
super.onTouchEvent(event)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setOnClicks() {
|
override fun setOnClicks() {
|
||||||
|
@ -329,16 +237,4 @@ class HomeActivity : UIObject, AppCompatActivity(),
|
||||||
override fun isHomeScreen(): Boolean {
|
override fun isHomeScreen(): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* TODO: Remove those. For now they are necessary
|
|
||||||
* because this inherits from GestureDetector.OnGestureListener */
|
|
||||||
override fun onDoubleTapEvent(event: MotionEvent): Boolean { return false }
|
|
||||||
override fun onDown(event: MotionEvent): Boolean { return false }
|
|
||||||
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, dX: Float, dY: Float): Boolean { return false }
|
|
||||||
override fun onShowPress(event: MotionEvent) {}
|
|
||||||
override fun onSingleTapUp(event: MotionEvent): Boolean { return false }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,241 @@
|
||||||
|
package de.jrpie.android.launcher.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.ViewConfiguration
|
||||||
|
import de.jrpie.android.launcher.actions.Gesture
|
||||||
|
import de.jrpie.android.launcher.preferences.LauncherPreferences
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.tan
|
||||||
|
|
||||||
|
class TouchGestureDetector(
|
||||||
|
private val context: Context,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
var edgeWidth: Float
|
||||||
|
) {
|
||||||
|
private val ANGULAR_THRESHOLD = tan(Math.PI / 6)
|
||||||
|
private val TOUCH_SLOP: Int
|
||||||
|
private val TOUCH_SLOP_SQUARE: Int
|
||||||
|
private val DOUBLE_TAP_SLOP: Int
|
||||||
|
private val DOUBLE_TAP_SLOP_SQUARE: Int
|
||||||
|
private val LONG_PRESS_TIMEOUT: Int
|
||||||
|
private val TAP_TIMEOUT: Int
|
||||||
|
private val DOUBLE_TAP_TIMEOUT: Int
|
||||||
|
|
||||||
|
private val MIN_TRIANGLE_HEIGHT = 250
|
||||||
|
|
||||||
|
|
||||||
|
data class Vector(val x: Float, val y: Float) {
|
||||||
|
fun absSquared(): Float {
|
||||||
|
return this.x * this.x + this.y * this.y
|
||||||
|
}
|
||||||
|
fun plus(vector: Vector): Vector {
|
||||||
|
return Vector(this.x + vector.x, this.y + vector.y)
|
||||||
|
}
|
||||||
|
fun max(other: Vector): Vector {
|
||||||
|
return Vector(max(this.x, other.x), max(this.y, other.y))
|
||||||
|
}
|
||||||
|
fun min(other: Vector): Vector {
|
||||||
|
return Vector(min(this.x, other.x), min(this.y, other.y))
|
||||||
|
}
|
||||||
|
operator fun minus(vector: Vector): Vector {
|
||||||
|
return Vector(this.x - vector.x, this.y - vector.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PointerPath(
|
||||||
|
val number: Int,
|
||||||
|
val start: Vector,
|
||||||
|
var last: Vector = start
|
||||||
|
) {
|
||||||
|
var min = Vector(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
|
||||||
|
var max = Vector(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY)
|
||||||
|
fun sizeSquared(): Float {
|
||||||
|
return (max - min).absSquared()
|
||||||
|
}
|
||||||
|
fun getDirection(): Vector {
|
||||||
|
return last - start
|
||||||
|
}
|
||||||
|
fun update(vector: Vector) {
|
||||||
|
min = min.min(vector)
|
||||||
|
max = max.max(vector)
|
||||||
|
last = vector
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun PointerPath.isTap(): Boolean {
|
||||||
|
return sizeSquared() < TOUCH_SLOP_SQUARE
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val configuration = ViewConfiguration.get(context)
|
||||||
|
TOUCH_SLOP = configuration.scaledTouchSlop
|
||||||
|
TOUCH_SLOP_SQUARE = TOUCH_SLOP * TOUCH_SLOP
|
||||||
|
DOUBLE_TAP_SLOP = configuration.scaledDoubleTapSlop
|
||||||
|
DOUBLE_TAP_SLOP_SQUARE = DOUBLE_TAP_SLOP * DOUBLE_TAP_SLOP
|
||||||
|
|
||||||
|
LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout()
|
||||||
|
TAP_TIMEOUT = ViewConfiguration.getTapTimeout()
|
||||||
|
DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var paths = HashMap<Int, PointerPath>()
|
||||||
|
|
||||||
|
private var lastTappedTime = 0L
|
||||||
|
private var lastTappedLocation: Vector? = null
|
||||||
|
|
||||||
|
fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
val pointerIdToIndex =
|
||||||
|
(0..<event.pointerCount).associateBy { event.getPointerId(it) }
|
||||||
|
|
||||||
|
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||||
|
paths = HashMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// add new pointers
|
||||||
|
for(i in 0..<event.pointerCount){
|
||||||
|
if(paths.containsKey(event.getPointerId(i))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val index = pointerIdToIndex[i] ?: continue
|
||||||
|
paths[i] = PointerPath(
|
||||||
|
paths.entries.size,
|
||||||
|
Vector(event.getX(index), event.getY(index))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for( i in 0..<event.pointerCount) {
|
||||||
|
val index = pointerIdToIndex[i] ?: continue
|
||||||
|
|
||||||
|
repeat(event.historySize) {
|
||||||
|
paths[i]?.update(Vector(event.getHistoricalX(index), event.getHistoricalY(index)))
|
||||||
|
}
|
||||||
|
paths[i]?.update(Vector(event.getX(index), event.getY(index)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.actionMasked == MotionEvent.ACTION_UP) {
|
||||||
|
classifyPaths(paths, event.downTime, event.eventTime)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getGestureForDirection(direction: Vector): Gesture? {
|
||||||
|
return if (ANGULAR_THRESHOLD * abs(direction.x) > abs(direction.y)) { // horizontal swipe
|
||||||
|
if (direction.x > TOUCH_SLOP)
|
||||||
|
Gesture.SWIPE_RIGHT
|
||||||
|
else if (direction.x < -TOUCH_SLOP)
|
||||||
|
Gesture.SWIPE_LEFT
|
||||||
|
else null
|
||||||
|
} else if (ANGULAR_THRESHOLD * abs(direction.y) > abs(direction.x)) { // vertical swipe
|
||||||
|
if (direction.y < -TOUCH_SLOP)
|
||||||
|
Gesture.SWIPE_UP
|
||||||
|
else if (direction.y > TOUCH_SLOP)
|
||||||
|
Gesture.SWIPE_DOWN
|
||||||
|
else null
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun classifyPaths(paths: Map<Int, PointerPath>, timeStart: Long, timeEnd: Long) {
|
||||||
|
val duration = timeEnd - timeStart
|
||||||
|
val pointerCount = paths.entries.size
|
||||||
|
if (paths.entries.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val mainPointerPath = paths.entries.firstOrNull { it.value.number == 0 }?.value ?: return
|
||||||
|
|
||||||
|
// Ignore swipes at the very top, since this interferes with the status bar.
|
||||||
|
// TODO: replace 100px by sensible dp value (e.g. twice the height of the status bar)
|
||||||
|
if (paths.entries.any { it.value.start.y < 100 }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointerCount == 1 && mainPointerPath.isTap()) {
|
||||||
|
// detect taps
|
||||||
|
|
||||||
|
if (duration in 0..TAP_TIMEOUT) {
|
||||||
|
if (timeStart - lastTappedTime < DOUBLE_TAP_TIMEOUT &&
|
||||||
|
lastTappedLocation?.let {
|
||||||
|
(mainPointerPath.last - it).absSquared() < DOUBLE_TAP_SLOP_SQUARE} == true
|
||||||
|
) {
|
||||||
|
Gesture.DOUBLE_CLICK.invoke(context)
|
||||||
|
} else {
|
||||||
|
lastTappedTime = timeEnd
|
||||||
|
lastTappedLocation = mainPointerPath.last
|
||||||
|
}
|
||||||
|
} else if (duration > LONG_PRESS_TIMEOUT) {
|
||||||
|
// TODO: Don't wait until the finger is lifted.
|
||||||
|
// Instead set a timer to start long click as soon as LONG_PRESS_TIMEOUT is reached
|
||||||
|
Gesture.LONG_CLICK.invoke(context)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// detect swipes
|
||||||
|
|
||||||
|
val doubleActions = LauncherPreferences.enabled_gestures().doubleSwipe()
|
||||||
|
val edgeActions = LauncherPreferences.enabled_gestures().edgeSwipe()
|
||||||
|
|
||||||
|
var gesture = getGestureForDirection(mainPointerPath.getDirection())
|
||||||
|
|
||||||
|
if (doubleActions && pointerCount > 1) {
|
||||||
|
if (paths.entries.any { getGestureForDirection(it.value.getDirection()) != gesture }) {
|
||||||
|
// the directions of the pointers don't match
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gesture = gesture?.let(Gesture::getDoubleVariant)
|
||||||
|
}
|
||||||
|
|
||||||
|
// detect triangles
|
||||||
|
val startEndMin = mainPointerPath.start.min(mainPointerPath.last)
|
||||||
|
val startEndMax = mainPointerPath.start.max(mainPointerPath.last)
|
||||||
|
when (gesture) {
|
||||||
|
Gesture.SWIPE_DOWN -> {
|
||||||
|
if(startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
|
||||||
|
gesture = Gesture.SWIPE_LARGER
|
||||||
|
} else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) {
|
||||||
|
gesture = Gesture.SWIPE_SMALLER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Gesture.SWIPE_UP -> {
|
||||||
|
if(startEndMax.x + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.x) {
|
||||||
|
gesture = Gesture.SWIPE_LARGER_REVERSE
|
||||||
|
} else if (startEndMin.x - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.x) {
|
||||||
|
gesture = Gesture.SWIPE_SMALLER_REVERSE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Gesture.SWIPE_RIGHT -> {
|
||||||
|
if(startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
|
||||||
|
gesture = Gesture.SWIPE_V
|
||||||
|
} else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) {
|
||||||
|
gesture = Gesture.SWIPE_LAMBDA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Gesture.SWIPE_LEFT -> {
|
||||||
|
if(startEndMax.y + MIN_TRIANGLE_HEIGHT < mainPointerPath.max.y) {
|
||||||
|
gesture = Gesture.SWIPE_V_REVERSE
|
||||||
|
} else if (startEndMin.y - MIN_TRIANGLE_HEIGHT > mainPointerPath.min.y) {
|
||||||
|
gesture = Gesture.SWIPE_LAMBDA_REVERSE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edgeActions) {
|
||||||
|
if (mainPointerPath.max.x < edgeWidth * width) {
|
||||||
|
gesture = gesture?.getEdgeVariant(Gesture.Edge.LEFT)
|
||||||
|
} else if (mainPointerPath.min.x > (1 - edgeWidth) * width) {
|
||||||
|
gesture = gesture?.getEdgeVariant(Gesture.Edge.RIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainPointerPath.max.y < edgeWidth * height) {
|
||||||
|
gesture = gesture?.getEdgeVariant(Gesture.Edge.TOP)
|
||||||
|
} else if (mainPointerPath.min.y > (1 - edgeWidth) * height) {
|
||||||
|
gesture = gesture?.getEdgeVariant(Gesture.Edge.BOTTOM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gesture?.invoke(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Default Apps for different actions (button-press, swipes ...) -->
|
<!-- Default Apps for different actions (button-press, swipes ...) -->
|
||||||
|
<string-array name="no_default">
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<!-- Back - Apps list -->
|
<!-- Back - Apps list -->
|
||||||
<string-array name="default_back">
|
<string-array name="default_back">
|
||||||
|
|
|
@ -59,6 +59,24 @@
|
||||||
<string name="settings_gesture_description_down_left_edge">Swipe down at the left edge of the screen</string>
|
<string name="settings_gesture_description_down_left_edge">Swipe down at the left edge of the screen</string>
|
||||||
<string name="settings_gesture_down_right_edge">Down (Right Edge)</string>
|
<string name="settings_gesture_down_right_edge">Down (Right Edge)</string>
|
||||||
<string name="settings_gesture_description_down_right_edge">Swipe down at the right edge of the screen</string>
|
<string name="settings_gesture_description_down_right_edge">Swipe down at the right edge of the screen</string>
|
||||||
|
|
||||||
|
<string name="settings_gesture_swipe_larger"><![CDATA[>]]></string>
|
||||||
|
<string name="settings_gesture_description_swipe_larger">Top left -> mid right -> bottom left</string>
|
||||||
|
<string name="settings_gesture_swipe_larger_reverse"><![CDATA[> (reverse)]]></string>
|
||||||
|
<string name="settings_gesture_description_swipe_larger_reverse">Bottom left -> mid right -> top left</string>
|
||||||
|
<string name="settings_gesture_swipe_smaller"><![CDATA[<]]></string>
|
||||||
|
<string name="settings_gesture_description_swipe_smaller">Top right -> mid left -> bottom right</string>
|
||||||
|
<string name="settings_gesture_swipe_smaller_reverse"><![CDATA[< (reverse)]]></string>
|
||||||
|
<string name="settings_gesture_description_swipe_smaller_reverse">Bottom right -> mid left -> top right</string>
|
||||||
|
<string name="settings_gesture_swipe_v">V</string>
|
||||||
|
<string name="settings_gesture_description_swipe_v">Top left -> bottom mid -> top right</string>
|
||||||
|
<string name="settings_gesture_swipe_v_reverse">V (reverse)</string>
|
||||||
|
<string name="settings_gesture_description_swipe_v_reverse">Top right -> bottom mid -> top left</string>
|
||||||
|
<string name="settings_gesture_swipe_lambda">Λ</string>
|
||||||
|
<string name="settings_gesture_description_swipe_lambda">Bottom left -> top mid -> bottom right</string>
|
||||||
|
<string name="settings_gesture_swipe_lambda_reverse">Λ (reverse)</string>
|
||||||
|
<string name="settings_gesture_description_swipe_lambda_reverse">Bottom right -> top mid -> bottom left</string>
|
||||||
|
|
||||||
<string name="settings_gesture_vol_up">Volume Up</string>
|
<string name="settings_gesture_vol_up">Volume Up</string>
|
||||||
<string name="settings_gesture_description_vol_up">Press the volume up button</string>
|
<string name="settings_gesture_description_vol_up">Press the volume up button</string>
|
||||||
<string name="settings_gesture_vol_down">Volume Down</string>
|
<string name="settings_gesture_vol_down">Volume Down</string>
|
||||||
|
|
49
docs/launcher.md
Normal file
49
docs/launcher.md
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# Notable changes compared to [Finn's Launcher][original-repo]:
|
||||||
|
|
||||||
|
µLauncher is a fork of [finnmglas's app Launcher][original-repo].
|
||||||
|
Here is an incomplete list of changes:
|
||||||
|
<!--The last commit of the original project is [340ee731](https://github.com/jrpie/launcher/commit/340ee7315293b028c060638e058516435bca296a)
|
||||||
|
The first commit of µLauncher is [cc2e7710](https://github.com/jrpie/launcher/commit/cc2e7710c824549c367d97a81a1646d27c6c8993),
|
||||||
|
which at the time was still intended as a patch for launcher.
|
||||||
|
The decision to create a hard fork was made two years later.-->
|
||||||
|
|
||||||
|
|
||||||
|
- Additional gestures:
|
||||||
|
- Back
|
||||||
|
- V,Λ,<,>
|
||||||
|
- Edge gestures: There is a setting to allow distinguishing swiping at the edges of the screen from swiping in the center.
|
||||||
|
- Compatible with [work profile](https://www.android.com/enterprise/work-profile/), so apps like [Shelter](https://gitea.angry.im/PeterCxy/Shelter) can be used.
|
||||||
|
- Compatible with [private space](https://source.android.com/docs/security/features/private-space)
|
||||||
|
- Option to rename apps
|
||||||
|
- Option to hide apps
|
||||||
|
- Favorite apps
|
||||||
|
- New actions:
|
||||||
|
- Toggle Torch
|
||||||
|
- Lock screen
|
||||||
|
- The home button now works as expected.
|
||||||
|
- Improved gesture detection.
|
||||||
|
|
||||||
|
### Visual
|
||||||
|
- This app uses the system wallpaper instead of a custom solution.
|
||||||
|
- The font has been changed to [Hack][hack-font], other fonts can be selected.
|
||||||
|
- Font Awesome Icons were replaced by Material icons.
|
||||||
|
- The gear button on the home screen was removed. A smaller button is show at the top right when necessary.
|
||||||
|
|
||||||
|
|
||||||
|
### Search
|
||||||
|
- The search algorithm was modified to prefer matches at the beginning of the app name, i.e. when searching for `"te"`, `"termux"` is sorted before `"notes"`.
|
||||||
|
- The search bar was moved to the bottom of the screen.
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
- Improved gesture detection.
|
||||||
|
- Different apps set as default.
|
||||||
|
- Package name was changed to `de.jrpie.android.launcher` to avoid clashing with the original app.
|
||||||
|
- Dropped support for API < 21 (i.e. pre Lollypop)
|
||||||
|
- Fixed some bugs
|
||||||
|
- Some refactoring
|
||||||
|
|
||||||
|
|
||||||
|
The complete list of changes can be viewed [here](https://github.com/jrpie/launcher/compare/340ee731...master).
|
||||||
|
|
||||||
|
---
|
||||||
|
[original-repo]: https://github.com/finnmglas/Launcher
|
Loading…
Add table
Reference in a new issue