44

I've been playing around with flutter an i'm loving it so far, but I'm having an issue getting the camera working.

I follow the directions on this page https://pub.dartlang.org/packages/camera and it works. However, the camera is stretched so that it fits the phone screen, but the image is warped and taller than it should be. Other apps which use the camera seem to keep it in proportion, is there a way to ensure that it doesn't get stretched but still fills the screen?

Angus Allman
  • 511
  • 1
  • 6
  • 10
  • 2
    This is answered here https://stackoverflow.com/questions/49721306/flutter-camera-preview – Richard Heap Apr 20 '18 at 16:54
  • @RichardHeap I've seen this answer in my googling, and it seems like its good for ensuring the size of the camera, but doesnt mention stopping the camera from being stretched. I still want the camera full screen, just not warped. Sorry for any uncertainty! – Angus Allman Apr 20 '18 at 16:56

17 Answers17

60

UPDATE: version v0.7.0 of the camera package introduced a breaking change for calculating camera aspectRatio and it seems they add the AspectRatio widget by default

Well, inspired by previous posts, I got it to work on 2 screens with different screen aspect ratios. What I did was print the values and look for differences (not quite what you'd call an educated approach). What came out of that was that one screen was taller than the other, although the same width so the screen aspect ratio is different.

Disclaimer: This was tested in an app locked to portrait orientation, no guaranty that this will work in landscape orientation. Also, it seems that this does put some of the camera preview pixels outside the screen, but at the moment I have no remedy for that.

No matter which camera resolution was selected, the camera controller aspect ratio stayed the same, but the screen aspect ratio differed so the calculations mentioned in older posts than this one worked for one (shorter) screen but not the other (taller).

After some long calculations it became apparent that, to fill the screen, the scale factor has to be greater than 1. So, in the end, to get the scale to work on both screens, there needs to be a check if one ratio is greater than the other and switch operands, to get a scale factor greater than 1, accordingly.

TL;DR

Here is what was changed and end result:

Calculation

1. Camera package version v0.7.0 and above

  Widget cameraWidget(context) {
    var camera = _cameraController.value;
    // fetch screen size
    final size = MediaQuery.of(context).size;
        
    // calculate scale depending on screen and camera ratios
    // this is actually size.aspectRatio / (1 / camera.aspectRatio)
    // because camera preview size is received as landscape
    // but we're calculating for portrait orientation
    var scale = size.aspectRatio * camera.aspectRatio;

    // to prevent scaling down, invert the value
    if (scale < 1) scale = 1 / scale;

    return Transform.scale(
      scale: scale,
      child: Center(
        child: CameraPreview(_cameraController),
      ),
  );
}

2. Older versions - v0.6.6 and below

  Widget cameraWidget(context) {
    // get screen size
    final size = MediaQuery.of(context).size;

    // calculate scale for aspect ratio widget
    var scale = _cameraController.value.aspectRatio / size.aspectRatio;

    // check if adjustments are needed...
    if (_cameraController.value.aspectRatio < size.aspectRatio) {
      scale = 1 / scale;
    }

    return Transform.scale(
      scale: scale,
      child: Center(
        child: AspectRatio(
          aspectRatio: _cameraController.value.aspectRatio,
          child: CameraPreview(_cameraController),
        ),
      ),
    );
  }

Results

NOTE: results below are valid only for versions below 0.7.0 but the concept is the same

Notice that the scale has to be greater than 1 to fill the whole screen.

  1. taller screen with above calculation
Screen size (w/h): Size(411.4, 797.7)
Screen size aspect ratio: 0.515759312320917
Controller preview size: Size(1280.0, 720.0)
Controller aspect ratio: 0.5625
Scale: 1.0906249999999997
  1. shorter screen with above calculation
Screen size (w/h): Size(411.4, 683.4)
Screen size aspect ratio: 0.6020066889632107
Controller preview size: Size(1280.0, 720.0)
Controller aspect ratio: 0.5625
Scale: 1.0702341137123745
  1. shorter screen without above calculation
Screen size (w/h): Size(411.4, 683.4)
Screen size aspect ratio: 0.6020066889632107
Controller preview size: Size(1280.0, 720.0)
Controller aspect ratio: 0.5625
Scale: 0.934375
rexxar
  • 1,671
  • 1
  • 21
  • 27
  • yeah, the only answer considering >= v0.7.0 – falkb Feb 28 '21 at 16:32
  • 1
    The best solution! Worked perfectly! – yushulx Apr 20 '21 at 09:14
  • This answer has helped further my understanding of scale and aspect ratio. – Nelson Katale Jun 10 '21 at 19:37
  • 1
    Great answer. I found that on a device with a very tall screen it wasn't aligned properly. Setting alignment property of the Transform to Alignment.topCenter solved this. – Chris Jul 01 '21 at 16:16
  • 1
    Correct answer! – stephnx Jul 21 '21 at 11:58
  • I tried this on IOS and it leaves a white border on the sides. On android this works fine. (tested on iphone 7 btw). is there another way to do this?? – Karan V Feb 15 '22 at 04:24
  • @KaranV I did see a more slick solution using "longestSide" property, but I forgot to add it here. I did not have any issues with iOs, please add me to your example repo where the issue is reproducible, maybe we can work it out (my GH name is on my profile). – rexxar Mar 11 '22 at 14:54
  • The greatest work I've seen – AnhPC03 Jan 06 '23 at 03:15
  • It seems to be scaling it down to the 'safeArea' region of the screen. Is there a simple around this? Thank you for sharing this minimally viable workaround! – C RICH Feb 15 '23 at 05:31
  • 1
    @CRICH I haven't touched this in a long time and I don't know how the camera package behaves nowadays but I guess you could get the height of the safeArea region, use it to calculate the real aspect ratio of the screen (something like `width/(height + safeArea height)`) and then calculate the required scale. sorry, this is all just out of my head, a theory – rexxar Feb 15 '23 at 12:20
48

Building upon the solution by lase, this works even when the aspect ratio of the device is different then the camera ratio:

final size = MediaQuery.of(context).size;
final deviceRatio = size.width / size.height;
return Transform.scale(
  scale: controller.value.aspectRatio / deviceRatio,
  child: Center(
    child: AspectRatio(
      aspectRatio: controller.value.aspectRatio,
      child: CameraPreview(controller),
    ),
  ),
);
Marek Lisý
  • 3,302
  • 2
  • 20
  • 28
14

I had a similar problem, and while Richard's answer was helpful, I also had the problem of the preview being too small. There might be better ways, but I was able to solve it by scaling this by an inversion of the aspect ratio like so:

return new Scaffold(
  appBar: new AppBar(title: new Text('Capture')),
  body: new Transform.scale(
      scale: 1 / controller.value.aspectRatio,
      child: new Center(
        child: new AspectRatio(
            aspectRatio: controller.value.aspectRatio,
            child: new CameraPreview(controller)),
      )),
);

From the inside out, this should:

  1. Build the CameraPreview at the correct aspect ratio
  2. Center this preview in the view
  3. Apply a scaling transform, which allows the preview to fill the screen appropriately, based on aspect ratio
lase
  • 2,508
  • 2
  • 24
  • 35
  • 2
    Unfortunately this does not work properly on stretched devices such as on Galaxy S9+ (ultra weird 37:18 ratio). The preview does not fit the whole screen, there are white stripes up and bottom. I will try to dig into this and post solution here. – Marek Lisý Sep 24 '18 at 10:16
14

I just want to build upon Marek Lisy's answer due to the fact that it does break the container. In my case I am using a TabBarView but that answer causes incorrect snapping to tabs. I fixed this using a ClipRect as seen below:

final size = MediaQuery.of(context).size;

if (!controller.value.isInitialized) {
  return Container();
}
return ClipRect(
  child: Container(
    child: Transform.scale(
      scale: controller.value.aspectRatio / size.aspectRatio,
      child: Center(
        child: AspectRatio(
          aspectRatio: controller.value.aspectRatio,
          child: CameraPreview(controller),
        ),
      ),
    ),
  ),
);

Best of luck guys! I spent an hour or so trying to figure this out. So hopefully nobody has the same problem.

kiwikodes
  • 569
  • 8
  • 14
  • what is the meaning of 'breaking the container' ? why do you need a container for this particular use case? Do you see any performance loss using ClipRect ? I thought it was not so good performance-wise but maybe I'm mistaking it with ClipRRect – Tiago Santos Aug 21 '20 at 18:51
6

You need to wrap the preview in a Widget that gives the correct aspect ratio - either by setting the width and height to the correct ratio yourself, or using AspectRatio. For example, the camera plugin example uses:

new Expanded(
  child: new Padding(
    padding: const EdgeInsets.all(5.0),
    child: new Center(
      child: new AspectRatio(
        aspectRatio: controller.value.aspectRatio,
        child: new CameraPreview(controller),
      ),
    ),
  ),
),
Richard Heap
  • 48,344
  • 9
  • 130
  • 112
  • Thanks for this! Its definitely a step in the right direction. Everything is in proportion now but its small in the middle of the screen. Do you know of any way to make it full screen? Sorry about this I'm still getting to grips with it. Thanks! – Angus Allman Apr 20 '18 at 17:53
  • Please try the code on the Readme tab of the camera plugin - it does what you want. If you even wonder why the bounding boxes of Widgets aren't where you expect, use Flutter Inspector. (In Idea, open it from the right tab, and click the "closed U" icon.) This shows the bounding boxes of widgets. – Richard Heap Apr 20 '18 at 18:43
  • 1
    `1 / controller.value.aspectRatio` – lolelo Sep 29 '21 at 08:56
3

Found this solution that worked for me: https://github.com/flutter/flutter/issues/15953#issuecomment-855182376

if (controller == null || !controller!.value.isInitialized) {
  return const CardView(child: Text('No Camera to Preview'));
}

var tmp = MediaQuery.of(context).size;

final screenH = math.max(tmp.height, tmp.width);
final screenW = math.min(tmp.height, tmp.width);

tmp = controller!.value.previewSize!;

final previewH = math.max(tmp.height, tmp.width);
final previewW = math.min(tmp.height, tmp.width);
final screenRatio = screenH / screenW;
final previewRatio = previewH / previewW;

return ClipRRect(
  child: OverflowBox(
    maxHeight: screenRatio > previewRatio
        ? screenH
        : screenW / previewW * previewH,
    maxWidth: screenRatio > previewRatio
        ? screenH / previewH * previewW
        : screenW,
    child: CameraPreview(
      controller!,
    ),
  ),
);
Giraldi
  • 16,451
  • 6
  • 33
  • 52
2

This worked for me so far for version 0.7.0+4

@override
Widget build(BuildContext context) {
  return OrientationBuilder(
    builder: (context, orientation) => AspectRatio(
      aspectRatio: () {
        final camera = controller.value;
        if (orientation == Orientation.portrait) {
          return 1 / camera.previewSize.aspectRatio;
          // same as return camera.previewSize.flipped.aspectRatio;
        }
        return camera.previewSize.aspectRatio;
      }(),
      child: CameraPreview(controller),
    ),
  );
}
1

This is what I came up with (only in Portrait mode):

    Map<String, double> ratios = {
      '1:1': 1 / 1,
      '9:16': 9 / 16,
      '3:4': 3 / 4,
      '9:21': 9 / 21,
      'full': MediaQuery.of(context).size.aspectRatio,
    };
          final size = MediaQuery.of(context).size;
          print('screen ratio: ${size.aspectRatio}, default: $_defaultRatio');

          return Transform.scale(
            scale: 1.0,
            child: AspectRatio(
              aspectRatio: _defaultRatio,
              child: OverflowBox(
                alignment: Alignment.center,
                child: FittedBox(
                  fit: size.aspectRatio >= _defaultRatio
                      ? BoxFit.fitHeight
                      : BoxFit.fitWidth,
                  child: Container(
                    width: size.aspectRatio >= _defaultRatio
                        ? size.height
                        : size.width,
                    height: size.aspectRatio >= _defaultRatio
                        ? size.height / controller.value.aspectRatio
                        : size.width / controller.value.aspectRatio,
                    child: Stack(
                      children: <Widget>[
                        CameraPreview(controller),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          );
Jim
  • 6,928
  • 1
  • 7
  • 18
1

In my case, I applied RotatedBox(). The following code worked perfectly:

return RotatedBox(
        quarterTurns: MediaQuery.of(context).orientation == Orientation.landscape ? 3 : 0,
        child: AspectRatio(
          aspectRatio: controller.value.aspectRatio,
          child: CameraPreview(controller),
        ),

I used 'package:camera/camera.dart'; The only thing you should think of is the "3 : 0" parameters. 3 — that is how many turns your box is made (clockwise). Three - means system buttons are on the right side. And the second 0 — that is how to scale the preview over the windows (now stretch). You may want to add flexibility in it and read actual positioning from the device and change these numbers. I used additional car overlay to see how a car should be positioned in the front of the camera to post the photo to ad. board. [Android screen photo landscape]

1

I were also facing the same issue in landscape orientation, which I resolved by below source code inside context -

if (!_cameraController.value.isInitialized) {
  return Container();
}

final size = MediaQuery.of(context).size;
final deviceRatio = size.width / size.height;
double xScale = _cameraController.value.aspectRatio / deviceRatio;
final yScale = 1.0;

if (MediaQuery.of(context).orientation == Orientation.landscape) {
  xScale = 1.0;
}

and so the widget view were aligned like this, which I tested on OnePlus 6 (29 Q) & Asus Z010D (23 Marshmallow) Android OS versions, and it were working fine.

RotatedBox(
  quarterTurns: MediaQuery.of(context).orientation == Orientation.landscape ? 3 : 0,
  child: Container(
    child: AspectRatio(
      aspectRatio: deviceRatio,
      child: Transform(
        alignment: Alignment.center,
        transform: Matrix4.diagonal3Values(xScale, yScale, 1),
        child: CameraPreview(_cameraController),
      ),
    ),
  ),
),

Hope this will give some help to someone.

ArifMustafa
  • 4,617
  • 5
  • 40
  • 48
1

After spending a very long time trying to find a way to handle this I moved over to this package https://pub.dev/packages/camerawesome and saved so much time.

Benjamin
  • 8,128
  • 3
  • 34
  • 45
1

You can do it like that StackFit.expand

Stack(
  fit: StackFit.expand,
  children: [_cameraPreviewWidget(), _cameraButtonWidget()],
)
Cenk YAGMUR
  • 3,154
  • 2
  • 26
  • 42
0

I tried All Solutions but all of them has problem in some conditions, please try this, It works like a charm:

First:

final size = MediaQuery.of(context).size;
final deviceRatio = size.width / size.height;

Then:

return Transform.scale(
   scale: 1,
   child: Center(
     child: AspectRatio(
       aspectRatio: deviceRatio,
       child: CameraPreview(_cameraController),
     ),
   ),
);

Hope it helps ...

Ali Esfandiari
  • 316
  • 4
  • 13
  • While this seems to be the best option so far, there's still some extra space that's not filled on my Nexus 7. – Zane Campbell Apr 24 '20 at 11:49
  • I haven't seen any problem in my case. – Ali Esfandiari Apr 25 '20 at 14:39
  • 1
    This approach assumes that the aspect ratio of the camera is the same as the aspect ratio of the camera, which is not guaranteed to be true. The best way is to use a SizedBox set to the actual dimensions of the photo/video and then a FittedBox around it. You can also use a RotatedBox if/as desired to deal with the phone's orientation changing. – David Oldford Jul 09 '20 at 18:21
  • 1
    @DavidOldford I don't know if you have a typo here... "This approach assumes that the aspect ratio of the camera is the same as the aspect ratio of the camera, which is not guaranteed to be true." I think it's safe to assume that the aspect ratio of the camera is going to be the same as the aspect ratio of the camera... – Air Oct 24 '22 at 20:05
  • Yeah I meant to say aspect ration of the camera the same as the aspect ratio of the screen. In the example deviceRatio is the aspect ration of the screen. – David Oldford Nov 03 '22 at 01:06
0

Edited: the below method works pretty well but using a FittedBox along with a SizedBox works much better. see this video from the flutter devs.

FittedBox(child: SizedBox(child: texture, width: imageWidthInPixels, height: imageHeightInPixels));

The best way to do this is to follow the instructions in the Widget of the week video on AspectRatio. Use AspectRatio to make sure the video maintains it's aspect ratio (go figure.) Then Wrap in an Align and then an Expanded. If you use a RotatedBox to adjust for orientation wrap the aspect ratio in it within the Align.

Expanded(
    child:Align(
        child:AspectRatio(
            child:texture, aspectRatio:cameraAspectRatio)
David Oldford
  • 1,127
  • 4
  • 11
0

The scaling has been reworked so you don't have to make any tricks using this other plugin: camera awesome

mcfly
  • 774
  • 1
  • 8
  • 18
0

if you are not bothered about the resolution then change ResolutionPreset to ResolutionPreset.medium will fix your issue.

 _controller = CameraController(
  // Get a specific camera from the list of available cameras.
  _cameraDescription,
  // Define the resolution to use.
  ResolutionPreset.medium,
);
saigopi.me
  • 14,011
  • 2
  • 83
  • 54
-1

I just want to add that, If after positioning the camera preview at the center and it isn't fullscreen, changing the resolution of the camera to veryHigh will definitely make it fullscreen.

CameraController( cameras[0], ResolutionPreset.veryHigh )

Iredia Ebikade
  • 1,785
  • 14
  • 18