Skip to content

Commit e4988a2

Browse files
authored
Merge pull request #543 from code-payments/release/2.1.9
release 2.1.9 refinements
2 parents a108abe + 5d546dc commit e4988a2

File tree

10 files changed

+242
-161
lines changed

10 files changed

+242
-161
lines changed

api/src/main/java/com/getcode/model/Feature.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ data class CameraGesturesFeature(
4848
override val available: Boolean = true, // always available
4949
): Feature
5050

51+
data class InvertedDragZoomFeature(
52+
override val enabled: Boolean = BetaOptions.Defaults.invertedDragZoom,
53+
override val available: Boolean = true, // always available
54+
): Feature
55+
5156
data class FlippableTipCardFeature(
5257
override val enabled: Boolean = BetaOptions.Defaults.canFlipTipCard,
5358
override val available: Boolean = true, // always available

api/src/main/java/com/getcode/model/PrefBool.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package com.getcode.model
44

55
import androidx.room.Entity
66
import androidx.room.PrimaryKey
7+
import dagger.internal.Beta
78

89
@Entity
910
data class PrefBool(
@@ -61,7 +62,8 @@ sealed class PrefsBool(val value: String) {
6162
data object SHARE_TWEET_TO_TIP : PrefsBool("share_tweet_to_tip"), BetaFlag, Immutable
6263
data object TIP_CARD_ON_HOMESCREEN: PrefsBool("tip_card_on_home_screen"), BetaFlag, Immutable
6364
data object TIP_CARD_FLIPPABLE: PrefsBool("tipcard_flippable"), BetaFlag
64-
data object CAMERA_GESTURES_ENABLED: PrefsBool("camera_gestures_enabled"), BetaFlag
65+
data object CAMERA_GESTURES_ENABLED: PrefsBool("camera_gestures_enabled"), BetaFlag, Immutable
66+
data object CAMERA_DRAG_INVERTED: PrefsBool("camera_drag_inverted"), BetaFlag
6567
}
6668

6769
val APP_SETTINGS: List<AppSetting> = listOf(PrefsBool.CAMERA_START_BY_DEFAULT, PrefsBool.REQUIRE_BIOMETRICS)

api/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ data class BetaOptions(
2222
val shareTweetToTip: Boolean,
2323
val tipCardOnHomeScreen: Boolean,
2424
val cameraGesturesEnabled: Boolean,
25+
val invertedDragZoom: Boolean,
2526
val canFlipTipCard: Boolean,
2627
) {
2728
companion object {
@@ -43,6 +44,7 @@ data class BetaOptions(
4344
shareTweetToTip = true,
4445
tipCardOnHomeScreen = true,
4546
cameraGesturesEnabled = true,
47+
invertedDragZoom = false,
4648
canFlipTipCard = false
4749
)
4850
}
@@ -82,6 +84,7 @@ class BetaFlagsRepository @Inject constructor(
8284
observeBetaFlag(PrefsBool.SHARE_TWEET_TO_TIP, default = defaults.shareTweetToTip),
8385
observeBetaFlag(PrefsBool.TIP_CARD_ON_HOMESCREEN, defaults.tipCardOnHomeScreen),
8486
observeBetaFlag(PrefsBool.CAMERA_GESTURES_ENABLED, defaults.cameraGesturesEnabled),
87+
observeBetaFlag(PrefsBool.CAMERA_DRAG_INVERTED, defaults.invertedDragZoom),
8588
observeBetaFlag(PrefsBool.TIP_CARD_FLIPPABLE, defaults.canFlipTipCard)
8689
) {
8790
BetaOptions(
@@ -101,7 +104,8 @@ class BetaFlagsRepository @Inject constructor(
101104
shareTweetToTip = it[13],
102105
tipCardOnHomeScreen = it[14],
103106
cameraGesturesEnabled = it[15],
104-
canFlipTipCard = it[16],
107+
invertedDragZoom = it[16],
108+
canFlipTipCard = it[17],
105109
)
106110
}
107111
}

api/src/main/java/com/getcode/network/repository/FeatureRepository.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.getcode.model.TipCardOnHomeScreenFeature
1010
import com.getcode.model.ConversationCashFeature
1111
import com.getcode.model.ConversationsFeature
1212
import com.getcode.model.FlippableTipCardFeature
13+
import com.getcode.model.InvertedDragZoomFeature
1314
import kotlinx.coroutines.flow.combine
1415
import kotlinx.coroutines.flow.map
1516
import javax.inject.Inject
@@ -33,6 +34,7 @@ class FeatureRepository @Inject constructor(
3334
val conversationsCash = betaFlags.observe().map { ConversationCashFeature(it.conversationCashEnabled) }
3435

3536
val cameraGestures = betaFlags.observe().map { CameraGesturesFeature(it.cameraGesturesEnabled) }
37+
val invertedDragZoom = betaFlags.observe().map { InvertedDragZoomFeature(it.invertedDragZoom) }
3638

3739
val requestKin = betaFlags.observe().map { RequestKinFeature(it.giveRequestsEnabled) }
3840

app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ fun BetaFlagsScreen(
6060
stringResource(id = R.string.beta_camera_gestures_description),
6161
state.cameraGesturesEnabled,
6262
),
63+
BetaFeature(
64+
PrefsBool.CAMERA_DRAG_INVERTED,
65+
R.string.beta_camera_invert_drag,
66+
stringResource(id = R.string.beta_camera_invert_drag_description),
67+
state.invertedDragZoom,
68+
),
6369
BetaFeature(
6470
PrefsBool.TIP_CARD_FLIPPABLE,
6571
R.string.beta_tipcard_can_flip,
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package com.getcode.view.main.camera
2+
3+
import android.content.Context
4+
import android.os.Handler
5+
import android.os.Looper
6+
import android.view.GestureDetector
7+
import android.view.MotionEvent
8+
import android.view.ScaleGestureDetector
9+
import androidx.camera.core.CameraControl
10+
import androidx.camera.core.CameraInfo
11+
import androidx.camera.core.FocusMeteringAction
12+
import androidx.camera.core.MeteringPoint
13+
import androidx.compose.ui.geometry.Offset
14+
import java.util.concurrent.TimeUnit
15+
import kotlin.math.pow
16+
17+
internal class CameraGestureController(
18+
context: Context,
19+
invertedDragEnabled: Boolean,
20+
private val gesturesEnabled: Boolean,
21+
private val cameraControl: CameraControl,
22+
private val cameraInfo: CameraInfo,
23+
onTap: (Offset) -> MeteringPoint,
24+
) {
25+
private val handler = Handler(Looper.getMainLooper())
26+
private var shouldIgnoreScroll = false
27+
private var resetIgnore: Runnable? = null
28+
private var initialZoomLevel = 0f
29+
private var accumulatedDelta = 0f
30+
31+
// Pinch-to-zoom gesture detector
32+
private val scaleGestureDetector = ScaleGestureDetector(
33+
context,
34+
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
35+
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
36+
shouldIgnoreScroll = true
37+
resetIgnore?.let { handler.removeCallbacks(it) }
38+
return true
39+
}
40+
41+
override fun onScale(detector: ScaleGestureDetector): Boolean {
42+
val currentZoomRatio = cameraInfo.zoomState.value?.zoomRatio ?: 1f
43+
val delta = detector.scaleFactor
44+
val newZoomRatio = currentZoomRatio * delta
45+
46+
// Clamp the new zoom ratio between the minimum and maximum zoom ratio
47+
val clampedZoomRatio = newZoomRatio.coerceIn(
48+
cameraInfo.zoomState.value?.minZoomRatio ?: 1f,
49+
cameraInfo.zoomState.value?.maxZoomRatio ?: currentZoomRatio
50+
)
51+
52+
// Apply the zoom to the camera control
53+
cameraControl.setZoomRatio(clampedZoomRatio)
54+
return true
55+
}
56+
57+
override fun onScaleEnd(detector: ScaleGestureDetector) {
58+
initialZoomLevel = cameraInfo.zoomState.value?.zoomRatio ?: 1f
59+
resetIgnore = Runnable { shouldIgnoreScroll = false }
60+
resetIgnore?.let { handler.postDelayed(it, 500) }
61+
}
62+
})
63+
64+
// Gesture detector for tap and drag-to-zoom
65+
private val gestureDetector = GestureDetector(
66+
context,
67+
object : GestureDetector.OnGestureListener {
68+
override fun onDown(e: MotionEvent): Boolean {
69+
initialZoomLevel = cameraInfo.zoomState.value?.zoomRatio ?: 1f
70+
accumulatedDelta = 0f
71+
return true
72+
}
73+
74+
override fun onSingleTapUp(event: MotionEvent): Boolean {
75+
val point = onTap(Offset(event.x, event.y))
76+
val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF)
77+
.setAutoCancelDuration(5, TimeUnit.SECONDS)
78+
.build()
79+
80+
cameraControl.startFocusAndMetering(action)
81+
return true
82+
}
83+
84+
override fun onScroll(
85+
e1: MotionEvent?,
86+
e2: MotionEvent,
87+
distanceX: Float,
88+
distanceY: Float
89+
): Boolean {
90+
if (!shouldIgnoreScroll) {
91+
accumulatedDelta = if (invertedDragEnabled) {
92+
accumulatedDelta + distanceY * 0.5f
93+
} else {
94+
accumulatedDelta - distanceY * 0.5f
95+
}
96+
97+
val zoomDelta = ease(
98+
value = accumulatedDelta,
99+
fromRange = 0f..250f,
100+
toRange = 0f..10f,
101+
easeIn = true,
102+
easeOut = false
103+
)
104+
105+
val maxZoom = cameraInfo.zoomState.value?.maxZoomRatio ?: 1f
106+
val minZoom = cameraInfo.zoomState.value?.minZoomRatio ?: 1f
107+
108+
val newZoom = (initialZoomLevel + zoomDelta).coerceIn(minZoom, maxZoom)
109+
cameraControl.setZoomRatio(newZoom)
110+
}
111+
return true
112+
}
113+
114+
override fun onShowPress(e: MotionEvent) {}
115+
override fun onLongPress(e: MotionEvent) {}
116+
override fun onFling(
117+
e1: MotionEvent?,
118+
e2: MotionEvent,
119+
velocityX: Float,
120+
velocityY: Float
121+
): Boolean {
122+
return false
123+
}
124+
}
125+
)
126+
127+
fun onTouchEvent(event: MotionEvent) {
128+
if (gesturesEnabled) {
129+
scaleGestureDetector.onTouchEvent(event)
130+
gestureDetector.onTouchEvent(event)
131+
132+
if (event.action == MotionEvent.ACTION_UP) {
133+
animateZoomReset(cameraInfo, cameraControl)
134+
initialZoomLevel = cameraInfo.zoomState.value?.zoomRatio ?: 1f
135+
}
136+
}
137+
}
138+
139+
private fun animateZoomReset(cameraInfo: CameraInfo?, cameraControl: CameraControl?) {
140+
val durationMs = 300L
141+
val frameInterval = 16L
142+
val maxSteps = durationMs / frameInterval
143+
val currentZoomLevel = cameraInfo?.zoomState?.value?.linearZoom ?: 0f
144+
145+
val decrement = currentZoomLevel / maxSteps
146+
147+
var currentStep = 0L
148+
handler.post(object : Runnable {
149+
override fun run() {
150+
if (currentStep < maxSteps) {
151+
val newZoomLevel = currentZoomLevel - (decrement * currentStep)
152+
cameraControl?.setLinearZoom(newZoomLevel.coerceIn(0f, 1f))
153+
currentStep++
154+
handler.postDelayed(this, frameInterval)
155+
} else {
156+
cameraControl?.setLinearZoom(0f)
157+
}
158+
}
159+
})
160+
}
161+
162+
private fun ease(
163+
value: Float,
164+
fromRange: ClosedFloatingPointRange<Float>,
165+
toRange: ClosedFloatingPointRange<Float>,
166+
easeIn: Boolean,
167+
easeOut: Boolean
168+
): Float {
169+
val normalizedValue = (value - fromRange.start) / (fromRange.endInclusive - fromRange.start)
170+
171+
val easedValue: Float = if (easeIn && easeOut) {
172+
if (normalizedValue < 0.5f) {
173+
4 * normalizedValue * normalizedValue * normalizedValue
174+
} else {
175+
1 - (-2 * normalizedValue + 2).toDouble().pow(3.0).toFloat() / 2
176+
}
177+
} else if (easeIn) {
178+
normalizedValue * normalizedValue * normalizedValue
179+
} else if (easeOut) {
180+
1 - (1 - normalizedValue).toDouble().pow(3.0).toFloat()
181+
} else {
182+
normalizedValue
183+
}
184+
185+
return easedValue * (toRange.endInclusive - toRange.start) + toRange.start
186+
}
187+
}

0 commit comments

Comments
 (0)