forked from shihaam/thijooree
wheel optimizations
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.nfc.hce" android:required="false" />
|
<uses-feature android:name="android.hardware.nfc.hce" android:required="false" />
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import android.animation.ValueAnimator
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.*
|
import android.graphics.*
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.VibrationEffect
|
||||||
|
import android.os.Vibrator
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import android.view.animation.LinearInterpolator
|
||||||
import android.animation.AnimatorListenerAdapter
|
import android.animation.AnimatorListenerAdapter
|
||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
@@ -70,8 +73,9 @@ class CircularNavFragment : Fragment() {
|
|||||||
accentColor = colorPrimary
|
accentColor = colorPrimary
|
||||||
surfaceColor = colorSurface
|
surfaceColor = colorSurface
|
||||||
labelColor = colorOnSurface
|
labelColor = colorOnSurface
|
||||||
onItemClick = { navId -> (activity as? HomeActivity)?.navigateTo(navId) }
|
onItemClick = { navId -> (activity as? HomeActivity)?.navigateTo(navId) }
|
||||||
onCenterClick = { (activity as? HomeActivity)?.lockApp() }
|
onCenterClick = { /* unused: tap on unlocked center locks the wheel */ }
|
||||||
|
onWheelCenterLockedTap = { (activity as? HomeActivity)?.notifyWheelLockTap() }
|
||||||
}
|
}
|
||||||
wheelContainer.addView(wheelView)
|
wheelContainer.addView(wheelView)
|
||||||
|
|
||||||
@@ -133,6 +137,10 @@ class CircularNavFragment : Fragment() {
|
|||||||
toolbar.findViewWithTag<android.view.View>("wheel_title")?.let { toolbar.removeView(it) }
|
toolbar.findViewWithTag<android.view.View>("wheel_title")?.let { toolbar.removeView(it) }
|
||||||
requireActivity().invalidateOptionsMenu()
|
requireActivity().invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun unlockWheelLock() {
|
||||||
|
wheelView?.unlockWheel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -169,8 +177,12 @@ class CircularWheelView @JvmOverloads constructor(
|
|||||||
var labelColor: Int = Color.DKGRAY
|
var labelColor: Int = Color.DKGRAY
|
||||||
set(value) { field = value; invalidate() }
|
set(value) { field = value; invalidate() }
|
||||||
|
|
||||||
|
var isWheelLocked = false
|
||||||
|
set(value) { field = value; invalidate() }
|
||||||
|
|
||||||
var onItemClick: ((Int) -> Unit)? = null
|
var onItemClick: ((Int) -> Unit)? = null
|
||||||
var onCenterClick: (() -> Unit)? = null
|
var onCenterClick: (() -> Unit)? = null
|
||||||
|
var onWheelCenterLockedTap: (() -> Unit)? = null
|
||||||
|
|
||||||
// ---- geometry ---------------------------------------------------------
|
// ---- geometry ---------------------------------------------------------
|
||||||
|
|
||||||
@@ -195,6 +207,10 @@ class CircularWheelView @JvmOverloads constructor(
|
|||||||
|
|
||||||
private var iconBitmaps: Array<Bitmap?> = emptyArray()
|
private var iconBitmaps: Array<Bitmap?> = emptyArray()
|
||||||
private var centerBitmap: Bitmap? = null
|
private var centerBitmap: Bitmap? = null
|
||||||
|
private var centerUnlockedBitmap: Bitmap? = null
|
||||||
|
private val grayFilter = ColorMatrixColorFilter(ColorMatrix().apply { setSaturation(0f) })
|
||||||
|
private var lockShakeAngle = 0f
|
||||||
|
private var shakeAnimator: ValueAnimator? = null
|
||||||
|
|
||||||
// ---- touch & fling ----------------------------------------------------
|
// ---- touch & fling ----------------------------------------------------
|
||||||
|
|
||||||
@@ -244,7 +260,8 @@ class CircularWheelView @JvmOverloads constructor(
|
|||||||
iconBitmaps[i] = tintedBitmap(item.iconRes, accentColor, iconPx)
|
iconBitmaps[i] = tintedBitmap(item.iconRes, accentColor, iconPx)
|
||||||
}
|
}
|
||||||
val centerPx = (innerRadius * 0.64f).toInt().coerceAtLeast(1)
|
val centerPx = (innerRadius * 0.64f).toInt().coerceAtLeast(1)
|
||||||
centerBitmap = tintedBitmap(R.drawable.ic_lock, accentColor, centerPx)
|
centerBitmap = tintedBitmap(R.drawable.ic_lock, accentColor, centerPx)
|
||||||
|
centerUnlockedBitmap = tintedBitmap(R.drawable.ic_lock_open, accentColor, centerPx)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tintedBitmap(@DrawableRes resId: Int, tint: Int, sizePx: Int): Bitmap? {
|
private fun tintedBitmap(@DrawableRes resId: Int, tint: Int, sizePx: Int): Bitmap? {
|
||||||
@@ -304,8 +321,13 @@ class CircularWheelView @JvmOverloads constructor(
|
|||||||
canvas.drawCircle(cx, cy, innerRadius + dp(3f), centerRingPaint)
|
canvas.drawCircle(cx, cy, innerRadius + dp(3f), centerRingPaint)
|
||||||
centerFillPaint.color = surfaceColor
|
centerFillPaint.color = surfaceColor
|
||||||
canvas.drawCircle(cx, cy, innerRadius, centerFillPaint)
|
canvas.drawCircle(cx, cy, innerRadius, centerFillPaint)
|
||||||
centerBitmap?.let {
|
val activeCenterBitmap = if (isWheelLocked) centerBitmap else centerUnlockedBitmap
|
||||||
|
activeCenterBitmap?.let {
|
||||||
|
canvas.save()
|
||||||
|
// Shake pivots around the bottom-centre of the icon
|
||||||
|
if (lockShakeAngle != 0f) canvas.rotate(lockShakeAngle, cx, cy + it.height / 2f)
|
||||||
canvas.drawBitmap(it, cx - it.width / 2f, cy - it.height / 2f, bitmapPaint)
|
canvas.drawBitmap(it, cx - it.width / 2f, cy - it.height / 2f, bitmapPaint)
|
||||||
|
canvas.restore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +344,15 @@ class CircularWheelView @JvmOverloads constructor(
|
|||||||
canvas.save()
|
canvas.save()
|
||||||
canvas.translate(iconX, iconY)
|
canvas.translate(iconX, iconY)
|
||||||
canvas.rotate(midDeg - 90f)
|
canvas.rotate(midDeg - 90f)
|
||||||
|
if (isWheelLocked) {
|
||||||
|
bitmapPaint.colorFilter = grayFilter
|
||||||
|
bitmapPaint.alpha = 100
|
||||||
|
}
|
||||||
canvas.drawBitmap(bmp, -bmp.width / 2f, -bmp.height / 2f, bitmapPaint)
|
canvas.drawBitmap(bmp, -bmp.width / 2f, -bmp.height / 2f, bitmapPaint)
|
||||||
|
if (isWheelLocked) {
|
||||||
|
bitmapPaint.colorFilter = null
|
||||||
|
bitmapPaint.alpha = 255
|
||||||
|
}
|
||||||
canvas.restore()
|
canvas.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +364,7 @@ class CircularWheelView @JvmOverloads constructor(
|
|||||||
val textX = cx + cosA * labelRadius
|
val textX = cx + cosA * labelRadius
|
||||||
val textY = cy + sinA * labelRadius
|
val textY = cy + sinA * labelRadius
|
||||||
val label = items[index].label
|
val label = items[index].label
|
||||||
textPaint.color = labelColor
|
textPaint.color = if (isWheelLocked) (labelColor and 0x00FFFFFF) or (80 shl 24) else labelColor
|
||||||
textPaint.textAlign = Paint.Align.LEFT
|
textPaint.textAlign = Paint.Align.LEFT
|
||||||
val halfAngleDeg = Math.toDegrees((textPaint.measureText(label) / 2.0) / labelRadius).toFloat()
|
val halfAngleDeg = Math.toDegrees((textPaint.measureText(label) / 2.0) / labelRadius).toFloat()
|
||||||
val localArcRect = RectF(-labelRadius, -2f * labelRadius, labelRadius, 0f)
|
val localArcRect = RectF(-labelRadius, -2f * labelRadius, labelRadius, 0f)
|
||||||
@@ -377,18 +407,38 @@ class CircularWheelView @JvmOverloads constructor(
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
MotionEvent.ACTION_UP -> {
|
||||||
if (!isDragging) {
|
if (!isDragging) {
|
||||||
val dist = hypot(event.x - cx, event.y - cy)
|
val dist = hypot(event.x - cx, event.y - cy)
|
||||||
when {
|
when {
|
||||||
dist <= innerRadius -> onCenterClick?.invoke()
|
dist <= innerRadius -> {
|
||||||
|
if (isWheelLocked) {
|
||||||
|
onWheelCenterLockedTap?.invoke()
|
||||||
|
} else {
|
||||||
|
isWheelLocked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
dist <= outerRadius -> {
|
dist <= outerRadius -> {
|
||||||
val idx = segmentAt(event.x, event.y)
|
if (isWheelLocked) {
|
||||||
if (idx in items.indices) animateToSixOClock(idx) { onItemClick?.invoke(items[idx].navId) }
|
val idx = segmentAt(event.x, event.y)
|
||||||
|
if (idx in items.indices) animateToSixOClock(idx) {
|
||||||
|
vibrateDevice()
|
||||||
|
shakeLock()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val idx = segmentAt(event.x, event.y)
|
||||||
|
if (idx in items.indices) animateToSixOClock(idx) { onItemClick?.invoke(items[idx].navId) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val vel = computeVelocity() // degrees per millisecond
|
val vel = computeVelocity()
|
||||||
|
if (abs(vel) > 0.05f) fling(vel) else snapToNearest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_CANCEL -> {
|
||||||
|
if (isDragging) {
|
||||||
|
val vel = computeVelocity()
|
||||||
if (abs(vel) > 0.05f) fling(vel) else snapToNearest()
|
if (abs(vel) > 0.05f) fling(vel) else snapToNearest()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -483,4 +533,29 @@ class CircularWheelView @JvmOverloads constructor(
|
|||||||
start()
|
start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun vibrateDevice() {
|
||||||
|
val v = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||||
|
v.vibrate(VibrationEffect.createOneShot(60, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shakeLock() {
|
||||||
|
shakeAnimator?.cancel()
|
||||||
|
shakeAnimator = ValueAnimator.ofFloat(0f, -18f, 18f, -12f, 12f, -6f, 6f, 0f).apply {
|
||||||
|
duration = 500
|
||||||
|
interpolator = LinearInterpolator()
|
||||||
|
addUpdateListener { lockShakeAngle = it.animatedValue as Float; invalidate() }
|
||||||
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(a: Animator) { lockShakeAngle = 0f; invalidate() }
|
||||||
|
})
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unlockWheel() {
|
||||||
|
isWheelLocked = false
|
||||||
|
lockShakeAngle = 0f
|
||||||
|
shakeAnimator?.cancel()
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
private val warningRunnable = Runnable { showAutolockWarning() }
|
private val warningRunnable = Runnable { showAutolockWarning() }
|
||||||
|
|
||||||
private var isLocked = false
|
private var isLocked = false
|
||||||
|
private var pendingWheelUnlock = false
|
||||||
|
|
||||||
private val autolockRunnable = Runnable {
|
private val autolockRunnable = Runnable {
|
||||||
countdownTimer?.cancel(); countdownTimer = null
|
countdownTimer?.cancel(); countdownTimer = null
|
||||||
@@ -101,6 +102,19 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
fun lockApp() = lock()
|
fun lockApp() = lock()
|
||||||
|
|
||||||
|
fun notifyWheelLockTap() {
|
||||||
|
val securitySet = getSharedPreferences("prefs", MODE_PRIVATE)
|
||||||
|
.getString("security_method", null) != null
|
||||||
|
if (securitySet) {
|
||||||
|
pendingWheelUnlock = true
|
||||||
|
lock()
|
||||||
|
} else {
|
||||||
|
// No security configured — unlock the wheel immediately
|
||||||
|
(supportFragmentManager.findFragmentById(R.id.contentFrame) as? CircularNavFragment)
|
||||||
|
?.unlockWheelLock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun lock() {
|
private fun lock() {
|
||||||
isLocked = true
|
isLocked = true
|
||||||
startActivity(
|
startActivity(
|
||||||
@@ -520,6 +534,11 @@ fun applyNavLabelVisibility() {
|
|||||||
pauseTime = 0L
|
pauseTime = 0L
|
||||||
resetAutolockTimer()
|
resetAutolockTimer()
|
||||||
autoRefresh(CredentialStore(this))
|
autoRefresh(CredentialStore(this))
|
||||||
|
if (pendingWheelUnlock) {
|
||||||
|
pendingWheelUnlock = false
|
||||||
|
(supportFragmentManager.findFragmentById(R.id.contentFrame) as? CircularNavFragment)
|
||||||
|
?.unlockWheelLock()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// If we were away long enough to have hit the autolock timeout (e.g. while
|
// If we were away long enough to have hit the autolock timeout (e.g. while
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<!-- Shackle (open - right leg lifted free) -->
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/transparent"
|
||||||
|
android:pathData="M8.9,10V6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1"
|
||||||
|
android:strokeColor="@android:color/white"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeWidth="2.2"/>
|
||||||
|
|
||||||
|
<!-- Body + keyhole cutout -->
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M6,8H18c1.1,0 2,0.9 2,2V20c0,1.1-0.9,2-2,2H6c-1.1,0-2,-0.9-2,-2V10c0,-1.1 0.9,-2 2,-2zM12,15c-1.1,0-2,0.9-2,2s0.9,2 2,2 2,-0.9 2,-2-0.9,-2-2,-2z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
Reference in New Issue
Block a user