adjust preview based on selected aspect ratio

- set the preview scale type to fitStart
- both the preview and image capture would use the same size
- some other code cleanups to remove unused code / formatting changes
This commit is contained in:
darthpaul 2022-07-15 21:32:54 +01:00
parent 776e842af5
commit 231a5f9c3f
7 changed files with 59 additions and 100 deletions

View File

@ -50,8 +50,13 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
private var mCurrVideoRecTimer = 0 private var mCurrVideoRecTimer = 0
var mLastHandledOrientation = 0 var mLastHandledOrientation = 0
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
window.addFlags(
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
useDynamicTheme = false useDynamicTheme = false
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
appLaunched(BuildConfig.APPLICATION_ID) appLaunched(BuildConfig.APPLICATION_ID)
@ -62,22 +67,6 @@ class MainActivity : SimpleActivity(), PhotoProcessor.MediaSavedListener, Camera
supportActionBar?.hide() supportActionBar?.hide()
checkWhatsNewDialog() checkWhatsNewDialog()
setupOrientationEventListener() setupOrientationEventListener()
if (isRPlus()) {
setShowWhenLocked(true)
setTurnScreenOn(true)
window.insetsController?.hide(WindowInsets.Type.statusBars())
} else if (isOreoMr1Plus()) {
setShowWhenLocked(true)
setTurnScreenOn(true)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
} else {
window.addFlags(
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
}
} }
override fun onResume() { override fun onResume() {

View File

@ -15,8 +15,6 @@ const val FLASHLIGHT_STATE = "flashlight_state"
const val INIT_PHOTO_MODE = "init_photo_mode" const val INIT_PHOTO_MODE = "init_photo_mode"
const val BACK_PHOTO_RESOLUTION_INDEX = "back_photo_resolution_index_2" const val BACK_PHOTO_RESOLUTION_INDEX = "back_photo_resolution_index_2"
const val BACK_VIDEO_RESOLUTION_INDEX = "back_video_resolution_index_2" const val BACK_VIDEO_RESOLUTION_INDEX = "back_video_resolution_index_2"
const val BACK_VIDEO_QUALITY = "back_video_quality_2"
const val FRONT_VIDEO_QUALITY = "front_video_quality_2"
const val FRONT_PHOTO_RESOLUTION_INDEX = "front_photo_resolution_index_2" const val FRONT_PHOTO_RESOLUTION_INDEX = "front_photo_resolution_index_2"
const val FRONT_VIDEO_RESOLUTION_INDEX = "front_video_resolution_index_2" const val FRONT_VIDEO_RESOLUTION_INDEX = "front_video_resolution_index_2"
const val KEEP_SETTINGS_VISIBLE = "keep_settings_visible" const val KEEP_SETTINGS_VISIBLE = "keep_settings_visible"

View File

@ -4,9 +4,6 @@ import android.content.Context
import android.graphics.ImageFormat import android.graphics.ImageFormat
import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.hardware.camera2.params.StreamConfigurationMap
import android.media.MediaRecorder
import android.util.Size
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector import androidx.camera.core.CameraSelector
import com.simplemobiletools.camera.extensions.config import com.simplemobiletools.camera.extensions.config
@ -19,8 +16,6 @@ class ImageQualityManager(
) { ) {
companion object { companion object {
private const val MAX_VIDEO_WIDTH = 4096
private const val MAX_VIDEO_HEIGHT = 2160
private val CAMERA_LENS = arrayOf(CameraCharacteristics.LENS_FACING_FRONT, CameraCharacteristics.LENS_FACING_BACK) private val CAMERA_LENS = arrayOf(CameraCharacteristics.LENS_FACING_FRONT, CameraCharacteristics.LENS_FACING_BACK)
} }
@ -29,29 +24,25 @@ class ImageQualityManager(
private val imageQualities = mutableListOf<CameraSelectorImageQualities>() private val imageQualities = mutableListOf<CameraSelectorImageQualities>()
fun initSupportedQualities() { fun initSupportedQualities() {
for (cameraId in cameraManager.cameraIdList) { if (imageQualities.isEmpty()) {
try { for (cameraId in cameraManager.cameraIdList) {
val characteristics = cameraManager.getCameraCharacteristics(cameraId) try {
for (lens in CAMERA_LENS) { val characteristics = cameraManager.getCameraCharacteristics(cameraId)
if (characteristics.get(CameraCharacteristics.LENS_FACING) == lens) { for (lens in CAMERA_LENS) {
val configMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: continue if (characteristics.get(CameraCharacteristics.LENS_FACING) == lens) {
val imageSizes = configMap.getOutputSizes(ImageFormat.JPEG).map { MySize(it.width, it.height) } val configMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: continue
val cameraSelector = lens.toCameraSelector() val imageSizes = configMap.getOutputSizes(ImageFormat.JPEG).map { MySize(it.width, it.height) }
imageQualities.add(CameraSelectorImageQualities(cameraSelector, imageSizes)) val cameraSelector = lens.toCameraSelector()
imageQualities.add(CameraSelectorImageQualities(cameraSelector, imageSizes))
}
} }
} catch (e: Exception) {
activity.showErrorToast(e)
} }
} catch (e: Exception) {
activity.showErrorToast(e)
} }
} }
} }
private fun getAvailableVideoSizes(configMap: StreamConfigurationMap): List<Size> {
return configMap.getOutputSizes(MediaRecorder::class.java).filter {
it.width <= MAX_VIDEO_WIDTH && it.height <= MAX_VIDEO_HEIGHT
}
}
private fun Int.toCameraSelector(): CameraSelector { private fun Int.toCameraSelector(): CameraSelector {
return if (this == CameraCharacteristics.LENS_FACING_FRONT) { return if (this == CameraCharacteristics.LENS_FACING_FRONT) {
CameraSelector.DEFAULT_FRONT_CAMERA CameraSelector.DEFAULT_FRONT_CAMERA
@ -60,20 +51,19 @@ class ImageQualityManager(
} }
} }
fun getUserSelectedResolution(cameraSelector: CameraSelector): Size? { fun getUserSelectedResolution(cameraSelector: CameraSelector): MySize {
val index = if (cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA) config.frontPhotoResIndex else config.backPhotoResIndex val index = if (cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA) config.frontPhotoResIndex else config.backPhotoResIndex
return imageQualities.filter { it.camSelector == cameraSelector } return imageQualities.filter { it.camSelector == cameraSelector }
.flatMap { it.qualities } .flatMap { it.qualities }
.sortedByDescending { it.pixels } .sortedWith(compareByDescending<MySize> { it.ratio }.thenByDescending { it.pixels })
.distinctBy { it.pixels } .distinctBy { it.pixels }
.map { Size(it.width, it.height) } .filter { it.megaPixels != "0.0" }[index]
.getOrNull(index)
} }
fun getSupportedResolutions(cameraSelector: CameraSelector): List<MySize> { fun getSupportedResolutions(cameraSelector: CameraSelector): List<MySize> {
return imageQualities.filter { it.camSelector == cameraSelector } return imageQualities.filter { it.camSelector == cameraSelector }
.flatMap { it.qualities } .flatMap { it.qualities }
.sortedByDescending { it.pixels } .sortedWith(compareByDescending<MySize> { it.ratio }.thenByDescending { it.pixels })
.distinctBy { it.pixels } .distinctBy { it.pixels }
.filter { it.megaPixels != "0.0" } .filter { it.megaPixels != "0.0" }
} }

View File

@ -6,7 +6,6 @@ import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.Quality import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector import androidx.camera.video.QualitySelector
import com.simplemobiletools.camera.extensions.config import com.simplemobiletools.camera.extensions.config
import com.simplemobiletools.camera.extensions.toCameraXQuality
import com.simplemobiletools.camera.extensions.toVideoQuality import com.simplemobiletools.camera.extensions.toVideoQuality
import com.simplemobiletools.camera.models.CameraSelectorVideoQualities import com.simplemobiletools.camera.models.CameraSelectorVideoQualities
import com.simplemobiletools.camera.models.VideoQuality import com.simplemobiletools.camera.models.VideoQuality
@ -45,10 +44,10 @@ class VideoQualityManager(
} }
} }
fun getUserSelectedQuality(cameraSelector: CameraSelector): Quality { fun getUserSelectedQuality(cameraSelector: CameraSelector): VideoQuality {
var selectionIndex = if (cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA) config.frontVideoResIndex else config.backVideoResIndex var selectionIndex = if (cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA) config.frontVideoResIndex else config.backVideoResIndex
selectionIndex = selectionIndex.coerceAtLeast(0) selectionIndex = selectionIndex.coerceAtLeast(0)
return getSupportedQualities(cameraSelector).getOrElse(selectionIndex) { VideoQuality.HD }.toCameraXQuality() return getSupportedQualities(cameraSelector).getOrElse(selectionIndex) { VideoQuality.HD }
} }
fun getSupportedQualities(cameraSelector: CameraSelector): List<VideoQuality> { fun getSupportedQualities(cameraSelector: CameraSelector): List<VideoQuality> {

View File

@ -18,7 +18,6 @@ import androidx.core.content.ContextCompat
import androidx.core.view.doOnLayout import androidx.core.view.doOnLayout
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.window.layout.WindowMetricsCalculator
import com.bumptech.glide.load.ImageHeaderParser.UNKNOWN_ORIENTATION import com.bumptech.glide.load.ImageHeaderParser.UNKNOWN_ORIENTATION
import com.simplemobiletools.camera.R import com.simplemobiletools.camera.R
import com.simplemobiletools.camera.dialogs.ChangeResolutionDialogX import com.simplemobiletools.camera.dialogs.ChangeResolutionDialogX
@ -26,11 +25,9 @@ import com.simplemobiletools.camera.extensions.*
import com.simplemobiletools.camera.helpers.* import com.simplemobiletools.camera.helpers.*
import com.simplemobiletools.camera.interfaces.MyPreview import com.simplemobiletools.camera.interfaces.MyPreview
import com.simplemobiletools.camera.models.MediaOutput import com.simplemobiletools.camera.models.MediaOutput
import com.simplemobiletools.camera.models.MySize
import com.simplemobiletools.commons.extensions.toast import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
class CameraXPreview( class CameraXPreview(
private val activity: AppCompatActivity, private val activity: AppCompatActivity,
@ -42,9 +39,6 @@ class CameraXPreview(
) : MyPreview, DefaultLifecycleObserver { ) : MyPreview, DefaultLifecycleObserver {
companion object { companion object {
private const val RATIO_4_3_VALUE = 4.0 / 3.0
private const val RATIO_16_9_VALUE = 16.0 / 9.0
// Auto focus is 1/6 of the area. // Auto focus is 1/6 of the area.
private const val AF_SIZE = 1.0f / 6.0f private const val AF_SIZE = 1.0f / 6.0f
private const val AE_SIZE = AF_SIZE * 1.5f private const val AE_SIZE = AF_SIZE * 1.5f
@ -55,7 +49,6 @@ class CameraXPreview(
private val mainExecutor = ContextCompat.getMainExecutor(activity) private val mainExecutor = ContextCompat.getMainExecutor(activity)
private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
private val mediaSoundHelper = MediaSoundHelper() private val mediaSoundHelper = MediaSoundHelper()
private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
private val videoQualityManager = VideoQualityManager(activity) private val videoQualityManager = VideoQualityManager(activity)
private val imageQualityManager = ImageQualityManager(activity) private val imageQualityManager = ImageQualityManager(activity)
private val exifRemover = ExifRemover(contentResolver) private val exifRemover = ExifRemover(contentResolver)
@ -73,9 +66,13 @@ class CameraXPreview(
in 225 until 315 -> Surface.ROTATION_90 in 225 until 315 -> Surface.ROTATION_90
else -> Surface.ROTATION_0 else -> Surface.ROTATION_0
} }
preview?.targetRotation = rotation
imageCapture?.targetRotation = rotation if (lastRotation != rotation) {
videoCapture?.targetRotation = rotation preview?.targetRotation = rotation
imageCapture?.targetRotation = rotation
videoCapture?.targetRotation = rotation
lastRotation = rotation
}
} }
} }
@ -89,6 +86,7 @@ class CameraXPreview(
private var cameraSelector = config.lastUsedCameraLens.toCameraSelector() private var cameraSelector = config.lastUsedCameraLens.toCameraSelector()
private var flashMode = FLASH_MODE_OFF private var flashMode = FLASH_MODE_OFF
private var isPhotoCapture = initInPhotoMode private var isPhotoCapture = initInPhotoMode
private var lastRotation = 0
init { init {
bindToLifeCycle() bindToLifeCycle()
@ -122,17 +120,18 @@ class CameraXPreview(
private fun bindCameraUseCases() { private fun bindCameraUseCases() {
val cameraProvider = cameraProvider ?: throw IllegalStateException("Camera initialization failed.") val cameraProvider = cameraProvider ?: throw IllegalStateException("Camera initialization failed.")
val metrics = windowMetricsCalculator.computeCurrentWindowMetrics(activity).bounds val rotation = previewView.display.rotation
val aspectRatio = if (isPhotoCapture) { val resolution = if (isPhotoCapture) {
aspectRatio(metrics.width(), metrics.height()) imageQualityManager.getUserSelectedResolution(cameraSelector)
} else { } else {
val selectedQuality = videoQualityManager.getUserSelectedQuality(cameraSelector) val selectedQuality = videoQualityManager.getUserSelectedQuality(cameraSelector)
selectedQuality.getAspectRatio() MySize(selectedQuality.width, selectedQuality.height)
} }
val rotation = previewView.display.rotation
preview = buildPreview(aspectRatio, rotation) val rotatedResolution = getRotatedResolution(resolution, rotation)
val captureUseCase = getCaptureUseCase(aspectRatio, rotation)
preview = buildPreview(rotatedResolution, rotation)
val captureUseCase = getCaptureUseCase(rotatedResolution, rotation)
cameraProvider.unbindAll() cameraProvider.unbindAll()
camera = cameraProvider.bindToLifecycle( camera = cameraProvider.bindToLifecycle(
activity, activity,
@ -167,10 +166,10 @@ class CameraXPreview(
} }
} }
private fun getCaptureUseCase(aspectRatio: Int, rotation: Int): UseCase { private fun getCaptureUseCase(resolution: Size, rotation: Int): UseCase {
return if (isPhotoCapture) { return if (isPhotoCapture) {
cameraProvider?.unbind(videoCapture) cameraProvider?.unbind(videoCapture)
buildImageCapture(aspectRatio, rotation).also { buildImageCapture(resolution, rotation).also {
imageCapture = it imageCapture = it
} }
} else { } else {
@ -181,22 +180,17 @@ class CameraXPreview(
} }
} }
private fun buildImageCapture(aspectRatio: Int, rotation: Int): ImageCapture { private fun buildImageCapture(resolution: Size, rotation: Int): ImageCapture {
return Builder() return Builder()
.setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY) .setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY)
.setFlashMode(flashMode) .setFlashMode(flashMode)
.setJpegQuality(config.photoQuality) .setJpegQuality(config.photoQuality)
.setTargetRotation(rotation) .setTargetRotation(rotation)
.apply { .setTargetResolution(resolution)
imageQualityManager.getUserSelectedResolution(cameraSelector)?.let { resolution ->
val rotatedResolution = getRotatedResolution(rotation, resolution)
setTargetResolution(rotatedResolution)
} ?: setTargetAspectRatio(aspectRatio)
}
.build() .build()
} }
private fun getRotatedResolution(rotationDegrees: Int, resolution: Size): Size { private fun getRotatedResolution(resolution: MySize, rotationDegrees: Int): Size {
return if (rotationDegrees == Surface.ROTATION_0 || rotationDegrees == Surface.ROTATION_180) { return if (rotationDegrees == Surface.ROTATION_0 || rotationDegrees == Surface.ROTATION_180) {
Size(resolution.height, resolution.width) Size(resolution.height, resolution.width)
} else { } else {
@ -204,16 +198,16 @@ class CameraXPreview(
} }
} }
private fun buildPreview(aspectRatio: Int, rotation: Int): Preview { private fun buildPreview(resolution: Size, rotation: Int): Preview {
return Preview.Builder() return Preview.Builder()
.setTargetRotation(rotation) .setTargetRotation(rotation)
.setTargetAspectRatio(aspectRatio) .setTargetResolution(resolution)
.build() .build()
} }
private fun buildVideoCapture(): VideoCapture<Recorder> { private fun buildVideoCapture(): VideoCapture<Recorder> {
val qualitySelector = QualitySelector.from( val qualitySelector = QualitySelector.from(
videoQualityManager.getUserSelectedQuality(cameraSelector), videoQualityManager.getUserSelectedQuality(cameraSelector).toCameraXQuality(),
FallbackStrategy.higherQualityOrLowerThan(Quality.SD), FallbackStrategy.higherQualityOrLowerThan(Quality.SD),
) )
val recorder = Recorder.Builder() val recorder = Recorder.Builder()
@ -222,14 +216,6 @@ class CameraXPreview(
return VideoCapture.withOutput(recorder) return VideoCapture.withOutput(recorder)
} }
private fun aspectRatio(width: Int, height: Int): Int {
val previewRatio = max(width, height).toDouble() / min(width, height)
if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
return AspectRatio.RATIO_4_3
}
return AspectRatio.RATIO_16_9
}
private fun hasBackCamera(): Boolean { private fun hasBackCamera(): Boolean {
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
} }
@ -288,7 +274,7 @@ class CameraXPreview(
activity, activity,
isFrontCameraInUse(), isFrontCameraInUse(),
imageQualityManager.getSupportedResolutions(cameraSelector), imageQualityManager.getSupportedResolutions(cameraSelector),
videoQualityManager.getSupportedQualities(cameraSelector) videoQualityManager.getSupportedQualities(cameraSelector),
) { ) {
if (oldQuality != videoQualityManager.getUserSelectedQuality(cameraSelector)) { if (oldQuality != videoQualityManager.getUserSelectedQuality(cameraSelector)) {
currentRecording?.stop() currentRecording?.stop()

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/view_holder" android:id="@+id/view_holder"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -9,7 +10,8 @@
<androidx.camera.view.PreviewView <androidx.camera.view.PreviewView
android:id="@+id/preview_view" android:id="@+id/preview_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
app:scaleType="fitStart" />
<View <View
android:id="@+id/capture_black_screen" android:id="@+id/capture_black_screen"

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/change_resolution_holder" android:id="@+id/change_resolution_holder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -20,17 +19,15 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_toStartOf="@+id/change_resolution_photo" android:layout_toStartOf="@+id/change_resolution_photo"
android:layout_toLeftOf="@+id/change_resolution_photo" android:text="@string/photo" />
android:text="@string/photo"/>
<com.simplemobiletools.commons.views.MyTextView <com.simplemobiletools.commons.views.MyTextView
android:id="@+id/change_resolution_photo" android:id="@+id/change_resolution_photo"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:background="@null" android:background="@null"
android:clickable="false"/> android:clickable="false" />
</RelativeLayout> </RelativeLayout>
@ -48,17 +45,15 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_toStartOf="@+id/change_resolution_video" android:layout_toStartOf="@+id/change_resolution_video"
android:layout_toLeftOf="@+id/change_resolution_video" android:text="@string/video" />
android:text="@string/video"/>
<com.simplemobiletools.commons.views.MyTextView <com.simplemobiletools.commons.views.MyTextView
android:id="@+id/change_resolution_video" android:id="@+id/change_resolution_video"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:background="@null" android:background="@null"
android:clickable="false"/> android:clickable="false" />
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>