Skip to content

Commit 184555c

Browse files
authored
Merge pull request #581 from jsaund/add-latency-features-to-extensions
Add latency features to extensions
2 parents 96cbc7d + c37d4a8 commit 184555c

File tree

12 files changed

+304
-55
lines changed

12 files changed

+304
-55
lines changed

CameraXExtensions/app/build.gradle

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ android {
3232
defaultConfig {
3333
applicationId "com.example.android.cameraxextensions"
3434
minSdk 24
35-
targetSdk 33
35+
targetSdk 34
3636
versionCode 1
3737
versionName "1.0.0"
3838

@@ -67,7 +67,7 @@ android {
6767

6868
dependencies {
6969
// Kotlin lang
70-
implementation 'androidx.core:core-ktx:1.9.0'
70+
implementation 'androidx.core:core-ktx:1.13.1'
7171

7272
// CameraX
7373
implementation "androidx.camera:camera-core:$camerax_version"
@@ -88,25 +88,27 @@ dependencies {
8888
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
8989

9090
// Image loading
91-
implementation "io.coil-kt:coil:2.1.0"
91+
implementation "io.coil-kt:coil:2.4.0"
92+
93+
// Material Components
94+
implementation 'com.google.android.material:material:1.12.0'
9295

9396
// Compose
94-
implementation 'androidx.compose.material:material:1.2.1'
95-
implementation 'androidx.compose.ui:ui:1.2.1'
96-
implementation 'androidx.compose.ui:ui-tooling-preview:1.2.1'
97-
debugImplementation 'androidx.compose.ui:ui-tooling:1.2.1'
98-
implementation 'androidx.activity:activity-compose:1.6.0'
99-
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
100-
101-
implementation 'androidx.activity:activity-ktx:1.6.0'
102-
implementation 'androidx.appcompat:appcompat:1.5.1'
103-
implementation 'com.google.android.material:material:1.6.1'
97+
implementation 'androidx.compose.material:material:1.6.7'
98+
implementation 'androidx.compose.ui:ui:1.6.7'
99+
implementation 'androidx.compose.ui:ui-tooling-preview:1.6.7'
100+
debugImplementation 'androidx.compose.ui:ui-tooling:1.6.7'
101+
implementation 'androidx.activity:activity-compose:1.9.0'
102+
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0'
103+
104+
implementation 'androidx.activity:activity-ktx:1.9.0'
105+
implementation 'androidx.appcompat:appcompat:1.6.1'
106+
implementation 'com.google.android.material:material:1.12.0'
104107
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
105-
implementation "androidx.recyclerview:recyclerview:1.2.1"
106-
108+
implementation "androidx.recyclerview:recyclerview:1.3.2"
107109

108110
// Test
109111
testImplementation 'junit:junit:4.13.2'
110-
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
111-
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
112-
}
112+
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
113+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
114+
}

CameraXExtensions/app/src/main/java/com/example/android/cameraxextensions/MainActivity.kt

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ package com.example.android.cameraxextensions
1818

1919
import android.Manifest
2020
import android.content.pm.PackageManager
21+
import android.net.Uri
2122
import android.os.Bundle
23+
import android.util.Log
2224
import androidx.activity.OnBackPressedCallback
2325
import androidx.activity.result.contract.ActivityResultContracts
2426
import androidx.appcompat.app.AppCompatActivity
2527
import androidx.camera.extensions.ExtensionMode
2628
import androidx.core.app.ActivityCompat
27-
import androidx.lifecycle.*
29+
import androidx.lifecycle.Lifecycle
30+
import androidx.lifecycle.LifecycleOwner
31+
import androidx.lifecycle.ViewModelProvider
32+
import androidx.lifecycle.lifecycleScope
33+
import androidx.lifecycle.repeatOnLifecycle
2834
import com.example.android.cameraxextensions.adapter.CameraExtensionItem
2935
import com.example.android.cameraxextensions.model.CameraState
3036
import com.example.android.cameraxextensions.model.CameraUiAction
@@ -76,6 +82,28 @@ class MainActivity : AppCompatActivity() {
7682
// monitors changes in camera permission state
7783
private lateinit var permissionState: MutableStateFlow<PermissionState>
7884

85+
private var captureUri: Uri? = null
86+
private var progressComplete: Boolean = false
87+
88+
private suspend fun showCapture() {
89+
if (captureUri == null || !progressComplete) return
90+
91+
cameraExtensionsViewModel.stopPreview()
92+
captureScreenViewState.emit(
93+
captureScreenViewState.value
94+
.updatePostCaptureScreen {
95+
captureUri?.let {
96+
PostCaptureScreenViewState.PostCaptureScreenVisibleViewState(it)
97+
} ?: PostCaptureScreenViewState.PostCaptureScreenHiddenViewState
98+
}.updateCameraScreen {
99+
it.hideCameraControls()
100+
.hideProcessProgressViewState()
101+
}
102+
)
103+
captureUri = null
104+
progressComplete = false
105+
}
106+
79107
override fun onCreate(savedInstanceState: Bundle?) {
80108
super.onCreate(savedInstanceState)
81109

@@ -145,6 +173,10 @@ class MainActivity : AppCompatActivity() {
145173
CameraUiAction.RequestPermissionClick -> {
146174
requestPermissionsLauncher.launch(Manifest.permission.CAMERA)
147175
}
176+
CameraUiAction.ProcessProgressComplete -> {
177+
progressComplete = true
178+
showCapture()
179+
}
148180
is CameraUiAction.Focus -> {
149181
cameraExtensionsViewModel.focus(action.meteringPoint)
150182
}
@@ -169,10 +201,13 @@ class MainActivity : AppCompatActivity() {
169201
.updateCameraScreen {
170202
it.enableCameraShutter(true)
171203
.enableSwitchLens(true)
204+
.hidePostview()
172205
}
173206
)
174207
}
175208
CaptureState.CaptureReady -> {
209+
captureUri = null
210+
progressComplete = false
176211
captureScreenViewState.emit(
177212
captureScreenViewState.value
178213
.updateCameraScreen {
@@ -191,23 +226,11 @@ class MainActivity : AppCompatActivity() {
191226
)
192227
}
193228
is CaptureState.CaptureFinished -> {
194-
cameraExtensionsViewModel.stopPreview()
195-
captureScreenViewState.emit(
196-
captureScreenViewState.value
197-
.updatePostCaptureScreen {
198-
val uri = state.outputResults.savedUri
199-
if (uri != null) {
200-
PostCaptureScreenViewState.PostCaptureScreenVisibleViewState(
201-
uri
202-
)
203-
} else {
204-
PostCaptureScreenViewState.PostCaptureScreenHiddenViewState
205-
}
206-
}
207-
.updateCameraScreen {
208-
it.hideCameraControls()
209-
}
210-
)
229+
captureUri = state.outputResults.savedUri
230+
if (!state.isProcessProgressSupported) {
231+
progressComplete = true
232+
}
233+
showCapture()
211234
}
212235
is CaptureState.CaptureFailed -> {
213236
cameraExtensionsScreen.showCaptureError("Couldn't take photo")
@@ -220,6 +243,24 @@ class MainActivity : AppCompatActivity() {
220243
it.showCameraControls()
221244
.enableCameraShutter(true)
222245
.enableSwitchLens(true)
246+
.hideProcessProgressViewState()
247+
.hidePostview()
248+
}
249+
)
250+
}
251+
is CaptureState.CapturePostview -> {
252+
captureScreenViewState.emit(
253+
captureScreenViewState.value
254+
.updateCameraScreen {
255+
it.showPostview(state.bitmap)
256+
}
257+
)
258+
}
259+
is CaptureState.CaptureProcessProgress -> {
260+
captureScreenViewState.emit(
261+
captureScreenViewState.value
262+
.updateCameraScreen {
263+
it.showProcessProgressViewState(state.progress)
223264
}
224265
)
225266
}
@@ -259,6 +300,7 @@ class MainActivity : AppCompatActivity() {
259300
}
260301
.updateCameraScreen {
261302
it.showCameraControls()
303+
.hidePostview()
262304
.enableCameraShutter(false)
263305
.enableSwitchLens(false)
264306
}
@@ -299,6 +341,7 @@ class MainActivity : AppCompatActivity() {
299341
captureScreenViewState.value
300342
.updateCameraScreen { state ->
301343
state.showCameraControls()
344+
state.hidePostview()
302345
}
303346
.updatePostCaptureScreen {
304347
PostCaptureScreenViewState.PostCaptureScreenHiddenViewState

CameraXExtensions/app/src/main/java/com/example/android/cameraxextensions/model/CameraUiAction.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ import androidx.camera.extensions.ExtensionMode
2323
* User initiated actions related to camera operations.
2424
*/
2525
sealed class CameraUiAction {
26-
object RequestPermissionClick : CameraUiAction()
27-
object SwitchCameraClick : CameraUiAction()
28-
object ShutterButtonClick : CameraUiAction()
29-
object ClosePhotoPreviewClick : CameraUiAction()
26+
data object RequestPermissionClick : CameraUiAction()
27+
data object SwitchCameraClick : CameraUiAction()
28+
data object ShutterButtonClick : CameraUiAction()
29+
data object ClosePhotoPreviewClick : CameraUiAction()
30+
data object ProcessProgressComplete : CameraUiAction()
3031
data class SelectCameraExtension(@ExtensionMode.Mode val extension: Int) : CameraUiAction()
3132
data class Focus(val meteringPoint: MeteringPoint) : CameraUiAction()
3233
data class Scale(val scaleFactor: Float) : CameraUiAction()

CameraXExtensions/app/src/main/java/com/example/android/cameraxextensions/model/CameraUiState.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.example.android.cameraxextensions.model
1818

19+
import android.graphics.Bitmap
1920
import androidx.camera.core.CameraSelector.LENS_FACING_BACK
2021
import androidx.camera.core.CameraSelector.LensFacing
2122
import androidx.camera.core.ImageCapture
@@ -74,10 +75,23 @@ sealed class CaptureState {
7475
*/
7576
object CaptureStarted : CaptureState()
7677

78+
/**
79+
* Capture postview is ready
80+
*/
81+
data class CapturePostview(val bitmap: Bitmap): CaptureState()
82+
83+
/**
84+
* Capture process progress updated with the [progress] value
85+
*/
86+
data class CaptureProcessProgress(val progress: Int): CaptureState()
87+
7788
/**
7889
* Capture completed successfully.
7990
*/
80-
data class CaptureFinished(val outputResults: ImageCapture.OutputFileResults) : CaptureState()
91+
data class CaptureFinished(
92+
val outputResults: ImageCapture.OutputFileResults,
93+
val isProcessProgressSupported: Boolean
94+
) : CaptureState()
8195

8296
/**
8397
* Capture failed with an error.

CameraXExtensions/app/src/main/java/com/example/android/cameraxextensions/ui/CameraExtensionsScreen.kt

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ package com.example.android.cameraxextensions.ui
1818

1919
import android.animation.Animator
2020
import android.animation.AnimatorListenerAdapter
21+
import android.animation.ObjectAnimator
2122
import android.annotation.SuppressLint
2223
import android.content.Context
24+
import android.graphics.Bitmap
2325
import android.net.Uri
2426
import android.util.TypedValue
2527
import android.view.GestureDetector.SimpleOnGestureListener
@@ -30,6 +32,7 @@ import android.widget.ImageView
3032
import android.widget.TextView
3133
import android.widget.Toast
3234
import androidx.camera.view.PreviewView
35+
import androidx.core.animation.doOnEnd
3336
import androidx.core.view.GestureDetectorCompat
3437
import androidx.core.view.isVisible
3538
import androidx.dynamicanimation.animation.DynamicAnimation
@@ -46,9 +49,11 @@ import com.example.android.cameraxextensions.model.CameraUiAction
4649
import com.example.android.cameraxextensions.viewstate.CameraPreviewScreenViewState
4750
import com.example.android.cameraxextensions.viewstate.CaptureScreenViewState
4851
import com.example.android.cameraxextensions.viewstate.PostCaptureScreenViewState
52+
import com.google.android.material.progressindicator.CircularProgressIndicator
4953
import kotlinx.coroutines.flow.Flow
5054
import kotlinx.coroutines.flow.MutableSharedFlow
5155
import kotlinx.coroutines.launch
56+
import kotlin.math.max
5257

5358
/**
5459
* Displays the camera preview and captured photo.
@@ -63,6 +68,7 @@ class CameraExtensionsScreen(private val root: View) {
6368
private const val SPRING_STIFFNESS_ALPHA_OUT = 100f
6469
private const val SPRING_STIFFNESS = 800f
6570
private const val SPRING_DAMPING_RATIO = 0.35f
71+
private const val MAX_PROGRESS_ANIM_DURATION_MS = 3000
6672
}
6773

6874
private val context: Context = root.context
@@ -79,6 +85,11 @@ class CameraExtensionsScreen(private val root: View) {
7985
private val permissionsRationale: TextView = root.findViewById(R.id.permissionsRationale)
8086
private val permissionsRequestButton: TextView =
8187
root.findViewById(R.id.permissionsRequestButton)
88+
private val photoPostview: ImageView = root.findViewById(R.id.photoPostview)
89+
private val processProgressContainer: View =
90+
root.findViewById(R.id.processProgressContainer)
91+
private val processProgressIndicator: CircularProgressIndicator =
92+
root.findViewById(R.id.processProgressIndicator)
8293

8394
val previewView: PreviewView = root.findViewById(R.id.previewView)
8495

@@ -216,10 +227,50 @@ class CameraExtensionsScreen(private val root: View) {
216227
}
217228
}
218229

230+
private fun showPostview(bitmap: Bitmap) {
231+
if (photoPostview.isVisible) return
232+
photoPostview.isVisible = true
233+
photoPostview.load(bitmap) {
234+
crossfade(true)
235+
crossfade(200)
236+
}
237+
}
238+
239+
private fun hidePostview() {
240+
photoPostview.isVisible = false
241+
}
242+
243+
private fun showProcessProgressIndicator(progress: Int) {
244+
processProgressContainer.isVisible = true
245+
if (progress == processProgressIndicator.progress) return
246+
247+
ObjectAnimator.ofInt(processProgressIndicator, "progress", progress).apply {
248+
val currentProgress = processProgressIndicator.progress
249+
val progressStep = max(0, progress - currentProgress)
250+
duration = (progressStep / 100f * MAX_PROGRESS_ANIM_DURATION_MS).toLong()
251+
doOnEnd {
252+
if (animatedValue == 100) {
253+
root.findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
254+
_action.emit(CameraUiAction.ProcessProgressComplete)
255+
}
256+
}
257+
}
258+
start()
259+
}
260+
}
261+
262+
private fun hideProcessProgressIndicator() {
263+
processProgressContainer.isVisible = false
264+
processProgressIndicator.progress = 0
265+
}
266+
219267
private fun showPhoto(uri: Uri?) {
220268
if (uri == null) return
221269
photoPreview.isVisible = true
222-
photoPreview.load(uri)
270+
photoPreview.load(uri) {
271+
crossfade(true)
272+
crossfade(200)
273+
}
223274
closePhotoPreview.isVisible = true
224275
}
225276

@@ -237,6 +288,18 @@ class CameraExtensionsScreen(private val root: View) {
237288

238289
extensionSelector.isVisible = state.extensionsSelectorViewState.isVisible
239290
extensionsAdapter.submitList(state.extensionsSelectorViewState.extensions)
291+
292+
if (state.postviewViewState.isVisible) {
293+
showPostview(state.postviewViewState.bitmap!!)
294+
} else {
295+
hidePostview()
296+
}
297+
298+
if (state.processProgressViewState.isVisible) {
299+
showProcessProgressIndicator(state.processProgressViewState.progress)
300+
} else {
301+
hideProcessProgressIndicator()
302+
}
240303
}
241304

242305
private fun onItemClick(view: View) {

0 commit comments

Comments
 (0)