diff --git a/CHANGELOG.md b/CHANGELOG.md
index abadfcb..9f7e046 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,26 @@ that can be found in the LICENSE file. -->
# Changelog
+See the [Migration Guide](guides/migration_guide.md) for the details of breaking changes between versions.
+
+## 4.0.0-dev.1
+
+To know more about breaking changes, see [Migration Guide][].
+
+### New features
+
+- Sync all UI details from WeChat 8.3.x. (#181)
+
+### Improvements
+
+- Improve the experience when using the exposure slider.
+- Prefer `FlashMode.off` for better performance.
+- Allow `cameras` to be set repeatedly.
+
+### Fixes
+
+- Fix accessibility on the switch cameras button.
+
## 3.8.0
### New features
@@ -324,3 +344,5 @@ that can be found in the LICENSE file. -->
- Support taking pictures and videos.
- Support video recording duration limitation.
+
+[Migration Guide]: guides/migration_guide.md
diff --git a/README-ZH.md b/README-ZH.md
index 74d2a96..371ffc5 100644
--- a/README-ZH.md
+++ b/README-ZH.md
@@ -19,18 +19,31 @@ Language: [English](README.md) | 中文简体
[**仿微信资源选择器**](https://pub.flutter-io.cn/packages/wechat_assets_picker) 的扩展。
选择器基于 `camera` 实现相机相关功能,`photo_manager` 实现资源相关内容。
-## 目录 🗂
-
-- [Flutter WeChat Camera Picker](#flutter-wechat-camera-picker)
- - [目录 🗂](#目录-)
- - [特性 ✨](#特性-)
- - [截图 📸](#截图-)
- - [准备工作 🍭](#准备工作-)
- - [使用方法 📖](#使用方法-)
- - [简单的使用方法](#简单的使用方法)
- - [使用配置](#使用配置)
- - [常见问题 💭](#常见问题-)
- - [iOS 上的预览在旋转时行为诡异](#iOS-上的预览在旋转时行为诡异)
+当前的界面设计基于的微信版本:**8.3.x**
+界面更新将在微信版本更新后随时进行跟进。
+
+查看 [迁移指南][] 了解如何从破坏性改动中迁移为可用代码。
+
+
+ Table of content
+
+
+* [Flutter WeChat Camera Picker](#flutter-wechat-camera-picker)
+ * [特性 ✨](#特性-)
+ * [截图 📸](#截图-)
+ * [准备工作 🍭](#准备工作-)
+ * [版本限制](#版本限制)
+ * [配置](#配置)
+ * [Android 13 (API 33) 权限配置](#android-13-api-33-权限配置)
+ * [使用方法 📖](#使用方法-)
+ * [简单的使用方法](#简单的使用方法)
+ * [使用配置](#使用配置)
+ * [简单的使用方法](#简单的使用方法-1)
+ * [使用自定义的 `State`](#使用自定义的-state)
+ * [常见问题 💭](#常见问题-)
+ * [iOS 上的预览在旋转时行为诡异](#ios-上的预览在旋转时行为诡异)
+
+
## 特性 ✨
@@ -164,3 +177,5 @@ final AssetEntity? entity = await CameraPicker.pickFromCamera(
你可以在这个 issue 里了解更多相关的信息:
https://github.com/flutter/flutter/issues/89216 。
除此之外的问题,你可以提交 issue 进行提问。
+
+[迁移指南]: https://github.com/fluttercandies/flutter_wechat_camera_picker/blob/main/guides/migration_guide.md
diff --git a/README.md b/README.md
index 1153cb8..91f182d 100644
--- a/README.md
+++ b/README.md
@@ -20,19 +20,31 @@ A **camera picker** based on WeChat's UI which is a separate runnable extension
The package based on `camera` for camera functions
and `photo_manager` for asset implementation.
-## Category 🗂
-
-- [Flutter WeChat Camera Picker](#flutter-wechat-camera-picker)
- - [Category 🗂](#category-)
- - [Features ✨](#features-)
- - [Screenshots 📸](#screenshots-)
- - [Preparing for use 🍭](#preparing-for-use-)
- - [Usage 📖](#usage-)
- - [Simple usage](#simple-usage)
- - [With configurations](#with-configurations)
- - [Frequently asked question 💭](#frequently-asked-question-)
- - [Why the orientation behavior is strange on iOS?](#why-the-orientation-behavior-is-strange-on-ios)
- - [Contributors ✨](#contributors-)
+Current WeChat version that UI based on: **8.3.x**
+UI designs will be updated following the WeChat update in anytime.
+
+See the [Migration Guide][] to learn how to migrate between breaking changes.
+
+
+ Table of content
+
+
+* [Flutter WeChat Camera Picker](#flutter-wechat-camera-picker)
+ * [Features ✨](#features-)
+ * [Screenshots 📸](#screenshots-)
+ * [Preparing for use 🍭](#preparing-for-use-)
+ * [Version constraints](#version-constraints)
+ * [Setup](#setup)
+ * [Android 13 (API 33) permissions](#android-13-api-33-permissions)
+ * [Usage 📖](#usage-)
+ * [Simple usage](#simple-usage)
+ * [With configurations](#with-configurations)
+ * [Using custom `State`s](#using-custom-states)
+ * [Frequently asked question 💭](#frequently-asked-question-)
+ * [Why the orientation behavior is strange on iOS?](#why-the-orientation-behavior-is-strange-on-ios)
+ * [Contributors ✨](#contributors-)
+
+
## Features ✨
@@ -181,3 +193,5 @@ Thank goes to these wonderful people ([emoji key](https://allcontributors.org/do
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
+
+[Migration Guide]: https://github.com/fluttercandies/flutter_wechat_camera_picker/blob/main/guides/migration_guide.md
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 131a66f..e4fa0a2 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -1,6 +1,6 @@
name: wechat_camera_picker_demo
description: A new Flutter project.
-version: 3.8.0+23
+version: 4.0.0+24
publish_to: none
environment:
diff --git a/guides/migration_guide.md b/guides/migration_guide.md
new file mode 100644
index 0000000..71bd17a
--- /dev/null
+++ b/guides/migration_guide.md
@@ -0,0 +1,55 @@
+
+
+# Migration Guide
+
+This document gathered all breaking changes and migrations requirement between major versions.
+
+## Versions
+
+- [4.0.0](#400)
+
+## 4.0.0
+
+### Summary
+
+> If you don't extend your customized `CameraPickerState`
+> or you didn't override below methods, you can stop reading.
+
+In order to sync the UI details with the latest WeChat style (v8.3.0),
+few naming or signatures of methods are changed. including:
+- `restartDisplayModeDisplayTimer`
+- `buildBackButton`
+- `buildCameraPreview`
+- `buildCaptureButton`
+- `buildFocusingPoint`
+
+### Details
+
+- `restartDisplayModeDisplayTimer` is renamed to `restartExposureModeDisplayTimer`.
+- `buildBackButton` no more requires `BoxConstraints constraints` as an argument,
+ the signature is `Widget buildBackButton(BuildContext context)` from now on.
+- `buildCameraPreview` no more requires `DeviceOrientation orientation` as an argument
+ since the implementation does not really use it.
+ It now requires `CameraValue cameraValue` as an argument. So the signature becomes:
+ ```dart
+ Widget buildCameraPreview({
+ required BuildContext context,
+ required CameraValue cameraValue,
+ required BoxConstraints constraints,
+ })
+ ```
+- `buildCaptureButton` now requires `BuildContext context` as an argument. So the signature becomes:
+ ```dart
+ Widget buildCaptureButton(BuildContext context, BoxConstraints constraints)
+ ```
+- `buildFocusingPoint` now adds `int quarterTurns` to make internal quarter turns.
+ So the signature becomes:
+ ```dart
+ Widget buildFocusingPoint({
+ required CameraValue cameraValue,
+ required BoxConstraints constraints,
+ int quarterTurns = 0,
+ })
+ ```
diff --git a/lib/src/constants/config.dart b/lib/src/constants/config.dart
index f75f116..586c429 100644
--- a/lib/src/constants/config.dart
+++ b/lib/src/constants/config.dart
@@ -51,8 +51,8 @@ class CameraPickerConfig {
/// 选择器是否可以录像
final bool enableRecording;
- /// Whether the picker can record video.
- /// 选择器是否可以录像
+ /// Whether the picker can record video only.
+ /// 选择器是否只可以录像
final bool onlyEnableRecording;
/// Whether allow the record can start with single tap.
diff --git a/lib/src/states/camera_picker_state.dart b/lib/src/states/camera_picker_state.dart
index 18c7e7c..0021769 100644
--- a/lib/src/states/camera_picker_state.dart
+++ b/lib/src/states/camera_picker_state.dart
@@ -25,7 +25,7 @@ import '../widgets/camera_picker.dart';
import '../widgets/camera_picker_viewer.dart';
import '../widgets/camera_progress_button.dart';
-const Color _lockedColor = Colors.amber;
+const Color _lockedColor = Colors.orangeAccent;
const Duration _kDuration = Duration(milliseconds: 300);
class CameraPickerState extends State
@@ -45,6 +45,7 @@ class CameraPickerState extends State
/// Whether the focus point is displaying.
/// 是否正在展示当前的聚焦点
final ValueNotifier isFocusPointDisplays = ValueNotifier(false);
+ final ValueNotifier isFocusPointFadeOut = ValueNotifier(false);
/// The controller for the current camera.
/// 当前相机实例的控制器
@@ -58,6 +59,8 @@ class CameraPickerState extends State
/// Current exposure offset.
/// 当前曝光值
final ValueNotifier currentExposureOffset = ValueNotifier(0);
+ final ValueNotifier currentExposureSliderOffset =
+ ValueNotifier(0);
double maxAvailableExposureOffset = 0;
double minAvailableExposureOffset = 0;
@@ -92,8 +95,8 @@ class CameraPickerState extends State
/// The [Timer] for keep the [lastExposurePoint] displays.
/// 用于控制上次手动聚焦点显示的计时器
Timer? exposurePointDisplayTimer;
-
Timer? exposureModeDisplayTimer;
+ Timer? exposureFadeOutTimer;
/// The [Timer] for record start detection.
/// 用于检测是否开始录制的计时器
@@ -130,11 +133,21 @@ class CameraPickerState extends State
////////////////////////////////////////////////////////////////////////////
CameraPickerConfig get pickerConfig => widget.pickerConfig;
+ /// Whether the camera preview should be scaled during captures.
+ /// 拍摄过程中相机预览是否需要缩放
+ bool get enableScaledPreview => pickerConfig.enableScaledPreview;
+
+ /// Whether the picker can record video.
+ /// 选择器是否可以录像
bool get enableRecording => pickerConfig.enableRecording;
+ /// Whether the picker only enables video recording.
+ /// 选择器是否只可以录像
bool get onlyEnableRecording =>
enableRecording && pickerConfig.onlyEnableRecording;
+ /// Whether allow the record can start with single tap.
+ /// 选择器是否可以单击录像
bool get enableTapRecording =>
onlyEnableRecording && pickerConfig.enableTapRecording;
@@ -169,6 +182,11 @@ class CameraPickerState extends State
return pickerConfig.minimumRecordingDuration;
}
+ /// Whether the camera preview should be rotated.
+ bool get isCameraRotated => pickerConfig.cameraQuarterTurns % 4 != 0;
+
+ int get cameraQuarterTurns => pickerConfig.cameraQuarterTurns;
+
/// A getter to the current [CameraDescription].
/// 获取当前相机实例
CameraDescription get currentCamera => cameras.elementAt(currentCameraIndex);
@@ -194,10 +212,13 @@ class CameraPickerState extends State
ambiguate(WidgetsBinding.instance)?.removeObserver(this);
innerController?.dispose();
currentExposureOffset.dispose();
+ currentExposureSliderOffset.dispose();
lastExposurePoint.dispose();
isFocusPointDisplays.dispose();
+ isFocusPointFadeOut.dispose();
exposurePointDisplayTimer?.cancel();
exposureModeDisplayTimer?.cancel();
+ exposureFadeOutTimer?.cancel();
recordDetectTimer?.cancel();
recordCountdownTimer?.cancel();
super.dispose();
@@ -223,7 +244,7 @@ class CameraPickerState extends State
BoxConstraints constraints,
CameraController controller,
) {
- final int turns = pickerConfig.cameraQuarterTurns;
+ final int turns = cameraQuarterTurns;
final String orientation = controller.value.deviceOrientation.toString();
// Fetch the biggest size from the constraints.
Size size = constraints.biggest;
@@ -258,12 +279,14 @@ class CameraPickerState extends State
currentZoom = 1;
baseZoom = 1;
// Meanwhile, cancel the existed exposure point and mode display.
- exposureModeDisplayTimer?.cancel();
exposurePointDisplayTimer?.cancel();
+ exposureModeDisplayTimer?.cancel();
+ exposureFadeOutTimer?.cancel();
+ isFocusPointDisplays.value = false;
+ isFocusPointFadeOut.value = false;
lastExposurePoint.value = null;
- if (currentExposureOffset.value != 0) {
- currentExposureOffset.value = 0;
- }
+ currentExposureOffset.value = 0;
+ currentExposureSliderOffset.value = 0;
});
// **IMPORTANT**: Push methods into a post frame callback, which ensures the
// controller has already unbind from widgets.
@@ -480,13 +503,21 @@ class CameraPickerState extends State
});
}
- void restartDisplayModeDisplayTimer() {
+ void restartExposureModeDisplayTimer() {
exposureModeDisplayTimer?.cancel();
exposureModeDisplayTimer = Timer(const Duration(seconds: 2), () {
isFocusPointDisplays.value = false;
});
}
+ void restartExposureFadeOutTimer() {
+ isFocusPointFadeOut.value = false;
+ exposureFadeOutTimer?.cancel();
+ exposureFadeOutTimer = Timer(const Duration(seconds: 2), () {
+ isFocusPointFadeOut.value = true;
+ });
+ }
+
/// Use the specific [mode] to update the exposure mode.
/// 设置曝光模式
Future switchExposureMode() async {
@@ -508,7 +539,8 @@ class CameraPickerState extends State
} catch (e, s) {
handleErrorWithHandler(e, pickerConfig.onError, s: s);
}
- restartDisplayModeDisplayTimer();
+ restartExposureModeDisplayTimer();
+ restartExposureFadeOutTimer();
}
/// Use the [position] to set exposure and focus.
@@ -518,11 +550,13 @@ class CameraPickerState extends State
BoxConstraints constraints,
) async {
isFocusPointDisplays.value = false;
- // Ignore point update when the new point is less than 8% and higher than
- // 92% of the screen's height.
- if (position.dy < constraints.maxHeight / 12 ||
- position.dy > constraints.maxHeight / 12 * 11) {
- return;
+ if (enableScaledPreview) {
+ // Ignore point update when the new point is less than 8% and higher than
+ // 92% of the screen's height.
+ if (position.dy < constraints.maxHeight / 12 ||
+ position.dy > constraints.maxHeight / 12 * 11) {
+ return;
+ }
}
realDebugPrint(
'Setting new exposure point (x: ${position.dx}, y: ${position.dy})',
@@ -530,20 +564,25 @@ class CameraPickerState extends State
lastExposurePoint.value = position;
restartExposurePointDisplayTimer();
currentExposureOffset.value = 0;
+ currentExposureSliderOffset.value = 0;
+ restartExposureFadeOutTimer();
+ isFocusPointFadeOut.value = false;
try {
- if (controller.value.exposureMode == ExposureMode.locked) {
- await controller.setExposureMode(ExposureMode.auto);
- }
+ await Future.wait(>[
+ controller.setExposureOffset(0),
+ if (controller.value.exposureMode == ExposureMode.locked)
+ controller.setExposureMode(ExposureMode.auto),
+ ]);
final Offset newPoint = lastExposurePoint.value!.scale(
1 / constraints.maxWidth,
1 / constraints.maxHeight,
);
- if (controller.value.exposurePointSupported) {
- controller.setExposurePoint(newPoint);
- }
- if (controller.value.focusPointSupported) {
- controller.setFocusPoint(newPoint);
- }
+ await Future.wait(>[
+ if (controller.value.exposurePointSupported)
+ controller.setExposurePoint(newPoint),
+ if (controller.value.focusPointSupported)
+ controller.setFocusPoint(newPoint),
+ ]);
} catch (e, s) {
handleErrorWithHandler(e, pickerConfig.onError, s: s);
}
@@ -552,6 +591,7 @@ class CameraPickerState extends State
/// Update the exposure offset using the exposure controller.
/// 使用曝光控制器更新曝光值
Future updateExposureOffset(double value) async {
+ currentExposureSliderOffset.value = value;
// Normalize the new exposure value if exposures have steps.
if (exposureStep > 0) {
final double inv = 1.0 / exposureStep;
@@ -570,6 +610,7 @@ class CameraPickerState extends State
}
currentExposureOffset.value = value;
try {
+ realDebugPrint('Updating the exposure offset value: $value');
// Use [CameraPlatform] explicitly to reduce channel calls.
await CameraPlatform.instance.setExposureOffset(
controller.cameraId,
@@ -581,8 +622,27 @@ class CameraPickerState extends State
if (!isFocusPointDisplays.value) {
isFocusPointDisplays.value = true;
}
- restartDisplayModeDisplayTimer();
restartExposurePointDisplayTimer();
+ restartExposureModeDisplayTimer();
+ restartExposureFadeOutTimer();
+ }
+
+ /// Request to set the focus and the exposure point on the [localPosition],
+ /// [lock] to lock the exposure mode at the same time.
+ /// 将对焦和曝光设置为给定的点 [localPosition],[lock] 控制是否同时锁定曝光模式。
+ Future requestFocusAndExposureOnPosition(
+ Offset localPosition,
+ BoxConstraints constraints, {
+ bool lock = false,
+ }) async {
+ // Only call exposure point updates when the controller is initialized.
+ if (innerController?.value.isInitialized ?? false) {
+ Feedback.forTap(context);
+ await setExposureAndFocusPoint(localPosition, constraints);
+ if (lock) {
+ await switchExposureMode();
+ }
+ }
}
/// Update the scale value while the user is shooting.
@@ -865,13 +925,25 @@ class CameraPickerState extends State
if (v.isRecordingVideo) {
return const SizedBox.shrink();
}
+ Widget backButton = buildBackButton(context);
+ Widget flashModeSwitch = buildFlashModeSwitch(context, v);
+ if (isCameraRotated && !enableScaledPreview) {
+ backButton = RotatedBox(
+ quarterTurns: cameraQuarterTurns,
+ child: backButton,
+ );
+ flashModeSwitch = RotatedBox(
+ quarterTurns: cameraQuarterTurns,
+ child: flashModeSwitch,
+ );
+ }
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
- if (cameras.length > 1) buildCameraSwitch(context),
+ if (innerController?.value.isRecordingVideo != true) backButton,
const Spacer(),
- buildFlashModeSwitch(context, v),
+ flashModeSwitch,
],
),
);
@@ -882,16 +954,18 @@ class CameraPickerState extends State
/// The button to switch between cameras.
/// 切换相机的按钮
Widget buildCameraSwitch(BuildContext context) {
- return IconButton(
- tooltip: textDelegate.sSwitchCameraLensDirectionLabel(
- nextCameraDescription.lensDirection,
- ),
- onPressed: switchCameras,
- icon: Icon(
- Platform.isIOS
- ? Icons.flip_camera_ios_outlined
- : Icons.flip_camera_android_outlined,
- size: 24,
+ return MergeSemantics(
+ child: IconButton(
+ tooltip: textDelegate.sSwitchCameraLensDirectionLabel(
+ nextCameraDescription.lensDirection,
+ ),
+ onPressed: () => switchCameras(),
+ icon: Icon(
+ Platform.isIOS
+ ? Icons.flip_camera_ios_outlined
+ : Icons.flip_camera_android_outlined,
+ size: 24,
+ ),
),
);
}
@@ -943,7 +1017,11 @@ class CameraPickerState extends State
opacity: controller?.value.isRecordingVideo ?? false ? 0 : 1,
child: Padding(
padding: const EdgeInsets.all(20),
- child: Text(tips, style: const TextStyle(fontSize: 15)),
+ child: Text(
+ tips,
+ style: const TextStyle(fontSize: 15),
+ textAlign: TextAlign.center,
+ ),
),
);
}
@@ -962,64 +1040,61 @@ class CameraPickerState extends State
height: 118,
child: Row(
children: [
- if (controller?.value.isRecordingVideo != true)
- Expanded(child: buildBackButton(context, constraints))
- else
- const Spacer(),
+ const Spacer(),
Expanded(
child: Center(
- child: MergeSemantics(child: buildCaptureButton(constraints)),
+ child: buildCaptureButton(context, constraints),
),
),
- const Spacer(),
+ if (innerController != null && cameras.length > 1)
+ Expanded(
+ child: RotatedBox(
+ quarterTurns: !enableScaledPreview ? cameraQuarterTurns : 0,
+ child: buildCameraSwitch(context),
+ ),
+ )
+ else
+ const Spacer(),
],
),
);
}
- /// The back button near to the [buildCaptureButton].
- /// 靠近拍照键的返回键
- Widget buildBackButton(BuildContext context, BoxConstraints constraints) {
+ /// The back button.
+ /// 返回键
+ Widget buildBackButton(BuildContext context) {
return IconButton(
- onPressed: Navigator.of(context).pop,
+ onPressed: () => Navigator.of(context).maybePop(),
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
- icon: Container(
- alignment: Alignment.center,
- width: 27,
- height: 27,
- decoration: const BoxDecoration(
- color: Colors.white,
- shape: BoxShape.circle,
- ),
- child: const Icon(Icons.keyboard_arrow_down, color: Colors.black),
- ),
+ icon: const Icon(Icons.clear),
);
}
/// The shooting button.
/// 拍照按钮
- Widget buildCaptureButton(BoxConstraints constraints) {
+ Widget buildCaptureButton(BuildContext context, BoxConstraints constraints) {
const Size outerSize = Size.square(115);
const Size innerSize = Size.square(82);
- return Semantics(
- label: textDelegate.sActionShootingButtonTooltip,
- onTap: onTap,
- onTapHint: onTapHint,
- onLongPress: onLongPress,
- onLongPressHint: onLongPressHint,
- child: Listener(
- behavior: HitTestBehavior.opaque,
- onPointerUp: onPointerUp,
- onPointerMove: onPointerMove(constraints),
- child: GestureDetector(
- onTap: onTap,
- onLongPress: onLongPress,
- child: SizedBox.fromSize(
- size: outerSize,
- child: Stack(
- children: [
- Center(
- child: AnimatedContainer(
+ return MergeSemantics(
+ child: Semantics(
+ label: textDelegate.sActionShootingButtonTooltip,
+ onTap: onTap,
+ onTapHint: onTapHint,
+ onLongPress: onLongPress,
+ onLongPressHint: onLongPressHint,
+ child: Listener(
+ behavior: HitTestBehavior.opaque,
+ onPointerUp: onPointerUp,
+ onPointerMove: onPointerMove(constraints),
+ child: GestureDetector(
+ onTap: onTap,
+ onLongPress: onLongPress,
+ child: SizedBox.fromSize(
+ size: outerSize,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ AnimatedContainer(
duration: kThemeChangeDuration,
width: isShootingButtonAnimate
? outerSize.width
@@ -1029,7 +1104,7 @@ class CameraPickerState extends State
: innerSize.height,
padding: EdgeInsets.all(isShootingButtonAnimate ? 41 : 11),
decoration: BoxDecoration(
- color: theme.canvasColor.withOpacity(0.85),
+ color: Theme.of(context).canvasColor.withOpacity(0.85),
shape: BoxShape.circle,
),
child: const DecoratedBox(
@@ -1039,17 +1114,21 @@ class CameraPickerState extends State
),
),
),
- ),
- if ((innerController?.value.isRecordingVideo ?? false) &&
- isRecordingRestricted)
- CameraProgressButton(
- isAnimating: isShootingButtonAnimate,
- duration: pickerConfig.maximumRecordingDuration!,
- outerRadius: outerSize.width,
- ringsColor: theme.indicatorColor,
- ringsWidth: 2,
- ),
- ],
+ if ((innerController?.value.isRecordingVideo ?? false) &&
+ isRecordingRestricted)
+ RotatedBox(
+ quarterTurns:
+ !enableScaledPreview ? cameraQuarterTurns : 0,
+ child: CameraProgressButton(
+ isAnimating: isShootingButtonAnimate,
+ duration: pickerConfig.maximumRecordingDuration!,
+ outerRadius: outerSize.width,
+ ringsColor: theme.indicatorColor,
+ ringsWidth: 2,
+ ),
+ ),
+ ],
+ ),
),
),
),
@@ -1072,26 +1151,25 @@ class CameraPickerState extends State
opacity: value ? 1 : 0,
child: child,
),
- child: Center(child: Container(width: 1, color: color)),
+ child: Center(child: Container(width: 1.5, color: color)),
);
return ValueListenableBuilder(
- valueListenable: currentExposureOffset,
+ valueListenable: currentExposureSliderOffset,
builder: (_, double exposure, __) {
- final double effectiveTop = (size + gap) +
+ final double topByCurrentExposure =
(minAvailableExposureOffset.abs() - exposure) *
(height - size * 3) /
(maxAvailableExposureOffset - minAvailableExposureOffset);
- final double effectiveBottom = height - effectiveTop - size;
+ final double lineTop = size + topByCurrentExposure;
+ final double lineBottom = height - lineTop - size;
return Stack(
clipBehavior: Clip.none,
children: [
- Positioned.fill(top: effectiveTop + gap, child: lineWidget),
- Positioned.fill(bottom: effectiveBottom + gap, child: lineWidget),
+ Positioned.fill(top: lineTop + gap, child: lineWidget),
+ Positioned.fill(bottom: lineBottom + gap, child: lineWidget),
Positioned(
- top: (minAvailableExposureOffset.abs() - exposure) *
- (height - size * 3) /
- (maxAvailableExposureOffset - minAvailableExposureOffset),
+ top: topByCurrentExposure - gap,
child: Transform.rotate(
angle: exposure,
child: Icon(Icons.wb_sunny_outlined, size: size, color: color),
@@ -1127,6 +1205,7 @@ class CameraPickerState extends State
Widget buildFocusingPoint({
required CameraValue cameraValue,
required BoxConstraints constraints,
+ int quarterTurns = 0,
}) {
Widget buildControls(double size, double height) {
const double verticalGap = 3;
@@ -1171,41 +1250,78 @@ class CameraPickerState extends State
Widget buildFromPoint(Offset point) {
const double controllerWidth = 20;
final double pointWidth = constraints.maxWidth / 5;
+ final double lineHeight = pointWidth * 2.5;
final double exposureControlWidth =
pickerConfig.enableExposureControlOnPoint ? controllerWidth : 0;
final double width = pointWidth + exposureControlWidth + 2;
- final bool shouldReverseLayout = point.dx > constraints.maxWidth / 4 * 3;
- final double effectiveLeft = math.min(
- constraints.maxWidth - width,
- math.max(0, point.dx - width / 2),
- );
- final double effectiveTop = math.min(
- constraints.maxHeight - pointWidth * 3,
- math.max(0, point.dy - pointWidth * 3 / 2),
- );
+ final bool shouldReverseLayout = cameraQuarterTurns.isEven &&
+ enableScaledPreview &&
+ point.dx > constraints.maxWidth / 4 * 3;
+ final double effectiveLeft, effectiveTop, effectiveWidth, effectiveHeight;
+ if (cameraQuarterTurns.isOdd && !enableScaledPreview) {
+ effectiveLeft = math.min(
+ constraints.maxWidth - lineHeight,
+ math.max(0, point.dx - lineHeight / 2),
+ );
+ effectiveTop = math.min(
+ constraints.maxHeight - width,
+ math.max(0, point.dy - width / 2),
+ );
+ effectiveWidth = lineHeight;
+ effectiveHeight = width;
+ } else {
+ effectiveLeft = math.min(
+ constraints.maxWidth - width,
+ math.max(0, point.dx - width / 2),
+ );
+ effectiveTop = math.min(
+ constraints.maxHeight - lineHeight,
+ math.max(0, point.dy - lineHeight / 2),
+ );
+ effectiveWidth = width;
+ effectiveHeight = lineHeight;
+ }
return Positioned(
left: effectiveLeft,
top: effectiveTop,
- width: width,
- height: pointWidth * 3,
+ width: effectiveWidth,
+ height: effectiveHeight,
child: ExcludeSemantics(
- child: Row(
- textDirection:
- shouldReverseLayout ? TextDirection.rtl : TextDirection.ltr,
- children: [
- CameraFocusPoint(
- key: ValueKey(DateTime.now().millisecondsSinceEpoch),
- size: pointWidth,
- color: theme.iconTheme.color!,
- ),
- if (pickerConfig.enableExposureControlOnPoint)
- const SizedBox(width: 2),
- if (pickerConfig.enableExposureControlOnPoint)
- SizedBox.fromSize(
- size: Size(exposureControlWidth, pointWidth * 3),
- child: buildControls(controllerWidth, pointWidth * 3),
+ child: ValueListenableBuilder(
+ valueListenable: isFocusPointFadeOut,
+ builder: (BuildContext context, bool isFadeOut, Widget? child) {
+ Widget body = AnimatedOpacity(
+ curve: Curves.ease,
+ duration: _kDuration,
+ opacity: isFadeOut ? .5 : 1,
+ child: Row(
+ textDirection: shouldReverseLayout
+ ? TextDirection.rtl
+ : TextDirection.ltr,
+ children: [
+ child!,
+ if (pickerConfig.enableExposureControlOnPoint)
+ const SizedBox(width: 2),
+ if (pickerConfig.enableExposureControlOnPoint)
+ SizedBox.fromSize(
+ size: Size(exposureControlWidth, lineHeight),
+ child: buildControls(controllerWidth, lineHeight),
+ ),
+ ],
),
- ],
+ );
+ if (quarterTurns != 0) {
+ body = RotatedBox(quarterTurns: quarterTurns, child: body);
+ }
+ return body;
+ },
+ child: CameraFocusPoint(
+ key: ValueKey(point),
+ size: pointWidth,
+ color: cameraValue.exposureMode == ExposureMode.locked
+ ? _lockedColor
+ : theme.iconTheme.color!,
+ ),
),
),
);
@@ -1228,45 +1344,47 @@ class CameraPickerState extends State
BuildContext context,
BoxConstraints constraints,
) {
- void focus(TapUpDetails d) {
- // Only call exposure point updates when the controller is initialized.
- if (innerController?.value.isInitialized ?? false) {
- Feedback.forTap(context);
- setExposureAndFocusPoint(d.localPosition, constraints);
- }
- }
-
- return Positioned.fill(
- child: Semantics(
- label: textDelegate.sCameraPreviewLabel(
- innerController?.description.lensDirection,
- ),
- image: true,
- onTap: () {
- // Focus on the center point when using semantics tap.
- final Size size = MediaQuery.of(context).size;
- final TapUpDetails details = TapUpDetails(
- kind: PointerDeviceKind.touch,
- globalPosition: Offset(size.width / 2, size.height / 2),
+ return Semantics(
+ label: textDelegate.sCameraPreviewLabel(
+ innerController?.description.lensDirection,
+ ),
+ image: true,
+ onTap: () {
+ // Focus on the center point when using semantics tap.
+ final Size size = MediaQuery.of(context).size;
+ final TapUpDetails details = TapUpDetails(
+ kind: PointerDeviceKind.touch,
+ globalPosition: Offset(size.width / 2, size.height / 2),
+ );
+ requestFocusAndExposureOnPosition(details.localPosition, constraints);
+ },
+ onTapHint: textDelegate.sActionManuallyFocusHint,
+ sortKey: const OrdinalSortKey(1),
+ hidden: innerController == null,
+ excludeSemantics: true,
+ child: GestureDetector(
+ onTapUp: (TapUpDetails d) {
+ requestFocusAndExposureOnPosition(
+ d.localPosition,
+ constraints,
);
- focus(details);
},
- onTapHint: textDelegate.sActionManuallyFocusHint,
- sortKey: const OrdinalSortKey(1),
- hidden: innerController == null,
- excludeSemantics: true,
- child: GestureDetector(
- onTapUp: focus,
- behavior: HitTestBehavior.translucent,
- child: const SizedBox.expand(),
- ),
+ onLongPressStart: (LongPressStartDetails d) {
+ requestFocusAndExposureOnPosition(
+ d.localPosition,
+ constraints,
+ lock: true,
+ );
+ },
+ behavior: HitTestBehavior.translucent,
+ child: const SizedBox.expand(),
),
);
}
Widget buildCameraPreview({
required BuildContext context,
- required DeviceOrientation orientation,
+ required CameraValue cameraValue,
required BoxConstraints constraints,
}) {
Widget preview = Listener(
@@ -1291,20 +1409,51 @@ class CameraPickerState extends State
controller,
preview,
);
- preview = Center(child: transformedWidget ?? preview);
+ if (!enableScaledPreview) {
+ preview = Stack(
+ children: [
+ preview,
+ Positioned.fill(
+ child: ExcludeSemantics(
+ child: RotatedBox(
+ quarterTurns: cameraQuarterTurns,
+ child: Align(
+ alignment: Alignment.bottomCenter,
+ child: buildCaptureTips(innerController),
+ ),
+ ),
+ ),
+ ),
+ if (pickerConfig.enableSetExposure)
+ buildExposureDetector(context, constraints),
+ buildFocusingPoint(
+ cameraValue: cameraValue,
+ constraints: constraints,
+ quarterTurns: cameraQuarterTurns,
+ ),
+ if (pickerConfig.foregroundBuilder != null)
+ Positioned.fill(
+ child: pickerConfig.foregroundBuilder!(
+ context,
+ innerController,
+ ),
+ ),
+ ],
+ );
+ }
// Scale the preview if the config is enabled.
- if (pickerConfig.enableScaledPreview) {
+ if (enableScaledPreview) {
preview = Transform.scale(
scale: effectiveCameraScale(constraints, controller),
- child: preview,
- );
- }
- // Rotated the preview if the turns is valid.
- if (pickerConfig.cameraQuarterTurns % 4 != 0) {
- preview = RotatedBox(
- quarterTurns: -pickerConfig.cameraQuarterTurns,
- child: preview,
+ child: Center(child: transformedWidget ?? preview),
);
+ // Rotated the preview if the turns is valid.
+ if (isCameraRotated) {
+ preview = RotatedBox(
+ quarterTurns: -cameraQuarterTurns,
+ child: preview,
+ );
+ }
}
return RepaintBoundary(child: preview);
}
@@ -1341,7 +1490,8 @@ class CameraPickerState extends State
child: buildSettingActions(context),
),
const Spacer(),
- ExcludeSemantics(child: buildCaptureTips(innerController)),
+ if (enableScaledPreview)
+ ExcludeSemantics(child: buildCaptureTips(innerController)),
Semantics(
sortKey: const OrdinalSortKey(2),
hidden: innerController == null,
@@ -1359,57 +1509,111 @@ class CameraPickerState extends State
Widget buildBody(BuildContext context) {
return LayoutBuilder(
- builder: (BuildContext context, BoxConstraints constraints) => Stack(
- fit: StackFit.expand,
- alignment: Alignment.center,
- children: [
- ExcludeSemantics(
- child: buildInitializeWrapper(
- builder: (CameraValue v, Widget? w) => buildCameraPreview(
- context: context,
- orientation: v.deviceOrientation,
- constraints: constraints,
- ),
- ),
- ),
- if (pickerConfig.enableSetExposure)
- buildExposureDetector(context, constraints),
- buildInitializeWrapper(
- builder: (CameraValue v, _) => buildFocusingPoint(
- cameraValue: v,
- constraints: constraints,
- ),
+ builder: (BuildContext context, BoxConstraints constraints) {
+ Widget previewWidget = ExcludeSemantics(
+ child: buildInitializeWrapper(
+ builder: (CameraValue v, Widget? w) {
+ if (enableScaledPreview) {
+ return buildCameraPreview(
+ context: context,
+ cameraValue: v,
+ constraints: constraints,
+ );
+ }
+ return Align(
+ alignment: AlignmentDirectional.topCenter,
+ child: AspectRatio(
+ aspectRatio: 1 / v.aspectRatio,
+ child: LayoutBuilder(
+ builder: (BuildContext c, BoxConstraints constraints) {
+ return buildCameraPreview(
+ context: c,
+ cameraValue: v,
+ constraints: constraints,
+ );
+ },
+ ),
+ ),
+ );
+ },
),
- buildForegroundBody(context, constraints),
- if (pickerConfig.foregroundBuilder != null)
- Positioned.fill(
- child: pickerConfig.foregroundBuilder!(context, innerController),
+ );
+ if (!enableScaledPreview) {
+ previewWidget = Semantics(
+ label: textDelegate.sCameraPreviewLabel(
+ innerController?.description.lensDirection,
),
- ],
- ),
+ image: true,
+ onTap: () {
+ // Focus on the center point when using semantics tap.
+ final Size size = MediaQuery.of(context).size;
+ final TapUpDetails details = TapUpDetails(
+ kind: PointerDeviceKind.touch,
+ globalPosition: Offset(size.width / 2, size.height / 2),
+ );
+ requestFocusAndExposureOnPosition(
+ details.localPosition,
+ constraints,
+ );
+ },
+ onTapHint: textDelegate.sActionManuallyFocusHint,
+ sortKey: const OrdinalSortKey(1),
+ hidden: innerController == null,
+ excludeSemantics: true,
+ child: previewWidget,
+ );
+ }
+ return Stack(
+ fit: StackFit.expand,
+ alignment: Alignment.center,
+ children: [
+ previewWidget,
+ if (enableScaledPreview) ...[
+ if (pickerConfig.enableSetExposure)
+ buildExposureDetector(context, constraints),
+ buildInitializeWrapper(
+ builder: (CameraValue v, _) => buildFocusingPoint(
+ cameraValue: v,
+ constraints: constraints,
+ ),
+ ),
+ if (pickerConfig.foregroundBuilder != null)
+ Positioned.fill(
+ child:
+ pickerConfig.foregroundBuilder!(context, innerController),
+ ),
+ ],
+ buildForegroundBody(context, constraints),
+ ],
+ );
+ },
);
}
@override
Widget build(BuildContext context) {
- final MediaQueryData mq = MediaQuery.of(context);
+ Widget body = Builder(builder: buildBody);
+ if (isCameraRotated && enableScaledPreview) {
+ final MediaQueryData mq = MediaQuery.of(context);
+ body = RotatedBox(
+ quarterTurns: pickerConfig.cameraQuarterTurns,
+ child: MediaQuery(
+ data: mq.copyWith(
+ size: pickerConfig.cameraQuarterTurns.isOdd
+ ? mq.size.flipped
+ : mq.size,
+ ),
+ child: body,
+ ),
+ );
+ }
return AnnotatedRegion(
value: SystemUiOverlayStyle.light,
child: Theme(
data: theme,
child: Material(
color: Colors.black,
- child: RotatedBox(
- quarterTurns: pickerConfig.cameraQuarterTurns,
- child: MediaQuery(
- data: mq.copyWith(
- size: pickerConfig.cameraQuarterTurns.isOdd
- ? mq.size.flipped
- : mq.size,
- ),
- child: Builder(builder: buildBody),
- ),
- ),
+ child: body,
),
),
);
diff --git a/pubspec.yaml b/pubspec.yaml
index 5d9a397..f3e5131 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
name: wechat_camera_picker
description: A camera picker based on WeChat's UI which is a separate runnable extension to wechat_assets_picker.
repository: https://github.com/fluttercandies/flutter_wechat_camera_picker
-version: 3.8.0
+version: 4.0.0-dev.1
environment:
sdk: ">=2.15.0 <3.0.0"