Skip to content

Commit

Permalink
Merge pull request #581 from jsaund/add-latency-features-to-extensions
Browse files Browse the repository at this point in the history
Add latency features to extensions
  • Loading branch information
jsaund authored May 30, 2024
2 parents 96cbc7d + c37d4a8 commit 184555c
Show file tree
Hide file tree
Showing 12 changed files with 304 additions and 55 deletions.
38 changes: 20 additions & 18 deletions CameraXExtensions/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ android {
defaultConfig {
applicationId "com.example.android.cameraxextensions"
minSdk 24
targetSdk 33
targetSdk 34
versionCode 1
versionName "1.0.0"

Expand Down Expand Up @@ -67,7 +67,7 @@ android {

dependencies {
// Kotlin lang
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.core:core-ktx:1.13.1'

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

// Image loading
implementation "io.coil-kt:coil:2.1.0"
implementation "io.coil-kt:coil:2.4.0"

// Material Components
implementation 'com.google.android.material:material:1.12.0'

// Compose
implementation 'androidx.compose.material:material:1.2.1'
implementation 'androidx.compose.ui:ui:1.2.1'
implementation 'androidx.compose.ui:ui-tooling-preview:1.2.1'
debugImplementation 'androidx.compose.ui:ui-tooling:1.2.1'
implementation 'androidx.activity:activity-compose:1.6.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'

implementation 'androidx.activity:activity-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.compose.material:material:1.6.7'
implementation 'androidx.compose.ui:ui:1.6.7'
implementation 'androidx.compose.ui:ui-tooling-preview:1.6.7'
debugImplementation 'androidx.compose.ui:ui-tooling:1.6.7'
implementation 'androidx.activity:activity-compose:1.9.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0'

implementation 'androidx.activity:activity-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.recyclerview:recyclerview:1.2.1"

implementation "androidx.recyclerview:recyclerview:1.3.2"

// Test
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ package com.example.android.cameraxextensions

import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.extensions.ExtensionMode
import androidx.core.app.ActivityCompat
import androidx.lifecycle.*
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.example.android.cameraxextensions.adapter.CameraExtensionItem
import com.example.android.cameraxextensions.model.CameraState
import com.example.android.cameraxextensions.model.CameraUiAction
Expand Down Expand Up @@ -76,6 +82,28 @@ class MainActivity : AppCompatActivity() {
// monitors changes in camera permission state
private lateinit var permissionState: MutableStateFlow<PermissionState>

private var captureUri: Uri? = null
private var progressComplete: Boolean = false

private suspend fun showCapture() {
if (captureUri == null || !progressComplete) return

cameraExtensionsViewModel.stopPreview()
captureScreenViewState.emit(
captureScreenViewState.value
.updatePostCaptureScreen {
captureUri?.let {
PostCaptureScreenViewState.PostCaptureScreenVisibleViewState(it)
} ?: PostCaptureScreenViewState.PostCaptureScreenHiddenViewState
}.updateCameraScreen {
it.hideCameraControls()
.hideProcessProgressViewState()
}
)
captureUri = null
progressComplete = false
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand Down Expand Up @@ -145,6 +173,10 @@ class MainActivity : AppCompatActivity() {
CameraUiAction.RequestPermissionClick -> {
requestPermissionsLauncher.launch(Manifest.permission.CAMERA)
}
CameraUiAction.ProcessProgressComplete -> {
progressComplete = true
showCapture()
}
is CameraUiAction.Focus -> {
cameraExtensionsViewModel.focus(action.meteringPoint)
}
Expand All @@ -169,10 +201,13 @@ class MainActivity : AppCompatActivity() {
.updateCameraScreen {
it.enableCameraShutter(true)
.enableSwitchLens(true)
.hidePostview()
}
)
}
CaptureState.CaptureReady -> {
captureUri = null
progressComplete = false
captureScreenViewState.emit(
captureScreenViewState.value
.updateCameraScreen {
Expand All @@ -191,23 +226,11 @@ class MainActivity : AppCompatActivity() {
)
}
is CaptureState.CaptureFinished -> {
cameraExtensionsViewModel.stopPreview()
captureScreenViewState.emit(
captureScreenViewState.value
.updatePostCaptureScreen {
val uri = state.outputResults.savedUri
if (uri != null) {
PostCaptureScreenViewState.PostCaptureScreenVisibleViewState(
uri
)
} else {
PostCaptureScreenViewState.PostCaptureScreenHiddenViewState
}
}
.updateCameraScreen {
it.hideCameraControls()
}
)
captureUri = state.outputResults.savedUri
if (!state.isProcessProgressSupported) {
progressComplete = true
}
showCapture()
}
is CaptureState.CaptureFailed -> {
cameraExtensionsScreen.showCaptureError("Couldn't take photo")
Expand All @@ -220,6 +243,24 @@ class MainActivity : AppCompatActivity() {
it.showCameraControls()
.enableCameraShutter(true)
.enableSwitchLens(true)
.hideProcessProgressViewState()
.hidePostview()
}
)
}
is CaptureState.CapturePostview -> {
captureScreenViewState.emit(
captureScreenViewState.value
.updateCameraScreen {
it.showPostview(state.bitmap)
}
)
}
is CaptureState.CaptureProcessProgress -> {
captureScreenViewState.emit(
captureScreenViewState.value
.updateCameraScreen {
it.showProcessProgressViewState(state.progress)
}
)
}
Expand Down Expand Up @@ -259,6 +300,7 @@ class MainActivity : AppCompatActivity() {
}
.updateCameraScreen {
it.showCameraControls()
.hidePostview()
.enableCameraShutter(false)
.enableSwitchLens(false)
}
Expand Down Expand Up @@ -299,6 +341,7 @@ class MainActivity : AppCompatActivity() {
captureScreenViewState.value
.updateCameraScreen { state ->
state.showCameraControls()
state.hidePostview()
}
.updatePostCaptureScreen {
PostCaptureScreenViewState.PostCaptureScreenHiddenViewState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ import androidx.camera.extensions.ExtensionMode
* User initiated actions related to camera operations.
*/
sealed class CameraUiAction {
object RequestPermissionClick : CameraUiAction()
object SwitchCameraClick : CameraUiAction()
object ShutterButtonClick : CameraUiAction()
object ClosePhotoPreviewClick : CameraUiAction()
data object RequestPermissionClick : CameraUiAction()
data object SwitchCameraClick : CameraUiAction()
data object ShutterButtonClick : CameraUiAction()
data object ClosePhotoPreviewClick : CameraUiAction()
data object ProcessProgressComplete : CameraUiAction()
data class SelectCameraExtension(@ExtensionMode.Mode val extension: Int) : CameraUiAction()
data class Focus(val meteringPoint: MeteringPoint) : CameraUiAction()
data class Scale(val scaleFactor: Float) : CameraUiAction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.example.android.cameraxextensions.model

import android.graphics.Bitmap
import androidx.camera.core.CameraSelector.LENS_FACING_BACK
import androidx.camera.core.CameraSelector.LensFacing
import androidx.camera.core.ImageCapture
Expand Down Expand Up @@ -74,10 +75,23 @@ sealed class CaptureState {
*/
object CaptureStarted : CaptureState()

/**
* Capture postview is ready
*/
data class CapturePostview(val bitmap: Bitmap): CaptureState()

/**
* Capture process progress updated with the [progress] value
*/
data class CaptureProcessProgress(val progress: Int): CaptureState()

/**
* Capture completed successfully.
*/
data class CaptureFinished(val outputResults: ImageCapture.OutputFileResults) : CaptureState()
data class CaptureFinished(
val outputResults: ImageCapture.OutputFileResults,
val isProcessProgressSupported: Boolean
) : CaptureState()

/**
* Capture failed with an error.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ package com.example.android.cameraxextensions.ui

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.util.TypedValue
import android.view.GestureDetector.SimpleOnGestureListener
Expand All @@ -30,6 +32,7 @@ import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.camera.view.PreviewView
import androidx.core.animation.doOnEnd
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.isVisible
import androidx.dynamicanimation.animation.DynamicAnimation
Expand All @@ -46,9 +49,11 @@ import com.example.android.cameraxextensions.model.CameraUiAction
import com.example.android.cameraxextensions.viewstate.CameraPreviewScreenViewState
import com.example.android.cameraxextensions.viewstate.CaptureScreenViewState
import com.example.android.cameraxextensions.viewstate.PostCaptureScreenViewState
import com.google.android.material.progressindicator.CircularProgressIndicator
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlin.math.max

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

private val context: Context = root.context
Expand All @@ -79,6 +85,11 @@ class CameraExtensionsScreen(private val root: View) {
private val permissionsRationale: TextView = root.findViewById(R.id.permissionsRationale)
private val permissionsRequestButton: TextView =
root.findViewById(R.id.permissionsRequestButton)
private val photoPostview: ImageView = root.findViewById(R.id.photoPostview)
private val processProgressContainer: View =
root.findViewById(R.id.processProgressContainer)
private val processProgressIndicator: CircularProgressIndicator =
root.findViewById(R.id.processProgressIndicator)

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

Expand Down Expand Up @@ -216,10 +227,50 @@ class CameraExtensionsScreen(private val root: View) {
}
}

private fun showPostview(bitmap: Bitmap) {
if (photoPostview.isVisible) return
photoPostview.isVisible = true
photoPostview.load(bitmap) {
crossfade(true)
crossfade(200)
}
}

private fun hidePostview() {
photoPostview.isVisible = false
}

private fun showProcessProgressIndicator(progress: Int) {
processProgressContainer.isVisible = true
if (progress == processProgressIndicator.progress) return

ObjectAnimator.ofInt(processProgressIndicator, "progress", progress).apply {
val currentProgress = processProgressIndicator.progress
val progressStep = max(0, progress - currentProgress)
duration = (progressStep / 100f * MAX_PROGRESS_ANIM_DURATION_MS).toLong()
doOnEnd {
if (animatedValue == 100) {
root.findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
_action.emit(CameraUiAction.ProcessProgressComplete)
}
}
}
start()
}
}

private fun hideProcessProgressIndicator() {
processProgressContainer.isVisible = false
processProgressIndicator.progress = 0
}

private fun showPhoto(uri: Uri?) {
if (uri == null) return
photoPreview.isVisible = true
photoPreview.load(uri)
photoPreview.load(uri) {
crossfade(true)
crossfade(200)
}
closePhotoPreview.isVisible = true
}

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

extensionSelector.isVisible = state.extensionsSelectorViewState.isVisible
extensionsAdapter.submitList(state.extensionsSelectorViewState.extensions)

if (state.postviewViewState.isVisible) {
showPostview(state.postviewViewState.bitmap!!)
} else {
hidePostview()
}

if (state.processProgressViewState.isVisible) {
showProcessProgressIndicator(state.processProgressViewState.progress)
} else {
hideProcessProgressIndicator()
}
}

private fun onItemClick(view: View) {
Expand Down
Loading

0 comments on commit 184555c

Please sign in to comment.