Skip to content

Commit

Permalink
feat: Enable/disable camera flash (openfoodfacts#2196)
Browse files Browse the repository at this point in the history
* Enable/disable camera flash

* Add missing type

* Update user_preferences.dart

* Update user_preferences.dart

Co-authored-by: Pierre Slamich <pierre@openfoodfacts.org>
Co-authored-by: Pierre Slamich <pierre.slamich@gmail.com>
  • Loading branch information
3 people authored Jun 7, 2022
1 parent 62b02d9 commit d6d8a07
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 10 deletions.
9 changes: 9 additions & 0 deletions packages/smooth_app/lib/data_models/user_preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class UserPreferences extends ChangeNotifier {
static const String _TAG_IS_FIRST_SCAN = 'is_first_scan';
static const String _TAG_SCAN_CAMERA_RESOLUTION_PRESET =
'camera_resolution_preset';
static const String _TAG_USE_FLASH_WITH_CAMERA = 'enable_flash_with_camera';
static const String _TAG_PLAY_CAMERA_SCAN_SOUND = 'camera_scan_sound';

/// Attribute group that is not collapsed
Expand Down Expand Up @@ -135,6 +136,14 @@ class UserPreferences extends ChangeNotifier {
bool? getFlag(final String key) =>
_sharedPreferences.getBool(_getFlagTag(key));

bool get useFlashWithCamera =>
_sharedPreferences.getBool(_TAG_USE_FLASH_WITH_CAMERA) ?? false;

Future<void> setUseFlashWithCamera(final bool useFlash) async {
await _sharedPreferences.setBool(_TAG_USE_FLASH_WITH_CAMERA, useFlash);
notifyListeners();
}

List<String> getExcludedAttributeIds() =>
_sharedPreferences.getStringList(_TAG_EXCLUDED_ATTRIBUTE_IDS) ??
<String>[];
Expand Down
8 changes: 8 additions & 0 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,14 @@
}
}
},
"camera_enable_flash": "Enable flash",
"@camera_enable_flash": {
"description": "Enable flash (tooltip)"
},
"camera_disable_flash": "Disable flash",
"@camera_disable_flash": {
"description": "Disable flash (tooltip)"
},
"category_picker_no_category_found_button": "Back",
"@category_picker_no_category_found_button": {
"description": "Button label when no category is available"
Expand Down
40 changes: 40 additions & 0 deletions packages/smooth_app/lib/pages/scan/camera_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import 'package:camera/camera.dart';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:smooth_app/data_models/user_preferences.dart';

/// A lifecycle-aware [CameraController]
class SmoothCameraController extends CameraController {
SmoothCameraController(
this.preferences,
CameraDescription description,
ResolutionPreset resolutionPreset, {
ImageFormatGroup? imageFormatGroup,
Expand All @@ -21,6 +23,8 @@ class SmoothCameraController extends CameraController {
imageFormatGroup: imageFormatGroup,
);

final UserPreferences preferences;

/// Status of the preview
bool _isPaused;

Expand All @@ -38,6 +42,7 @@ class SmoothCameraController extends CameraController {
required Offset focusPoint,
required DeviceOrientation deviceOrientation,
required onLatestImageAvailable onAvailable,
bool? enableTorch,
}) async {
if (!_isInitialized && !_isBeingInitialized) {
_isBeingInitialized = true;
Expand All @@ -47,6 +52,7 @@ class SmoothCameraController extends CameraController {
await setExposurePoint(focusPoint);
await lockCaptureOrientation(deviceOrientation);
await startImageStream(onAvailable);
await enableFlash(enableTorch ?? preferences.useFlashWithCamera);
_isInitialized = true;
_isBeingInitialized = false;

Expand Down Expand Up @@ -74,6 +80,7 @@ class SmoothCameraController extends CameraController {
@override
Future<void> pausePreview() async {
if (_isInitialized) {
await _pauseFlash();
await super.pausePreview();
_isPaused = true;
}
Expand All @@ -90,9 +97,42 @@ class SmoothCameraController extends CameraController {
@override
Future<void> resumePreview() async {
await super.resumePreview();
await _resumeFlash();
_isPaused = false;
}

Future<void> _resumeFlash() async {
if (preferences.useFlashWithCamera) {
return enableFlash(preferences.useFlashWithCamera);
}
}

Future<void> _pauseFlash() {
// Don't persist value to preferences
return setFlashMode(FlashMode.off).then(
// A slight delay is required as the native part doesn't wait here
(_) => Future<void>.delayed(
const Duration(milliseconds: 250),
),
);
}

Future<void> enableFlash(bool enable) async {
await setFlashMode(enable ? FlashMode.torch : FlashMode.off);
await preferences.setUseFlashWithCamera(enable);
}

/// Please use [enableFlash] instead
@protected
@override
Future<void> setFlashMode(FlashMode mode) {
return super.setFlashMode(mode);
}

bool get isFlashModeEnabled {
return value.flashMode == FlashMode.torch;
}

@override
Future<void> stopImageStream() async {
await super.stopImageStream();
Expand Down
1 change: 1 addition & 0 deletions packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ class MLKitScannerPageState extends LifecycleAwareState<MLKitScannerPage>

CameraHelper.initController(
SmoothCameraController(
_userPreferences,
_camera!,
_userPreferences.useVeryHighResolutionPreset
? ResolutionPreset.veryHigh
Expand Down
150 changes: 150 additions & 0 deletions packages/smooth_app/lib/pages/scan/scan_flash_toggle.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/data_models/user_preferences.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/helpers/camera_helper.dart';

class ScannerFlashToggleWidget extends StatelessWidget {
const ScannerFlashToggleWidget({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);

return Selector<UserPreferences, bool>(
selector: (_, UserPreferences prefs) => prefs.useFlashWithCamera,
builder: (BuildContext context, bool value, _) {
return Tooltip(
message: value
? appLocalizations.camera_disable_flash
: appLocalizations.camera_enable_flash,
decoration: BoxDecoration(
color: value ? Colors.red : Colors.green,
borderRadius: ANGULAR_BORDER_RADIUS,
),
child: InkWell(
customBorder: const CircleBorder(),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 6.0,
vertical: 8.0,
),
child: Icon(
value ? Icons.flash_on : Icons.flash_off,
size: 20.0,
color: Colors.white,
),
),
onTap: () async {
await HapticFeedback.selectionClick();
await CameraHelper.controller?.enableFlash(!value);
},
),
);
});
}

/// Returns the Size of the visor
static Size getSize(BuildContext context) => Size(
MediaQuery.of(context).size.width * 0.8,
150.0,
);
}

class ScanVisorPainter extends CustomPainter {
ScanVisorPainter();

static const double strokeWidth = 3.0;
static const double _fullCornerSize = 31.0;
static const double _halfCornerSize = _fullCornerSize / 2;
static const Radius _borderRadius = Radius.circular(_halfCornerSize);

final Paint _paint = Paint()
..strokeWidth = strokeWidth
..color = Colors.white
..style = PaintingStyle.stroke;

@override
void paint(Canvas canvas, Size size) {
final Rect rect = Rect.fromLTRB(0.0, 0.0, size.width, size.height);
canvas.drawPath(getPath(rect, false), _paint);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => false;

/// Returns a path to draw the visor
/// [includeLineBetweenCorners] will draw lines between each corner, instead
/// of moving the cursor
static Path getPath(Rect rect, bool includeLineBetweenCorners) {
final double bottomPosition;
if (includeLineBetweenCorners) {
bottomPosition = rect.bottom - strokeWidth;
} else {
bottomPosition = rect.bottom;
}

final Path path = Path()
// Top left
..moveTo(rect.left, rect.top + _fullCornerSize)
..lineTo(rect.left, rect.top + _halfCornerSize)
..arcToPoint(
Offset(rect.left + _halfCornerSize, rect.top),
radius: _borderRadius,
)
..lineTo(rect.left + _fullCornerSize, rect.top);

// Top right
if (includeLineBetweenCorners) {
path.lineTo(rect.right - _fullCornerSize, rect.top);
} else {
path.moveTo(rect.right - _fullCornerSize, rect.top);
}

path
..lineTo(rect.right - _halfCornerSize, rect.top)
..arcToPoint(
Offset(rect.right, _halfCornerSize),
radius: _borderRadius,
)
..lineTo(rect.right, rect.top + _fullCornerSize);

// Bottom right
if (includeLineBetweenCorners) {
path.lineTo(rect.right, bottomPosition - _fullCornerSize);
} else {
path.moveTo(rect.right, bottomPosition - _fullCornerSize);
}

path
..lineTo(rect.right, bottomPosition - _halfCornerSize)
..arcToPoint(
Offset(rect.right - _halfCornerSize, bottomPosition),
radius: _borderRadius,
)
..lineTo(rect.right - _fullCornerSize, bottomPosition);

// Bottom left
if (includeLineBetweenCorners) {
path.lineTo(rect.left + _fullCornerSize, bottomPosition);
} else {
path.moveTo(rect.left + _fullCornerSize, bottomPosition);
}

path
..lineTo(rect.left + _halfCornerSize, bottomPosition)
..arcToPoint(
Offset(rect.left, bottomPosition - _halfCornerSize),
radius: _borderRadius,
)
..lineTo(rect.left, bottomPosition - _fullCornerSize);

if (includeLineBetweenCorners) {
path.lineTo(rect.left, rect.top + _halfCornerSize);
}

return path;
}
}
31 changes: 21 additions & 10 deletions packages/smooth_app/lib/pages/scan/scan_visor.dart
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:smooth_app/pages/scan/scan_flash_toggle.dart';

class ScannerVisorWidget extends StatelessWidget {
const ScannerVisorWidget({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return SizedBox.fromSize(
size: getSize(context),
child: CustomPaint(
painter: ScanVisorPainter(),
child: Center(
child: SvgPicture.asset(
'assets/icons/visor_icon.svg',
width: 35.0,
height: 32.0,
return Stack(
children: <Widget>[
SizedBox.fromSize(
size: getSize(context),
child: CustomPaint(
painter: ScanVisorPainter(),
child: Center(
child: SvgPicture.asset(
'assets/icons/visor_icon.svg',
width: 35.0,
height: 32.0,
),
),
),
),
),
Positioned.directional(
textDirection: Directionality.of(context),
end: 0.0,
bottom: 0.0,
child: const ScannerFlashToggleWidget(),
)
],
);
}

Expand Down

0 comments on commit d6d8a07

Please sign in to comment.