Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add latency features to extensions #581

Merged
merged 8 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading