Native Camera View in Flutter does not occupy the whole width and height
Asked Answered
A

5

7

I am trying to integrate CameraX inside my flutter project. I am doing so via platform channels. I don't want to integrate any third party lib.

Here is a screenshot of the camera only occupying one third of the height in Android

enter image description here

Below is my code

class ScanQr extends StatelessWidget {
  const ScanQr({super.key});

  @override
  Widget build(BuildContext context) {
    var width = MediaQuery.of(context).size.width;
    var height = MediaQuery.of(context).size.height;

    return Scaffold(
      backgroundColor: Colors.teal,
      body: SizedBox(
        height: height, 
        width: width,
        child: CealScanQrView(
          width: width,
          height: height,
        ),
      ),
    );
  }
}


class CealScanQrView extends StatelessWidget {
  const CealScanQrView({required this.width, required this.height, super.key});

  final double width;
  final double height;
 
  @override
  Widget build(BuildContext context) {
    final Map<String, dynamic> creationParams = <String, dynamic>{};

    creationParams["width"] = width;
    creationParams["height"] = height;

    return Platform.isAndroid
        ? AndroidView(
            viewType: cealScanQrView,
            layoutDirection: TextDirection.ltr,
            creationParams: creationParams,
            creationParamsCodec: const StandardMessageCodec(),
          )
        : UiKitView(
            viewType: cealScanQrView,
            layoutDirection: TextDirection.ltr,
            creationParams: creationParams,
            creationParamsCodec: const StandardMessageCodec(),
          );
  }
}

Here is my android code

In MainActivity's configureFlutterEngine method

flutterEngine
            .platformViewsController
            .registry
            .registerViewFactory("cealScanQrView", CealScanQrViewFactory(this))


class CealScanQrView(
    private val context: Context, id: Int, creationParams: Map<String?, Any?>?,
    private val activity: FlutterActivity
) : PlatformView {

    private var mCameraProvider: ProcessCameraProvider? = null
    private var linearLayout: LinearLayout = LinearLayout(context)

    private var preview: PreviewView = PreviewView(context)

    private lateinit var cameraExecutor: ExecutorService
    private lateinit var options: BarcodeScannerOptions
    private lateinit var scanner: BarcodeScanner

    private var analysisUseCase: ImageAnalysis = ImageAnalysis.Builder()
        .build()

    companion object {
        private val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = mutableListOf(Manifest.permission.CAMERA).toTypedArray()
    }


    init {
        val linearLayoutParams = ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT
        )

        linearLayout.layoutParams = linearLayoutParams
        linearLayout.orientation = LinearLayout.VERTICAL

        preview.setBackgroundColor(Color.RED)
        preview.layoutParams = ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT
        )

        linearLayout.addView(preview)
        linearLayout.layoutParams.width = 400
        linearLayout.layoutParams.height = 400
        setUpCamera()

        preview.viewTreeObserver.addOnGlobalLayoutListener(object :
            ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                preview.viewTreeObserver.removeOnGlobalLayoutListener(this)
                preview.layoutParams.height =
                    creationParams?.get("height").toString().toDouble().toInt()
                preview.requestLayout()
            }
        })
    }

    private fun setUpCamera() {
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                context as FlutterActivity, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
            )
        }
        cameraExecutor = Executors.newSingleThreadExecutor()

        options = BarcodeScannerOptions.Builder()
            .setBarcodeFormats(
                Barcode.FORMAT_QR_CODE
            )
            .build()
        scanner = BarcodeScanning.getClient(options)
        analysisUseCase.setAnalyzer(
            // newSingleThreadExecutor() will let us perform analysis on a single worker thread
            Executors.newSingleThreadExecutor()
        ) { imageProxy ->
            processImageProxy(scanner, imageProxy)
        }
    }

    override fun getView(): View {
        return linearLayout
    }

    override fun dispose() {
        cameraExecutor.shutdown()
    }

    @SuppressLint("UnsafeOptInUsageError")
    private fun processImageProxy(
        barcodeScanner: BarcodeScanner,
        imageProxy: ImageProxy
    ) {
        imageProxy.image?.let { image ->
            val inputImage =
                InputImage.fromMediaImage(
                    image,
                    imageProxy.imageInfo.rotationDegrees
                )
            barcodeScanner.process(inputImage)
                .addOnSuccessListener { barcodeList ->
                    val barcode = barcodeList.getOrNull(0)
                    // `rawValue` is the decoded value of the barcode
                    barcode?.rawValue?.let { value ->
                        mCameraProvider?.unbindAll()
                        Toast.makeText(context, value, Toast.LENGTH_LONG).show()
                    }
                }
                .addOnFailureListener {
                    // This failure will happen if the barcode scanning model
                    // fails to download from Google Play Services
                }
                .addOnCompleteListener {
                    // When the image is from CameraX analysis use case, must
                    // call image.close() on received images when finished
                    // using them. Otherwise, new images may not be received
                    // or the camera may stall.
                    imageProxy.image?.close()
                    imageProxy.close()
                }
        }
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
    }

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
        cameraProviderFuture.addListener({
            // Used to bind the lifecycle of cameras to the lifecycle owner
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
            mCameraProvider = cameraProvider
            // Preview
            val surfacePreview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(preview.surfaceProvider)
                }
            // Select back camera as a default
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()
                // Bind use cases to camera
                cameraProvider.bindToLifecycle(
                    activity,
                    cameraSelector,
                    surfacePreview,
                    analysisUseCase,
                )
            } catch (exc: Exception) {
                // Do nothing on exception
            }
        }, ContextCompat.getMainExecutor(context))
    }


}

