Apparence-io / CamerAwesome

πŸ“Έ Embedding a camera experience within your own app shouldn't be that hard. A flutter plugin to integrate awesome Android / iOS camera experience.
https://ApparenceKit.dev
MIT License
949 stars 240 forks source link

Ipad orientation with custom builder #166

Closed KirioXX closed 1 year ago

KirioXX commented 1 year ago

Steps to Reproduce

This is my setup:

import 'package:auto_route/auto_route.dart';
import 'package:camerawesome/pigeon.dart';
import 'package:flutter/material.dart';

import 'package:path_provider/path_provider.dart';
import 'package:camerawesome/camerawesome_plugin.dart';
import 'camera_countdown.dart';
import 'camera_layout.dart';
import 'package:uuid/uuid.dart';

export 'package:camerawesome/camerawesome_plugin.dart' show CaptureMode;

class CameraPageResponse {
  CameraPageResponse({required this.filePath});
  final String filePath;
}

class CameraPage extends StatelessWidget {
  const CameraPage({
    super.key,
    this.captureMode = CaptureMode.photo,
    this.maxVideoDuration,
  });

  final CaptureMode captureMode;
  final Duration? maxVideoDuration;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CameraAwesomeBuilder.custom(
        initialCaptureMode: captureMode,
        saveConfig: captureMode == CaptureMode.photo
            ? SaveConfig.photo(
                pathBuilder: () async {
                  final extDir = await getApplicationDocumentsDirectory();
                  return '${extDir.path}/${const Uuid().v4()}.jpg';
                },
              )
            : SaveConfig.video(
                pathBuilder: () async {
                  final extDir = await getApplicationDocumentsDirectory();
                  return '${extDir.path}/${const Uuid().v4()}.mp4';
                },
              ),
        exifPreferences: ExifPreferences(
          saveGPSLocation: true,
        ),
        builder: (cameraState) {
          return cameraState.when(
            onPreparingCamera: (state) =>
                const Center(child: CircularProgressIndicator()),
            onPhotoMode: (state) => _TakePhotoUI(state),
            onVideoMode: (state) => _RecordVideoUI(state),
            onVideoRecordingMode: (state) => _RecordVideoUI(
              state,
              maxVideoDuration: maxVideoDuration,
            ),
          );
        },
      ),
    );
  }
}

class _TakePhotoUI extends StatefulWidget {
  final PhotoCameraState state;

  const _TakePhotoUI(this.state);

  @override
  State<_TakePhotoUI> createState() => _TakePhotoUIState();
}

