From 1c7df819325faf962d26c334f406bc1a192b22ad Mon Sep 17 00:00:00 2001 From: CharcoalChen Date: Thu, 5 Oct 2023 17:10:55 +0800 Subject: [PATCH] [skip ci] Add UI design to show the capture progress related info provided by Camera2Extensions API 34 1. Update compileSdk as 34 2. Add UI to show capture progress related info when the following features are supported - StillCaptureLatency - Still capture processing progress - Postview image When the capture progress related UI is shown, the mode switch, capture button and strength slide controls will be disabled. 3. Add a slider for adjusting the extension strength when it is supported - The slider becomes visibile after retrieving the initial strength value from capture result 4. Add utils project dependence for YuvToRgbConvertor - All files under the utils folder are copied from the CameraXVideo project folder 5. Update the android build tools to version 7.4.2 --- Camera2Extensions/app/build.gradle | 4 +- .../extensions/fragments/CameraFragment.kt | 535 ++++++++++++++++-- .../main/res/drawable/ic_progress_info_bg.xml | 13 + .../src/main/res/layout/fragment_camera.xml | 90 +++ .../app/src/main/res/values/dimens.xml | 7 + .../app/src/main/res/values/strings.xml | 6 +- .../app/src/main/res/values/styles.xml | 2 +- Camera2Extensions/build.gradle | 6 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 59536 bytes .../gradle/wrapper/gradle-wrapper.properties | 3 +- Camera2Extensions/gradlew | 282 +++++---- Camera2Extensions/gradlew.bat | 43 +- Camera2Extensions/settings.gradle | 3 +- Camera2Extensions/utils/README.md | 2 + Camera2Extensions/utils/build.gradle | 73 +++ .../utils/src/main/AndroidManifest.xml | 17 + .../camera/utils/AutoFitSurfaceView.kt | 79 +++ .../android/camera/utils/CameraSizes.kt | 79 +++ .../example/android/camera/utils/ExifUtils.kt | 73 +++ .../camera/utils/GenericListAdapter.kt | 55 ++ .../camera/utils/OrientationLiveData.kt | 95 ++++ .../com/example/android/camera/utils/Yuv.kt | 191 +++++++ .../android/camera/utils/YuvToRgbConverter.kt | 99 ++++ .../src/main/res/drawable/ic_shutter.xml | 21 + .../main/res/drawable/ic_shutter_focused.xml | 28 + .../main/res/drawable/ic_shutter_normal.xml | 28 + .../main/res/drawable/ic_shutter_pressed.xml | 28 + 27 files changed, 1681 insertions(+), 181 deletions(-) create mode 100644 Camera2Extensions/app/src/main/res/drawable/ic_progress_info_bg.xml create mode 100644 Camera2Extensions/utils/README.md create mode 100644 Camera2Extensions/utils/build.gradle create mode 100644 Camera2Extensions/utils/src/main/AndroidManifest.xml create mode 100644 Camera2Extensions/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt create mode 100644 Camera2Extensions/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt create mode 100644 Camera2Extensions/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt create mode 100644 Camera2Extensions/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt create mode 100644 Camera2Extensions/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt create mode 100644 Camera2Extensions/utils/src/main/java/com/example/android/camera/utils/Yuv.kt create mode 100644 Camera2Extensions/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt create mode 100644 Camera2Extensions/utils/src/main/res/drawable/ic_shutter.xml create mode 100644 Camera2Extensions/utils/src/main/res/drawable/ic_shutter_focused.xml create mode 100644 Camera2Extensions/utils/src/main/res/drawable/ic_shutter_normal.xml create mode 100644 Camera2Extensions/utils/src/main/res/drawable/ic_shutter_pressed.xml diff --git a/Camera2Extensions/app/build.gradle b/Camera2Extensions/app/build.gradle index 2ecfcfa1..b3b9bc32 100644 --- a/Camera2Extensions/app/build.gradle +++ b/Camera2Extensions/app/build.gradle @@ -20,10 +20,10 @@ apply plugin: 'kotlin-kapt' apply plugin: "androidx.navigation.safeargs" android { - compileSdkVersion 'android-33' defaultConfig { testInstrumentationRunner kotlin_version applicationId "com.android.example.camera2.extensions" + compileSdk 34 minSdkVersion 31 targetSdkVersion 33 versionCode 1 @@ -65,6 +65,7 @@ android { dependencies { implementation "androidx.activity:activity-ktx:1.2.3" implementation "androidx.fragment:fragment-ktx:1.3.5" + implementation(project(":utils")) // Kotlin lang implementation 'androidx.core:core-ktx:1.6.0' @@ -76,6 +77,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'com.google.android.material:material:1.6.0' // Navigation library def nav_version = "2.3.5" diff --git a/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/fragments/CameraFragment.kt b/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/fragments/CameraFragment.kt index 1cf82947..07d2ce26 100644 --- a/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/fragments/CameraFragment.kt +++ b/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/fragments/CameraFragment.kt @@ -20,6 +20,7 @@ import android.annotation.SuppressLint import android.content.Context import android.graphics.* import android.hardware.camera2.* +import android.hardware.camera2.CameraExtensionSession.StillCaptureLatency import android.hardware.camera2.params.ExtensionSessionConfiguration import android.hardware.camera2.params.MeteringRectangle import android.hardware.camera2.params.OutputConfiguration @@ -33,21 +34,41 @@ import android.widget.Toast import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs +import com.example.android.camera.utils.YuvToRgbConverter import com.example.android.camera2.extensions.R import com.example.android.camera2.extensions.ZoomUtil import com.example.android.camera2.extensions.databinding.FragmentCameraBinding -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.asExecutor -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine +import com.google.android.material.slider.Slider import java.io.File import java.io.FileOutputStream import java.io.OutputStream import java.nio.ByteBuffer +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit import java.util.stream.Collectors import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.math.abs +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * The still capture progress is in DONE state. + */ +private const val PROGRESS_STATE_DONE = 0 +/** + * The still capture progress is in HOLD_STILL state which should show a message to request the end + * users to hold still. + */ +private const val PROGRESS_STATE_HOLD_STILL = 1 +/** + * The still capture progress is in STILL_PROCESSING state which should show a message to let the + * end users know the still image is under processing. + */ +private const val PROGRESS_STATE_STILL_PROCESSING = 2 /* * This is the main camera fragment where all camera extension logic can be found. @@ -120,6 +141,107 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { private var zoomRatio: Float = ZoomUtil.minZoom() + /** + * Track the extension strength support for current extension mode. + */ + private var isExtensionStrengthAvailable = false + + /** + * Track the extension strength setting. + */ + private var extensionStrength = -1 + /** + * Track the postview support for current extension mode. + */ + private var isPostviewAvailable = false + /** + * Track the capture process progress support for current extension mode. + */ + private var isCaptureProcessProgressAvailable = false + /** + * A reference to the image reader to receive the postview when it can be supported for current + * extension mode. + */ + private var postviewImageReader: ImageReader? = null + /** + * A ScheduledFuture for repeatedly updating the capture progress info. + */ + private var progressInfoScheduledFuture: ScheduledFuture<*>? = null + /** + * Track the process state of current still capture request. + */ + private var progressState = PROGRESS_STATE_DONE + /** + * Track the still capture latency of current still capture request. + */ + private var stillCaptureLatency: StillCaptureLatency? = null + /** + * Track the HOLD_STILL or STILL_PROCESSING start timestamp of current still capture request. + */ + private var progressStartTimestampMs: Long = 0 + /** + * Track the capture processing progress of current still capture request. + */ + private var captureProcessingProgress = -1 + + /** + * Calculates the remaining duration for the latency of current state. + * + * The duration is calculated against the start timestamp which was stored when current process + * state was begun. + */ + private fun calculateRemainingDurationInMs(): Long { + // Only supported when API level is 34 or above + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return 0 + } + + // Only supported when capture latency info is provided + if (stillCaptureLatency == null) { + return 0 + } + + val currentTimestampMs = SystemClock.elapsedRealtime() + val pastTimeMs = currentTimestampMs - progressStartTimestampMs + val remainingTimeMs = if (progressState == PROGRESS_STATE_HOLD_STILL) { + stillCaptureLatency!!.captureLatency - pastTimeMs + } else { + stillCaptureLatency!!.processingLatency - pastTimeMs + } + + return if (remainingTimeMs > 0) remainingTimeMs else 0 + } + + /** + * Calculates the process progress for the latency of current state. + * + * The progress is calculated against the total latency duration of current state. + */ + private fun calculateProgressByRemainingDuration(): Int { + // Only supported when API level is 34 or above + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return 0 + } + + // Only supported when capture latency info is provided + if (stillCaptureLatency == null) { + return 0 + } + + val currentTimestampMs = SystemClock.elapsedRealtime() + val pastTimeMs = currentTimestampMs - progressStartTimestampMs + return (if (progressState == PROGRESS_STATE_HOLD_STILL) { + pastTimeMs * 100 / stillCaptureLatency!!.captureLatency + } else { + pastTimeMs * 100 / stillCaptureLatency!!.processingLatency + }).toInt() + } + + /** + * Lens facing of the working camera + */ + private var lensFacing = CameraCharacteristics.LENS_FACING_BACK + /** * Gesture detector used for tap to focus */ @@ -171,6 +293,14 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { request: CaptureRequest ) { Log.v(TAG, "onCaptureProcessStarted") + // Turns to STILL_PROCESSING stage when the request tag is STILL_CAPTURE_TAG + if (request.tag == STILL_CAPTURE_TAG && progressState == PROGRESS_STATE_HOLD_STILL) { + progressState = PROGRESS_STATE_STILL_PROCESSING + progressStartTimestampMs = SystemClock.elapsedRealtime() + requireActivity().runOnUiThread { + binding.progressState?.text = getString(R.string.state_still_processing) + } + } } override fun onCaptureResultAvailable( @@ -199,6 +329,23 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { queueAutoFocusReset() } + + // The initial extension strength value will be provided by the capture results. Checks it + // to set and show the slider for end users to adjust their preferred strength setting. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && + isExtensionStrengthAvailable && extensionStrength == -1 && + result.keys.contains(CaptureResult.EXTENSION_STRENGTH) + ) { + result.get(CaptureResult.EXTENSION_STRENGTH)?.let { + extensionStrength = it + requireActivity().runOnUiThread { + binding.strengthSlider?.apply { + value = extensionStrength.toFloat() + visibility = View.VISIBLE + } + } + } + } } override fun onCaptureFailed( @@ -206,6 +353,7 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { request: CaptureRequest ) { Log.v(TAG, "onCaptureProcessFailed") + hideCaptureProgressUI() } override fun onCaptureSequenceCompleted( @@ -220,6 +368,45 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { sequenceId: Int ) { Log.v(TAG, "onCaptureProcessSequenceAborted: $sequenceId") + hideCaptureProgressUI() + } + + override fun onCaptureProcessProgressed( + session: CameraExtensionSession, + request: CaptureRequest, + progress: Int, + ) { + // Caches current processing progress and updates the progress info + captureProcessingProgress = progress + updateProgressInfo() + } + } + + /** + * The slide OnChangeListener implementation for receiving the value change and submitting the + * request to change the extension strength setting. + */ + private val sliderOnChangeListener: Slider.OnChangeListener = + object : Slider.OnChangeListener { + override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { + if (!fromUser || Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return + } + + extensionStrength = value.toInt() + submitRequest( + CameraDevice.TEMPLATE_PREVIEW, + previewSurface, + true, + ) { builder -> + builder.apply { + set( + CaptureRequest.EXTENSION_STRENGTH, + extensionStrength + ) + Log.d(TAG, "submit request for extension strength: $extensionStrength") + } + } } } @@ -281,10 +468,14 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { extensionCharacteristics = cameraManager.getCameraExtensionCharacteristics(args.cameraId) characteristics = cameraManager.getCameraCharacteristics(args.cameraId) + lensFacing = characteristics[CameraCharacteristics.LENS_FACING]!! supportedExtensions.addAll(extensionCharacteristics.supportedExtensions) if (currentExtension == -1) { currentExtension = supportedExtensions[0] currentExtensionIdx = 0 + refreshStrengthAndCaptureProgressAvailabilityInfo() + binding.strengthSlider?.visibility = View.GONE + extensionStrength = -1 binding.switchButton.text = getExtensionLabel(currentExtension) } @@ -293,7 +484,10 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { lifecycleScope.launch(Dispatchers.IO) { currentExtensionIdx = (currentExtensionIdx + 1) % supportedExtensions.size currentExtension = supportedExtensions[currentExtensionIdx] + refreshStrengthAndCaptureProgressAvailabilityInfo() + extensionStrength = -1 requireActivity().runOnUiThread { + binding.strengthSlider?.visibility = View.GONE binding.switchButton.text = getExtensionLabel(currentExtension) restartPreview = true } @@ -319,6 +513,23 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { true } + + // Sets strength slider change listener + binding.strengthSlider?.addOnChangeListener(sliderOnChangeListener) + } + + /** + * Refreshes the extension strength and capture progress related availability info. + */ + private fun refreshStrengthAndCaptureProgressAvailabilityInfo() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + isExtensionStrengthAvailable = + extensionCharacteristics.getAvailableCaptureRequestKeys(currentExtension) + .contains(CaptureRequest.EXTENSION_STRENGTH) + isPostviewAvailable = extensionCharacteristics.isPostviewAvailable(currentExtension) + isCaptureProcessProgressAvailable = + extensionCharacteristics.isCaptureProcessProgressAvailable(currentExtension) + } } /** @@ -435,44 +646,12 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { if (!binding.texture.isAvailable) { return } - val texture = binding.texture.surfaceTexture + previewSize = pickPreviewResolution(cameraManager, args.cameraId) - texture?.setDefaultBufferSize(previewSize.width, previewSize.height) - previewSurface = Surface(texture) - val yuvColorEncodingSystemSizes = extensionCharacteristics.getExtensionSupportedSizes( - currentExtension, ImageFormat.YUV_420_888 - ) - val jpegSizes = extensionCharacteristics.getExtensionSupportedSizes( - currentExtension, ImageFormat.JPEG - ) - val stillFormat = if (jpegSizes.isEmpty()) ImageFormat.YUV_420_888 else ImageFormat.JPEG - val stillCaptureSize = if (jpegSizes.isEmpty()) yuvColorEncodingSystemSizes[0] else jpegSizes[0] - stillImageReader = ImageReader.newInstance( - stillCaptureSize.width, - stillCaptureSize.height, stillFormat, 1 - ) - stillImageReader.setOnImageAvailableListener( - { reader: ImageReader -> - var output: OutputStream - try { - reader.acquireLatestImage().use { image -> - val file = File( - requireActivity().getExternalFilesDir(null), - if (image.format == ImageFormat.JPEG) "frame.jpg" else "frame.yuv" - ) - output = FileOutputStream(file) - output.write(getDataFromImage(image)) - output.close() - Toast.makeText( - requireActivity(), "Frame saved at: " + file.path, - Toast.LENGTH_SHORT - ).show() - } - } catch (e: Exception) { - e.printStackTrace() - } - }, storeHandler - ) + previewSurface = createPreviewSurface(previewSize) + stillImageReader = createStillImageReader() + postviewImageReader = createPostviewImageReader() + val outputConfig = ArrayList() outputConfig.add(OutputConfiguration(stillImageReader.surface)) outputConfig.add(OutputConfiguration(previewSurface)) @@ -482,6 +661,7 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { override fun onClosed(session: CameraExtensionSession) { if (restartPreview) { stillImageReader.close() + postviewImageReader?.close() restartPreview = false startPreview() } else { @@ -512,6 +692,12 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { } } ) + // Adds postview image reader surface to extension session configuration if it is supported. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + postviewImageReader?.let { + extensionConfiguration.postviewOutputConfiguration = OutputConfiguration(it.surface) + } + } try { cameraDevice.createExtensionSession(extensionConfiguration) } catch (e: CameraAccessException) { @@ -523,18 +709,270 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { } } + /** + * Creates the preview surface + */ + private fun createPreviewSurface(previewSize: Size): Surface { + val texture = binding.texture.surfaceTexture + texture?.setDefaultBufferSize(previewSize.width, previewSize.height) + return Surface(texture) + } + + /** + * Creates the still image reader and sets up OnImageAvailableListener + */ + private fun createStillImageReader(): ImageReader { + val yuvColorEncodingSystemSizes = extensionCharacteristics.getExtensionSupportedSizes( + currentExtension, ImageFormat.YUV_420_888 + ) + val jpegSizes = extensionCharacteristics.getExtensionSupportedSizes( + currentExtension, ImageFormat.JPEG + ) + val stillFormat = if (jpegSizes.isEmpty()) ImageFormat.YUV_420_888 else ImageFormat.JPEG + val stillCaptureSize = if (jpegSizes.isEmpty()) yuvColorEncodingSystemSizes[0] else jpegSizes[0] + val stillImageReader = ImageReader.newInstance( + stillCaptureSize.width, + stillCaptureSize.height, stillFormat, 1 + ) + stillImageReader.setOnImageAvailableListener( + { reader: ImageReader -> + var output: OutputStream + try { + reader.acquireLatestImage().use { image -> + hideCaptureProgressUI() + val file = File( + requireActivity().getExternalFilesDir(null), + if (image.format == ImageFormat.JPEG) "frame.jpg" else "frame.yuv" + ) + output = FileOutputStream(file) + output.write(getDataFromImage(image)) + output.close() + Toast.makeText( + requireActivity(), "Frame saved at: " + file.path, + Toast.LENGTH_SHORT + ).show() + } + } catch (e: Exception) { + e.printStackTrace() + } + }, storeHandler + ) + return stillImageReader + } + + /** + * Creates postview image reader and sets up OnImageAvailableListener if current extension mode + * supports postview. + */ + private fun createPostviewImageReader(): ImageReader? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE || !isPostviewAvailable) { + return null + } + + val jpegSupportedSizes = extensionCharacteristics.getPostviewSupportedSizes( + currentExtension, + Size( + stillImageReader.width, + stillImageReader.height + ), + ImageFormat.JPEG + ) + val yuvSupportedSizes = extensionCharacteristics.getPostviewSupportedSizes( + currentExtension, + Size( + stillImageReader.width, + stillImageReader.height + ), + ImageFormat.YUV_420_888 + ) + val postviewSize: Size + val postviewFormat: Int + if (!jpegSupportedSizes.isEmpty()) { + postviewSize = jpegSupportedSizes[0] + postviewFormat = ImageFormat.JPEG + } else { + postviewSize = yuvSupportedSizes[0] + postviewFormat = ImageFormat.YUV_420_888 + } + val postviewImageReader = + ImageReader.newInstance(postviewSize.width, postviewSize.height, postviewFormat, 1) + postviewImageReader.setOnImageAvailableListener( + { reader: ImageReader -> + try { + reader.acquireLatestImage().use { image -> + drawPostviewImage(image) + } + } catch (e: Exception) { + e.printStackTrace() + } + }, storeHandler + ) + + return postviewImageReader + } + + /** + * Draw postview image to the capture progress UI + */ + private fun drawPostviewImage(image: Image) { + createBitmapFromImage(image)?.let { bitmap -> + requireActivity().runOnUiThread { + binding.progressInfoImage?.apply { + // The following settings are for correctly displaying the postview image in portrait + // orientation which Camera2Extensions sample app currently supports for. + if (lensFacing == CameraCharacteristics.LENS_FACING_BACK) { + rotation = 90.0f + scaleY = 1.0f + } else { + rotation = 270.0f + scaleY = -1.0f + } + setImageBitmap(bitmap) + visibility = View.VISIBLE + } + } + } + } + + private fun createBitmapFromImage(image: Image): Bitmap? { + Log.d(TAG, "createBitmapFromImage from image of format ${image.format}") + when (image.format) { + ImageFormat.JPEG -> { + val data = getDataFromImage(image) + return BitmapFactory.decodeByteArray(data, 0, data.size, null) + } + + ImageFormat.YUV_420_888 -> { + val yuvToRgbConverter = YuvToRgbConverter(requireContext()) + val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888) + yuvToRgbConverter.yuvToRgb(image, bitmap) + return bitmap + } + } + + return null + } + /** * Takes a picture. */ private fun takePicture() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + // Retrieves the still capture latency info + cameraExtensionSession.realtimeStillCaptureLatency?.let { + stillCaptureLatency = it + progressStartTimestampMs = SystemClock.elapsedRealtime() + } + captureProcessingProgress = -1 + showCaptureProgressUI() + } submitRequest( CameraDevice.TEMPLATE_STILL_CAPTURE, - stillImageReader.surface, + if (isPostviewAvailable) { + listOf(stillImageReader.surface, postviewImageReader!!.surface) + } else { + listOf(stillImageReader.surface) + }, false ) { request -> request.apply { set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomRatio) + setTag(STILL_CAPTURE_TAG) + } + } + } + + /** + * Shows the UI for capture progress info + */ + private fun showCaptureProgressUI() { + // Do not show the UI if none of still capture latency, process progress or postview is + // supported. + if (stillCaptureLatency == null && !isCaptureProcessProgressAvailable && !isPostviewAvailable) { + return + } + + progressState = PROGRESS_STATE_HOLD_STILL + requireActivity().runOnUiThread { + enableUiControls(false) + binding.progressInfoContainer?.visibility = View.VISIBLE + binding.progressState?.text = getString(R.string.state_hold_still) + binding.progressIndicator?.isIndeterminate = stillCaptureLatency == null + } + // Schedules to execute a runnable repeatedly to update the progress info + if (stillCaptureLatency != null) { + progressInfoScheduledFuture = Executors.newSingleThreadScheduledExecutor() + .scheduleAtFixedRate( + { updateProgressInfo() }, 0, 100, TimeUnit.MILLISECONDS + ) + } + } + + private fun enableUiControls(isEnabled: Boolean) { + requireActivity().runOnUiThread { + binding.switchButton.isEnabled = isEnabled + binding.captureButton.isEnabled = isEnabled + binding.strengthSlider?.isEnabled = isEnabled + } + } + + /** + * Updates the capture progress info + */ + private fun updateProgressInfo() { + requireActivity().runOnUiThread { + binding.progressState?.text = when (progressState) { + PROGRESS_STATE_HOLD_STILL -> resources.getString(R.string.state_hold_still) + PROGRESS_STATE_STILL_PROCESSING -> resources.getString(R.string.state_still_processing) + else -> "" } + + binding.progressIndicator?.isIndeterminate = false + + if (progressState == PROGRESS_STATE_STILL_PROCESSING && isCaptureProcessProgressAvailable) { + binding.progressIndicator?.progress = captureProcessingProgress + + if (captureProcessingProgress == 100) { + hideCaptureProgressUI() + } + } + + stillCaptureLatency?.let { + val remainingDurationMs = calculateRemainingDurationInMs() + binding.progressLatencyDuration?.text = + resources.getString(R.string.latency_duration, (remainingDurationMs + 500) / 1000) + // Updates the progress indicator according to the remaining duration of latency time if + // capture process progress is not supported. + if (progressState == PROGRESS_STATE_HOLD_STILL || !isCaptureProcessProgressAvailable) { + binding.progressIndicator?.progress = calculateProgressByRemainingDuration() + } + // Automatically turns to still-processing state if capture process progress is not + // supported + if (remainingDurationMs.toInt() == 0 && progressState == PROGRESS_STATE_HOLD_STILL) { + progressState = PROGRESS_STATE_STILL_PROCESSING + progressStartTimestampMs = SystemClock.elapsedRealtime() + updateProgressInfo() + } + } + } + } + + /** + * Hides the UI for capture progress info + */ + private fun hideCaptureProgressUI() { + progressState = PROGRESS_STATE_DONE + requireActivity().runOnUiThread { + binding.progressInfoContainer?.apply { + visibility = View.GONE + binding.progressInfoImage?.visibility = View.GONE + binding.progressLatencyDuration?.text = "" + } + enableUiControls(true) + } + progressInfoScheduledFuture?.apply { + cancel(true) + progressInfoScheduledFuture = null } } @@ -543,10 +981,20 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { target: Surface, isRepeating: Boolean, block: (captureRequest: CaptureRequest.Builder) -> CaptureRequest.Builder) { + return submitRequest(templateType, listOf(target), isRepeating, block) + } + + private fun submitRequest( + templateType: Int, + targets: List, + isRepeating: Boolean, + block: (captureRequest: CaptureRequest.Builder) -> CaptureRequest.Builder) { try { val captureBuilder = cameraDevice.createCaptureRequest(templateType) .apply { - addTarget(target) + targets.forEach { + addTarget(it) + } if (tag != null) { setTag(tag) } @@ -760,6 +1208,7 @@ class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { companion object { private val TAG = CameraFragment::class.java.simpleName + private const val STILL_CAPTURE_TAG = "still_capture_tag" private const val AUTO_FOCUS_TAG = "auto_focus_tag" private const val AUTO_FOCUS_TIMEOUT_MILLIS = 5_000L private const val METERING_RECTANGLE_SIZE = 0.15f diff --git a/Camera2Extensions/app/src/main/res/drawable/ic_progress_info_bg.xml b/Camera2Extensions/app/src/main/res/drawable/ic_progress_info_bg.xml new file mode 100644 index 00000000..35296f0a --- /dev/null +++ b/Camera2Extensions/app/src/main/res/drawable/ic_progress_info_bg.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/Camera2Extensions/app/src/main/res/layout/fragment_camera.xml b/Camera2Extensions/app/src/main/res/layout/fragment_camera.xml index be69f276..a958ffbd 100644 --- a/Camera2Extensions/app/src/main/res/layout/fragment_camera.xml +++ b/Camera2Extensions/app/src/main/res/layout/fragment_camera.xml @@ -38,6 +38,25 @@ android:contentDescription="@string/switch_content" android:padding="@dimen/button_padding" /> + + + + + + + + + + + + + \ No newline at end of file diff --git a/Camera2Extensions/app/src/main/res/values/dimens.xml b/Camera2Extensions/app/src/main/res/values/dimens.xml index 3d9c73da..ebf4236b 100644 --- a/Camera2Extensions/app/src/main/res/values/dimens.xml +++ b/Camera2Extensions/app/src/main/res/values/dimens.xml @@ -17,7 +17,14 @@ 20dp + 24dp 96dp 96dp 96dp + 90dp + 120dp + 8dp + 24dp + 32sp + 24sp \ No newline at end of file diff --git a/Camera2Extensions/app/src/main/res/values/strings.xml b/Camera2Extensions/app/src/main/res/values/strings.xml index d3024d93..32a88a69 100644 --- a/Camera2Extensions/app/src/main/res/values/strings.xml +++ b/Camera2Extensions/app/src/main/res/values/strings.xml @@ -18,6 +18,10 @@ Camera2Extensions Capture - Switch extension + %ds Camera permission required! + Extension strength slider> + Hold Still... + Processing... + Switch extension diff --git a/Camera2Extensions/app/src/main/res/values/styles.xml b/Camera2Extensions/app/src/main/res/values/styles.xml index cf7cd7e0..7b222f57 100644 --- a/Camera2Extensions/app/src/main/res/values/styles.xml +++ b/Camera2Extensions/app/src/main/res/values/styles.xml @@ -17,7 +17,7 @@ -