In AndroidManifest.xml

<uses-permission android:name="android.permission.CAMERA" />

There are two problems which I am facing

Firstly I don't see the camera permission when my screen opens even though I am asking the permission for it. I have to manually go to the settings screen to enable camera permission And secondly the camera does not occupy entire screen

Even if I set linearLayout.layoutParams.height = 1000 still the height remains the same. I completely stopped the app and reran multiple times but no effect

Edit I have created a gist where I have removed linear layout. Check here

Astragal answered 31/1, 2023 at 10:5 Comment(3)
I tried your example, After several iterations, I could get the Preview widget itself to fill the screen, but the camera view was still maintaining a small portion of the screen. I don't have the time to debug further. If I find time I will try more options.Seven
I am getting the exact same issue. Did anyone find a solution to this?Larder
Its a known issue, fix is mentioned here, either add a SurfaceView (0 X 0) to view Hierarchy or use ExpensiveAndroidView instead of AndroidView github.com/flutter/flutter/issues/109690Larder
I
2

I observed several things which might cause the behavior of your camera preview:

  1. You didn't specify an aspect ratio for your (Preview- & ImageAnalysis-) Usecases. The default is 4:3. Note that the native aspect ratio of most cameras is indeed 4:3. If you use 16:9 the visible part of your image will be smaller, but it fits better for a common phone. E.g.:
Preview.Builder().setTargetAspectRatio(AspectRatio.RATIO_16_9).build()
  1. You didn't specify a scale type for your PreviewView. This also might result in image cropping. E.g.:
preview.scaleType = PreviewView.ScaleType.FILL_CENTER
  1. The Camera orientation doesn't fit to your screen. You should update the targetRotation of your usecases. You can read more on that topic in the CameraX documentation

  2. You should set the LayoutParams for your LinearLayout to MATCH_PARENT as well. And I also think you could just use a FrameLayout. Also remove the viewTreeObserver.

  3. You have to listen to the result of the permission request. You have to override the method onRequestPermissionsResult in your Activity. It will receive the result together with the request code you've specified: REQUEST_CODE_PERMISSIONS. However it might be easier to use a newer API for requesting those permissions: registerForPermissionResult. E.g.:

val launcher = (context as ComponentActivity).registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
    if (granted) {
        startCamera()
    }
}
launcher.launch(android.Manifest.permission.CAMERA)
Impressible answered 7/2, 2023 at 15:40 Comment(5)
Does not work. Same issue. Please check the gist here gist.github.com/PritishSawant/c775ab3ef48ae3e53196fc597ab8f367 where I have followed all the changes you mentioned. I can't directly use registerForActivityResult inside my kotlin class as it is not an activity. Anyways permission is not a big issue for me, the camera height isAstragal
You can use (context as ComponentActivity).registerForActivityResult(...)Impressible
Thanks, any idea on why the camera is not occupying the whole heightAstragal
I don't know. I don't use Flutter. When I slap your code into an empty project and remove the flutter code the camera occupies the whole screen for me. I replaced the PlatformView with a FrameLayout and add the PreviewView to it.Impressible
I tried FrameLayout as well but does not worksAstragal
U
1

please add the two line of code in startCamera() function

preview.scaleType = PreviewView.ScaleType.FILL_CENTER cameraPreview.implementationMode = PreviewView.ImplementationMode.COMPATIBLE

Unmeaning answered 19/9, 2023 at 14:40 Comment(1)
Thank you brother. You have saved me and my friend a ton of headache.Affaire
C
1

Try setting the implementationMode

cameraPreview.implementationMode = PreviewView.ImplementationMode.COMPATIBLE
Cookhouse answered 24/9 at 14:17 Comment(0)
M
0

1 Don't occupy the entire screen. It's very simple you might forget to set the width dynamically on android code because of that what happens is that android code gets only 400 widths and 400 heights (in lins linearLayout.layoutParams.width = 400 linearLayout.layoutParams.height = 400 ) and after the flutter call it receives only this size, so you must make it in an Adaptive way, see this answer.

2 Don't get permission: Simply using this package permission_handler will help and automate for you across all devices.

If my answer helped you don't forget to like it.

Mediatorial answered 2/2, 2023 at 19:20 Comment(3)
I don't want to use third party packagesAstragal
Even if I set linearLayout.layoutParams.height = 1000 still the height remains the same. I completely stopped the app and reran multiple times but no eefectAstragal
Thanks for advice,I think dislike is not best way to disagree with me.Otherwise Thanks.Mediatorial
B
-1

check orientation and set it to portrait

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
  runApp(MyApp());
...
}

try weap your display area with

SizedBox.expand(
  child: YOUR DISPLAY AREA,
)

also try

Center(
  child: ,
)
Bowne answered 6/2, 2023 at 15:34 Comment(1)
The issue is on the native sideAstragal

© 2022 - 2024 — McMap. All rights reserved.