class _TakePhotoUIState extends State<_TakePhotoUI> {
  @override
  void initState() {
    super.initState();
    widget.state.captureState$.listen((event) {
      if (event != null && event.status == MediaCaptureStatus.success) {
        context.router.pop<CameraPageResponse>(
          CameraPageResponse(filePath: event.filePath),
        );
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return AwesomeCameraLayout(state: widget.state);
  }
}

class _RecordVideoUI extends StatefulWidget {
  final CameraState state;
  final Duration? maxVideoDuration;

  const _RecordVideoUI(
    this.state, {
    this.maxVideoDuration,
  });

  @override
  State<_RecordVideoUI> createState() => _RecordVideoUIState();
}

class _RecordVideoUIState extends State<_RecordVideoUI> {
  @override
  void initState() {
    super.initState();
    widget.state.captureState$.listen((event) {
      if (event != null && event.status == MediaCaptureStatus.success) {
        context.router.pop<CameraPageResponse>(
          CameraPageResponse(filePath: event.filePath),
        );
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(children: [
      AwesomeCameraLayout(state: widget.state),
      if (widget.state is VideoRecordingCameraState &&
          widget.maxVideoDuration != null)
        Positioned(
          bottom: 20,
          right: 10,
          child: CameraCountdown(
            time: widget.maxVideoDuration!,
            callback: () {
              (widget.state as VideoRecordingCameraState).stopRecording();
            },
          ),
        ),
    ]);
  }
}

Expected results

Expect to get a new photo.

Actual results

When I try to take a photo I get this error message:

[VERBOSE-2:dart_vm_initializer.cc(41)] Unhandled Exception: type 'Null' is not a subtype of type 'bool'
#0      PhotoCameraState.takePhoto
package:camerawesome/…/states/photo_camera_state.dart:58
<asynchronous suspension>

and no photo is created.

About your device

Brand Model OS
Apple iPad (7th gen) 16.2

g-apparence commented 1 year ago

Thank you reporting this. We will try to reproduce this with the same configuration.

Could you add more logs?

g-apparence commented 1 year ago

Ok I found the error. Thank you reporting this, I'm fixing it.

g-apparence commented 1 year ago

Fixed on master.

As I've seen, you used our custom factory.

KirioXX commented 1 year ago

Awesome! Thank you very much for the quick fix @g-apparence.

I gave the awesome factory a try first but we need a bit more controlle especially when taking video. But I have to say it has been great. It is much easier to use over the previous version and the new documentation is awesome.

There are just two things that I missed:

  1. In the documentation a recomandation on how to get the media result would be great. I went in the end whith a listener on the captureState$ like this:

    
    class _TakePhotoUI extends StatefulWidget {
    final PhotoCameraState state;
    
    const _TakePhotoUI(this.state);
    
    @override
    State<_TakePhotoUI> createState() => _TakePhotoUIState();
    }

class _TakePhotoUIState extends State<_TakePhotoUI> { @override void initState() { super.initState(); widget.state.captureState$.listen((event) { if (event != null && event.status == MediaCaptureStatus.success) { context.router.pop( CameraPageResponse(filePath: event.filePath), ); } }); }

@override Widget build(BuildContext context) { return AwesomeCameraLayout(state: widget.state); } }

same for the video ui.

2. It would be great if you could make parts of the layout components accessible too.
I copied for now all of the `AwesomeCameraLayout` components and tweaked them.

<details>
<summary>My AwesomeCameraLayout components</summary>

```dart
import 'package:camerawesome/camerawesome_plugin.dart';
import 'package:flutter/material.dart';

class AwesomeCameraLayout extends StatelessWidget {
  final CameraState state;

  const AwesomeCameraLayout({
    super.key,
    required this.state,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const SizedBox(height: 16),
        AwesomeTopActions(state: state),
        const Spacer(),
        const SizedBox(height: 12),
        AwesomeBackground(
          child: Column(children: [
            AwesomeCameraModeSelector(state: state),
            AwesomeBottomActions(state: state),
            const SizedBox(height: 32),
          ]),
        ),
      ],
    );
  }
}

class AwesomeTopActions extends StatelessWidget {
  final CameraState state;

  const AwesomeTopActions({
    super.key,
    required this.state,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        AwesomeFlashButton(state: state),
        AwesomeAspectRatioButton(state: state),
        const SizedBox(),
      ],
    );
  }
}

class AwesomeBottomActions extends StatelessWidget {
  final CameraState state;

  const AwesomeBottomActions({
    super.key,
    required this.state,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 24.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          Flexible(
            flex: 0,
            child: AwesomeCameraSwitchButton(state: state),
          ),
          AwesomeCaptureButton(
            state: state,
          ),
          const Flexible(
            flex: 0,
            child: SizedBox(width: 72, height: 72),
          ),
        ],
      ),
    );
  }
}

class AwesomeBackground extends StatelessWidget {
  final Widget child;

  const AwesomeBackground({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.black54,
      child: child,
    );
  }
}

What I have not figured out jet is why my orientation is wrong and the frame is not fitted to the screen size. My frame at the moment looks like this in landscape:

g-apparence commented 1 year ago

πŸ™πŸ» Thanks for feedbacks! It's really helping. We will work on this πŸ‘Œ

For your question : What I have not figured out jet is why my orientation is wrong and the frame is not fitted to the screen size. My frame at the moment looks like this in landscape:

Can you tell me what is your device? It seems that the angle returned is wrong. (Some chineese devices doesn't start on same angle than others).

KirioXX commented 1 year ago

Yes my device is a iPad (7th gen), iPadOS 16.2. I think when I tried the awesome factory it was correct. Wonder if there is something off with my configuration. That is the full camera page:

import 'package:app/views/pages/utils/camera/camera_countdown.dart';
import 'package:app/views/pages/utils/camera/camera_layout.dart';
import 'package:auto_route/auto_route.dart';
import 'package:camerawesome/camerawesome_plugin.dart';
import 'package:camerawesome/pigeon.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';
import 'package:video_compress/video_compress.dart';

export 'package:camerawesome/camerawesome_plugin.dart' show CaptureMode;

class CameraPageResponse {
  CameraPageResponse({required this.filePath});
  final String filePath;
}

class CameraPage extends StatelessWidget {
  const CameraPage({
    super.key,
    this.captureMode = CaptureMode.photo,
    this.maxVideoDuration,
  });

  final CaptureMode captureMode;
  final Duration? maxVideoDuration;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(children: [
        CameraAwesomeBuilder.custom(
          initialCaptureMode: captureMode,
          saveConfig: captureMode == CaptureMode.photo
              ? SaveConfig.photo(
                  pathBuilder: () async {
                    final extDir = await getApplicationDocumentsDirectory();
                    return '${extDir.path}/${const Uuid().v4()}.jpg';
                  },
                )
              : SaveConfig.video(
                  pathBuilder: () async {
                    final extDir = await getApplicationDocumentsDirectory();
                    return '${extDir.path}/${const Uuid().v4()}.mp4';
                  },
                ),
          exifPreferences: ExifPreferences(
            saveGPSLocation: true,
          ),
          builder: (cameraState) {
            return cameraState.when(
              onPreparingCamera: (state) => const Center(
                child: CircularProgressIndicator(),
              ),
              onPhotoMode: (state) => _TakePhotoUI(state),
              onVideoMode: (state) => _RecordVideoUI(state),
              onVideoRecordingMode: (state) => _RecordVideoUI(
                state,
                maxVideoDuration: maxVideoDuration,
              ),
            );
          },
        ),
        const Positioned(
          top: 40,
          left: 10,
          child: AutoLeadingButton(
            color: Colors.white,
          ),
        )
      ]),
    );
  }
}

class _TakePhotoUI extends StatefulWidget {
  final PhotoCameraState state;

  const _TakePhotoUI(this.state);

  @override
  State<_TakePhotoUI> createState() => _TakePhotoUIState();
}

class _TakePhotoUIState extends State<_TakePhotoUI> {
  @override
  void initState() {
    super.initState();
    widget.state.captureState$.listen((event) {
      if (event != null && event.status == MediaCaptureStatus.success) {
        context.router.pop<CameraPageResponse>(
          CameraPageResponse(filePath: event.filePath),
        );
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return AwesomeCameraLayout(state: widget.state);
  }
}

class _RecordVideoUI extends StatefulWidget {
  final CameraState state;
  final Duration? maxVideoDuration;

  const _RecordVideoUI(
    this.state, {
    this.maxVideoDuration,
  });

  @override
  State<_RecordVideoUI> createState() => _RecordVideoUIState();
}

class _RecordVideoUIState extends State<_RecordVideoUI> {
  @override
  void initState() {
    super.initState();
    widget.state.captureState$.listen((event) async {
      if (event != null && event.status == MediaCaptureStatus.success) {
        final info = await VideoCompress.compressVideo(
          event.filePath,
          quality: VideoQuality.Res640x480Quality,
          deleteOrigin: false, // It's false by default
        );
        if (info?.path != null) {
          context.router.pop<CameraPageResponse>(
            CameraPageResponse(filePath: info!.path!),
          );
        }
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand,
      children: [
        AwesomeCameraLayout(state: widget.state),
        if (widget.state is VideoRecordingCameraState &&
            widget.maxVideoDuration != null)
          Positioned(
            bottom: 20,
            right: 10,
            child: CameraCountdown(
              time: widget.maxVideoDuration!,
              callback: () {
                (widget.state as VideoRecordingCameraState).stopRecording();
              },
            ),
          ),
      ],
    );
  }
}

and it is using the layout commponents that added to the previous comment.

apalala-dev commented 1 year ago

I was able to reproduce the issue with the orientation, I will try to fix it and let you know when it's done ✌️

KirioXX commented 1 year ago

Awesome thank you very much! πŸ™

apalala-dev commented 1 year ago

My initial guess was wrong, sorry. I can't reproduce it :( If you find other devices having the same problem share it with us, it might help!

KirioXX commented 1 year ago

That is strange. I pretty much have this issue every time I open the camera. But there seems to be more off with the orientation. This happens when I hold the tablet upside down: IMG_F2DCAD8F0C83-1

KirioXX commented 1 year ago

I tried it now on my Pixel 6 with Android 13 and there everything is fine. But I let my colleague test with the same iPad 7th gen but with iPad OS 15.7 with the same result. It seems like it is only the orientation of the preview because when we take a picture everything is in the correct orientation.

I also created also a test project https://github.com/KirioXX/camerawesome_oriententation_test and I was able to get the same result in this project.

apalala-dev commented 1 year ago

Thank you for the test project πŸ‘Œ The issue seems to be specific to iPads, I can't reproduce it on Android devices (both on phone and tablet). We should be able to test it on an iPad next week, we'll get you updated.

istornz commented 1 year ago

Hello @KirioXX I checked your test project and was able to reproduce the issue on my iPad πŸ‘

The issue is because camerAwesome only support Portrait Up orientation. We force the orientation in the plugin but this is not working as expected on iPad (see https://github.com/flutter/flutter/issues/27235).

To fix the issue, you need to enable Fullscreen in the Xcode iOS project of your app: https://github.com/flutter/flutter/issues/27235#issuecomment-497747175.

Another solution, if your app only support Portrait Up orientation, just uncheck other & only keep Portrait Up: https://github.com/flutter/flutter/issues/27235#issuecomment-508995063

KirioXX commented 1 year ago

Hey @istornz, thank you very much for looking into it! I enabled Fullscreen and the orientation is now working but sadly the button orientation is not working. Is there a separate configuration for iPadOS to get this back?

istornz commented 1 year ago

@KirioXX Perfect, yes it's "normal" I fixed this on a separate branch, it will be merged soon !

KirioXX commented 1 year ago

Awesome! Thank you very much @istornz! πŸ™Œ