From 5df5ca9adfcba280cd7e01f05bfb9b799247db1c Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 15 Feb 2024 13:09:16 +0100 Subject: [PATCH] fix: Fix `PreviewView` stretching on Android (now finally a real fix) (#2564) * fix: Only resolve once SurfaceHolder actually resized * fix: Fix onMeasure not being called for `PreviewView` * fix: Auto-trigger layout computation on Surface Change * fix: Add proper LayoutParams to `PreviewView` * Format --- .../java/com/mrousavy/camera/CameraView.kt | 6 ++ .../com/mrousavy/camera/core/PreviewView.kt | 57 +++++++++++-------- .../camera/extensions/SurfaceHolder+resize.kt | 20 ++++--- 3 files changed, 52 insertions(+), 31 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt index 7f0f7b4..12bffc7 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import android.hardware.camera2.CameraManager import android.util.Log +import android.view.Gravity import android.view.ScaleGestureDetector import android.widget.FrameLayout import com.facebook.react.bridge.ReadableMap @@ -110,6 +111,11 @@ class CameraView(context: Context) : clipToOutline = true cameraSession = CameraSession(context, cameraManager, this) previewView = cameraSession.createPreviewView(context) + previewView.layoutParams = LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT, + Gravity.CENTER + ) addView(previewView) } diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt index c862be7..df33b83 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt @@ -11,6 +11,7 @@ import android.view.SurfaceView import android.widget.FrameLayout import com.facebook.react.bridge.UiThreadUtil import com.mrousavy.camera.extensions.getMaximumPreviewSize +import com.mrousavy.camera.extensions.installHierarchyFitter import com.mrousavy.camera.extensions.resize import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.ResizeMode @@ -19,7 +20,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @SuppressLint("ViewConstructor") -class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceView(context) { +class PreviewView(context: Context, callback: SurfaceHolder.Callback) : + FrameLayout(context), + SurfaceHolder.Callback { var size: Size = getMaximumPreviewSize() private set var resizeMode: ResizeMode = ResizeMode.COVER @@ -31,28 +34,6 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV invalidate() } } - - init { - Log.i(TAG, "Creating PreviewView...") - layoutParams = FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT, - Gravity.CENTER - ) - holder.setKeepScreenOn(true) - holder.addCallback(callback) - } - - suspend fun setSurfaceSize(width: Int, height: Int) { - withContext(Dispatchers.Main) { - size = Size(width, height) - Log.i(TAG, "Setting PreviewView Surface Size to $size...") - requestLayout() - invalidate() - holder.resize(width, height) - } - } - private val viewSize: Size get() { val displayMetrics = context.resources.displayMetrics @@ -60,6 +41,36 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV val dpY = height / displayMetrics.density return Size(dpX.toInt(), dpY.toInt()) } + private val surfaceView = SurfaceView(context) + + init { + Log.i(TAG, "Creating PreviewView...") + this.installHierarchyFitter() + surfaceView.layoutParams = LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT, + Gravity.CENTER + ) + surfaceView.holder.setKeepScreenOn(true) + surfaceView.holder.addCallback(this) + surfaceView.holder.addCallback(callback) + addView(surfaceView) + } + + override fun surfaceCreated(holder: SurfaceHolder) = Unit + override fun surfaceDestroyed(holder: SurfaceHolder) = Unit + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + Log.i(TAG, "PreviewView Surface size changed: $size -> ${width}x$height, re-computing layout...") + size = Size(width, height) + requestLayout() + invalidate() + } + + suspend fun setSurfaceSize(width: Int, height: Int) { + withContext(Dispatchers.Main) { + surfaceView.holder.resize(width, height) + } + } fun convertLayerPointToCameraCoordinates(point: Point, cameraDeviceDetails: CameraDeviceDetails): Point { val sensorOrientation = Orientation.fromRotationDegrees(cameraDeviceDetails.sensorOrientation) diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt index cc0fe9c..561f6fc 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/SurfaceHolder+resize.kt @@ -6,32 +6,36 @@ import androidx.annotation.UiThread import kotlin.coroutines.resume import kotlinx.coroutines.suspendCancellableCoroutine +private const val TAG = "SurfaceHolder" + @UiThread -suspend fun SurfaceHolder.resize(width: Int, height: Int) { +suspend fun SurfaceHolder.resize(targetWidth: Int, targetHeight: Int) { return suspendCancellableCoroutine { continuation -> val currentSize = this.surfaceFrame - if (currentSize.width() == width && currentSize.height() == height) { + if (currentSize.width() == targetWidth && currentSize.height() == targetHeight) { // Already in target size continuation.resume(Unit) return@suspendCancellableCoroutine } - Log.i("SurfaceHolder", "Resizing SurfaceHolder to $width x $height...") + Log.i(TAG, "Resizing SurfaceHolder to $targetWidth x $targetHeight...") val callback = object : SurfaceHolder.Callback { override fun surfaceCreated(holder: SurfaceHolder) = Unit override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { - holder.removeCallback(this) - Log.i("SurfaceHolder", "Resized SurfaceHolder to $width x $height!") - continuation.resume(Unit) + if (width == targetWidth && height == targetHeight) { + holder.removeCallback(this) + Log.i(TAG, "Resized SurfaceHolder to $width x $height!") + continuation.resume(Unit) + } } override fun surfaceDestroyed(holder: SurfaceHolder) { holder.removeCallback(this) - Log.e("SurfaceHolder", "Failed to resize SurfaceHolder to $width x $height!") + Log.e(TAG, "Failed to resize SurfaceHolder to $targetWidth x $targetHeight!") continuation.cancel(Error("Tried to resize SurfaceView, but Surface has been destroyed!")) } } this.addCallback(callback) - this.setFixedSize(width, height) + this.setFixedSize(targetWidth, targetHeight) } }