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
This commit is contained in:
Marc Rousavy 2024-02-15 13:09:16 +01:00 committed by GitHub
parent 21042048ae
commit 5df5ca9adf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 52 additions and 31 deletions

View File

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.util.Log import android.util.Log
import android.view.Gravity
import android.view.ScaleGestureDetector import android.view.ScaleGestureDetector
import android.widget.FrameLayout import android.widget.FrameLayout
import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableMap
@ -110,6 +111,11 @@ class CameraView(context: Context) :
clipToOutline = true clipToOutline = true
cameraSession = CameraSession(context, cameraManager, this) cameraSession = CameraSession(context, cameraManager, this)
previewView = cameraSession.createPreviewView(context) previewView = cameraSession.createPreviewView(context)
previewView.layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT,
Gravity.CENTER
)
addView(previewView) addView(previewView)
} }

View File

@ -11,6 +11,7 @@ import android.view.SurfaceView
import android.widget.FrameLayout import android.widget.FrameLayout
import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.bridge.UiThreadUtil
import com.mrousavy.camera.extensions.getMaximumPreviewSize import com.mrousavy.camera.extensions.getMaximumPreviewSize
import com.mrousavy.camera.extensions.installHierarchyFitter
import com.mrousavy.camera.extensions.resize import com.mrousavy.camera.extensions.resize
import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.ResizeMode import com.mrousavy.camera.types.ResizeMode
@ -19,7 +20,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@SuppressLint("ViewConstructor") @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() var size: Size = getMaximumPreviewSize()
private set private set
var resizeMode: ResizeMode = ResizeMode.COVER var resizeMode: ResizeMode = ResizeMode.COVER
@ -31,28 +34,6 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV
invalidate() 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 private val viewSize: Size
get() { get() {
val displayMetrics = context.resources.displayMetrics val displayMetrics = context.resources.displayMetrics
@ -60,6 +41,36 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : SurfaceV
val dpY = height / displayMetrics.density val dpY = height / displayMetrics.density
return Size(dpX.toInt(), dpY.toInt()) 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 { fun convertLayerPointToCameraCoordinates(point: Point, cameraDeviceDetails: CameraDeviceDetails): Point {
val sensorOrientation = Orientation.fromRotationDegrees(cameraDeviceDetails.sensorOrientation) val sensorOrientation = Orientation.fromRotationDegrees(cameraDeviceDetails.sensorOrientation)

View File

@ -6,32 +6,36 @@ import androidx.annotation.UiThread
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
private const val TAG = "SurfaceHolder"
@UiThread @UiThread
suspend fun SurfaceHolder.resize(width: Int, height: Int) { suspend fun SurfaceHolder.resize(targetWidth: Int, targetHeight: Int) {
return suspendCancellableCoroutine { continuation -> return suspendCancellableCoroutine { continuation ->
val currentSize = this.surfaceFrame val currentSize = this.surfaceFrame
if (currentSize.width() == width && currentSize.height() == height) { if (currentSize.width() == targetWidth && currentSize.height() == targetHeight) {
// Already in target size // Already in target size
continuation.resume(Unit) continuation.resume(Unit)
return@suspendCancellableCoroutine 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 { val callback = object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) = Unit override fun surfaceCreated(holder: SurfaceHolder) = Unit
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
holder.removeCallback(this) if (width == targetWidth && height == targetHeight) {
Log.i("SurfaceHolder", "Resized SurfaceHolder to $width x $height!") holder.removeCallback(this)
continuation.resume(Unit) Log.i(TAG, "Resized SurfaceHolder to $width x $height!")
continuation.resume(Unit)
}
} }
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
holder.removeCallback(this) 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!")) continuation.cancel(Error("Tried to resize SurfaceView, but Surface has been destroyed!"))
} }
} }
this.addCallback(callback) this.addCallback(callback)
this.setFixedSize(width, height) this.setFixedSize(targetWidth, targetHeight)
} }
} }