diff --git a/CHANGELOG.md b/CHANGELOG.md index f99b40d..5212e5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ that can be found in the LICENSE file. --> See the [Migration Guide](guides/migration_guide.md) for breaking changes between versions. +## Unreleased + +*None.* + +## 4.3.2 + +### Fixes + +- Fix button displays when tap to record. +- Prevent camera description exceptions when initializing the camera in the lifecycle callback. + +### Improvements + +- Use more precise overlay styles. +- Switching between different lens with a single camera by default. +- Always delete the preview file when popping from the preview. + ## 4.3.1 ### Improvements diff --git a/example/lib/l10n/gen/app_localizations.dart b/example/lib/l10n/gen/app_localizations.dart index 5a5adda..165c1eb 100644 --- a/example/lib/l10n/gen/app_localizations.dart +++ b/example/lib/l10n/gen/app_localizations.dart @@ -8,6 +8,8 @@ import 'package:intl/intl.dart' as intl; import 'app_localizations_en.dart'; import 'app_localizations_zh.dart'; +// ignore_for_file: type=lint + /// Callers can lookup localized strings with an instance of AppLocalizations /// returned by `AppLocalizations.of(context)`. /// diff --git a/example/lib/l10n/gen/app_localizations_en.dart b/example/lib/l10n/gen/app_localizations_en.dart index e338608..0142624 100644 --- a/example/lib/l10n/gen/app_localizations_en.dart +++ b/example/lib/l10n/gen/app_localizations_en.dart @@ -1,5 +1,7 @@ import 'app_localizations.dart'; +// ignore_for_file: type=lint + /// The translations for English (`en`). class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); diff --git a/example/lib/l10n/gen/app_localizations_zh.dart b/example/lib/l10n/gen/app_localizations_zh.dart index e35ff17..3cd7b60 100644 --- a/example/lib/l10n/gen/app_localizations_zh.dart +++ b/example/lib/l10n/gen/app_localizations_zh.dart @@ -1,5 +1,7 @@ import 'app_localizations.dart'; +// ignore_for_file: type=lint + /// The translations for Chinese (`zh`). class AppLocalizationsZh extends AppLocalizations { AppLocalizationsZh([String locale = 'zh']) : super(locale); diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index 8932410..ee96304 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -11,7 +11,7 @@ import '../extensions/l10n_extensions.dart'; import '../main.dart'; import '../models/picker_method.dart'; import '../widgets/method_list_view.dart'; -import '../widgets/selected_assets_view.dart'; +import '../widgets/selected_assets_list_view.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -22,13 +22,12 @@ class HomePage extends StatefulWidget { class _MyHomePageState extends State { final ValueNotifier isDisplayingDetail = ValueNotifier(true); - final ValueNotifier selectedAsset = - ValueNotifier(null); + List assets = []; Future selectAssets(PickMethod model) async { - final AssetEntity? result = await model.method(context); + final result = await model.method(context); if (result != null) { - selectedAsset.value = result; + assets = [...assets, result]; if (mounted) { setState(() {}); } @@ -38,6 +37,7 @@ class _MyHomePageState extends State { Widget header(BuildContext context) { return Container( margin: const EdgeInsetsDirectional.only(top: 30), + padding: const EdgeInsets.symmetric(horizontal: 20.0), height: 60, child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -57,11 +57,13 @@ class _MyHomePageState extends State { children: [ Semantics( sortKey: const OrdinalSortKey(0), - child: Text( - context.l10n.appTitle, - style: Theme.of(context).textTheme.titleLarge, - overflow: TextOverflow.fade, - maxLines: 1, + child: FittedBox( + child: Text( + context.l10n.appTitle, + style: Theme.of(context).textTheme.titleLarge, + overflow: TextOverflow.fade, + maxLines: 1, + ), ), ), Semantics( @@ -107,19 +109,15 @@ class _MyHomePageState extends State { onSelectMethod: selectAssets, ), ), - ValueListenableBuilder( - valueListenable: selectedAsset, - builder: (_, AssetEntity? asset, __) { - if (asset == null) { - return const SizedBox.shrink(); - } - return SelectedAssetView( - asset: asset, - isDisplayingDetail: isDisplayingDetail, - onRemoveAsset: () => selectedAsset.value = null, - ); - }, - ), + if (assets.isNotEmpty) + SelectedAssetsListView( + assets: assets, + isDisplayingDetail: isDisplayingDetail, + onRemoveAsset: (int index) { + assets.removeAt(index); + setState(() {}); + }, + ), ], ), ), diff --git a/example/lib/widgets/selected_assets_list_view.dart b/example/lib/widgets/selected_assets_list_view.dart new file mode 100644 index 0000000..70c2cc1 --- /dev/null +++ b/example/lib/widgets/selected_assets_list_view.dart @@ -0,0 +1,147 @@ +// Copyright 2019 The FlutterCandies author. All rights reserved. +// Use of this source code is governed by an Apache license that can be found +// in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:wechat_camera_picker/wechat_camera_picker.dart'; + +import '../extensions/l10n_extensions.dart'; +import 'asset_widget_builder.dart'; +import 'preview_asset_widget.dart'; + +class SelectedAssetsListView extends StatelessWidget { + const SelectedAssetsListView({ + super.key, + required this.assets, + required this.isDisplayingDetail, + required this.onRemoveAsset, + }); + + final List assets; + final ValueNotifier isDisplayingDetail; + final void Function(int index) onRemoveAsset; + + Widget _selectedAssetWidget(BuildContext context, int index) { + final AssetEntity asset = assets.elementAt(index); + return ValueListenableBuilder( + valueListenable: isDisplayingDetail, + builder: (_, bool value, __) => GestureDetector( + onTap: () async { + if (value) { + showDialog( + context: context, + builder: (BuildContext context) => GestureDetector( + onTap: Navigator.of(context).pop, + child: Center(child: PreviewAssetWidget(asset)), + ), + ); + } + }, + child: RepaintBoundary( + child: ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: AssetWidgetBuilder( + entity: asset, + isDisplayingDetail: value, + ), + ), + ), + ), + ); + } + + Widget _selectedAssetDeleteButton(BuildContext context, int index) { + return GestureDetector( + onTap: () => onRemoveAsset(index), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + color: Theme.of(context).canvasColor.withOpacity(0.5), + ), + child: const Icon(Icons.close, size: 18.0), + ), + ); + } + + Widget selectedAssetsListView(BuildContext context) { + return Expanded( + child: ListView.builder( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 8.0), + scrollDirection: Axis.horizontal, + itemCount: assets.length, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 16.0, + ), + child: AspectRatio( + aspectRatio: 1.0, + child: Stack( + children: [ + Positioned.fill(child: _selectedAssetWidget(context, index)), + ValueListenableBuilder( + valueListenable: isDisplayingDetail, + builder: (_, bool value, __) => AnimatedPositioned( + duration: kThemeAnimationDuration, + top: value ? 6.0 : -30.0, + right: value ? 6.0 : -30.0, + child: _selectedAssetDeleteButton(context, index), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: isDisplayingDetail, + builder: (_, bool value, __) => AnimatedContainer( + duration: kThemeChangeDuration, + curve: Curves.easeInOut, + height: assets.isNotEmpty + ? value + ? 120.0 + : 80.0 + : 40.0, + child: Column( + children: [ + SizedBox( + height: 20.0, + child: GestureDetector( + onTap: () { + if (assets.isNotEmpty) { + isDisplayingDetail.value = !isDisplayingDetail.value; + } + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(context.l10n.selectedAssetsText), + if (assets.isNotEmpty) + Padding( + padding: const EdgeInsetsDirectional.only(start: 10.0), + child: Icon( + value ? Icons.arrow_downward : Icons.arrow_upward, + size: 18.0, + ), + ), + ], + ), + ), + ), + selectedAssetsListView(context), + ], + ), + ), + ); + } +} diff --git a/example/lib/widgets/selected_assets_view.dart b/example/lib/widgets/selected_assets_view.dart deleted file mode 100644 index f16d4c1..0000000 --- a/example/lib/widgets/selected_assets_view.dart +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2019 The FlutterCandies author. All rights reserved. -// Use of this source code is governed by an Apache license that can be found -// in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:wechat_camera_picker/wechat_camera_picker.dart'; - -import '../extensions/l10n_extensions.dart'; -import 'asset_widget_builder.dart'; -import 'preview_asset_widget.dart'; - -class SelectedAssetView extends StatelessWidget { - const SelectedAssetView({ - super.key, - required this.asset, - required this.isDisplayingDetail, - required this.onRemoveAsset, - }); - - final AssetEntity asset; - final ValueNotifier isDisplayingDetail; - final VoidCallback onRemoveAsset; - - Widget _selectedAssetWidget(BuildContext context) { - return ValueListenableBuilder( - valueListenable: isDisplayingDetail, - builder: (_, bool value, __) => GestureDetector( - onTap: () async { - if (value) { - showDialog( - context: context, - builder: (BuildContext context) => GestureDetector( - onTap: Navigator.of(context).pop, - child: Center(child: PreviewAssetWidget(asset)), - ), - ); - } - }, - child: RepaintBoundary( - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: AssetWidgetBuilder(entity: asset, isDisplayingDetail: value), - ), - ), - ), - ); - } - - Widget _selectedAssetDeleteButton(BuildContext context) { - return GestureDetector( - onTap: onRemoveAsset, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: Theme.of(context).canvasColor.withOpacity(0.5), - ), - child: const Icon(Icons.close, size: 18), - ), - ); - } - - Widget _selectedAssetView(BuildContext context) { - return Container( - alignment: Alignment.center, - padding: const EdgeInsets.all(16), - child: AspectRatio( - aspectRatio: 1, - child: Stack( - children: [ - Positioned.fill(child: _selectedAssetWidget(context)), - ValueListenableBuilder( - valueListenable: isDisplayingDetail, - builder: (_, bool value, Widget? child) => AnimatedPositioned( - duration: kThemeAnimationDuration, - top: value ? 6 : -30, - right: value ? 6 : -30, - child: child!, - ), - child: _selectedAssetDeleteButton(context), - ), - ], - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: isDisplayingDetail, - builder: (_, bool value, Widget? child) => AnimatedContainer( - duration: kThemeChangeDuration, - curve: Curves.easeInOut, - height: value ? 120 : 80, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 20, - child: GestureDetector( - onTap: () => - isDisplayingDetail.value = !isDisplayingDetail.value, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(context.l10n.selectedAssetsText), - Padding( - padding: const EdgeInsetsDirectional.only(start: 10), - child: Icon( - value ? Icons.arrow_downward : Icons.arrow_upward, - size: 18, - ), - ), - ], - ), - ), - ), - Expanded(child: child!), - ], - ), - ), - child: _selectedAssetView(context), - ); - } -} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5f597dc..3ae7edd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: wechat_camera_picker_demo description: A new Flutter project. -version: 4.3.1+37 +version: 4.3.2+38 publish_to: none environment: diff --git a/lib/src/states/camera_picker_state.dart b/lib/src/states/camera_picker_state.dart index e42f636..88e0720 100644 --- a/lib/src/states/camera_picker_state.dart +++ b/lib/src/states/camera_picker_state.dart @@ -8,6 +8,7 @@ import 'dart:math' as math; import 'package:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -42,7 +43,7 @@ class CameraPickerState extends State /// Available cameras. /// 可用的相机实例 - late List cameras; + List cameras = []; /// Whether the controller is handling method calls. /// 相机控制器是否在处理方法调用 @@ -269,7 +270,9 @@ class CameraPickerState extends State void didChangeAppLifecycleState(AppLifecycleState state) { final CameraController? c = innerController; if (state == AppLifecycleState.resumed && !accessDenied) { - initCameras(cameraDescription: currentCamera); + initCameras( + cameraDescription: cameras.elementAtOrNull(currentCameraIndex), + ); } else if (c == null || !c.value.isInitialized) { // App state changed before we got the chance to initialize. return; @@ -605,10 +608,17 @@ class CameraPickerState extends State if (controller.value.isTakingPicture || controller.value.isRecordingVideo) { return; } - ++currentCameraIndex; - if (currentCameraIndex == cameras.length) { - currentCameraIndex = 0; - } + final preferredCameras = CameraLensDirection.values + .map((e) => cameras.firstWhereOrNull((c) => c.lensDirection == e)) + .whereType() + .map((e) => cameras.indexOf(e)) + .toList(); + int index = preferredCameras.indexOf(currentCameraIndex); + ++index; + if (index == preferredCameras.length) { + index = 0; + } + currentCameraIndex = preferredCameras[index]; initCameras(cameraDescription: currentCamera); } @@ -1066,6 +1076,7 @@ class CameraPickerState extends State handleErrorWithHandler(e, s, pickerConfig.onError); } finally { safeSetState(() { + isCaptureButtonTapDown = false; isControllerBusy = false; isShootingButtonAnimate = false; }); @@ -1410,7 +1421,11 @@ class CameraPickerState extends State onTap: onTap, onLongPress: onLongPress, onTapDown: (_) => safeSetState(() => isCaptureButtonTapDown = true), - onTapUp: (_) => safeSetState(() => isCaptureButtonTapDown = false), + onTapUp: (_) => safeSetState(() { + if (!enableTapRecording) { + isCaptureButtonTapDown = false; + } + }), onTapCancel: () => safeSetState(() => isCaptureButtonTapDown = false), onLongPressStart: (_) => @@ -1972,7 +1987,11 @@ class CameraPickerState extends State ); } return AnnotatedRegion( - value: SystemUiOverlayStyle.light, + value: const SystemUiOverlayStyle( + systemNavigationBarIconBrightness: Brightness.light, + statusBarIconBrightness: Brightness.light, + statusBarBrightness: Brightness.dark, + ), child: Theme( data: theme, child: Material( diff --git a/lib/src/states/camera_picker_viewer_state.dart b/lib/src/states/camera_picker_viewer_state.dart index 0787c0a..36340b4 100644 --- a/lib/src/states/camera_picker_viewer_state.dart +++ b/lib/src/states/camera_picker_viewer_state.dart @@ -204,9 +204,6 @@ class CameraPickerViewerState extends State { if (isSavingEntity) { return; } - if (previewFile.existsSync()) { - previewFile.delete(); - } Navigator.of(context).pop(); }, padding: EdgeInsets.zero, @@ -368,15 +365,27 @@ class CameraPickerViewerState extends State { if (!hasLoaded) { return const SizedBox.shrink(); } - return Material( - color: Colors.black, - child: Stack( - fit: StackFit.expand, - children: [ - buildPreview(context), - buildForeground(context), - if (isSavingEntity) buildLoading(context), - ], + return PopScope( + canPop: true, + // ignore: deprecated_member_use + onPopInvoked: (didPop) { + if (didPop && previewFile.existsSync()) { + previewFile.delete().catchError((e, s) { + handleErrorWithHandler(e, s, onError); + return previewFile; + }); + } + }, + child: Material( + color: Colors.black, + child: Stack( + fit: StackFit.expand, + children: [ + buildPreview(context), + buildForeground(context), + if (isSavingEntity) buildLoading(context), + ], + ), ), ); } diff --git a/lib/src/widgets/camera_picker.dart b/lib/src/widgets/camera_picker.dart index b3b3a01..06bcf7b 100644 --- a/lib/src/widgets/camera_picker.dart +++ b/lib/src/widgets/camera_picker.dart @@ -1,7 +1,6 @@ // Copyright 2019 The FlutterCandies author. All rights reserved. // Use of this source code is governed by an Apache license that can be found // in the LICENSE file. -// ignore_for_file: deprecated_member_use import 'dart:async'; @@ -93,6 +92,7 @@ class CameraPicker extends StatefulWidget { primaryContainer: Colors.grey[900], secondary: themeColor, secondaryContainer: themeColor, + // ignore: deprecated_member_use background: Colors.grey[900]!, surface: Colors.grey[900]!, brightness: Brightness.dark, @@ -100,6 +100,7 @@ class CameraPicker extends StatefulWidget { onPrimary: Colors.black, onSecondary: Colors.black, onSurface: Colors.white, + // ignore: deprecated_member_use onBackground: Colors.white, onError: Colors.black, ), diff --git a/pubspec.yaml b/pubspec.yaml index 0cc05c4..7bb1da0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: wechat_camera_picker -version: 4.3.1 +version: 4.3.2 description: | A camera picker for Flutter projects based on WeChat's UI, which is also a separate runnable extension to the @@ -22,14 +22,15 @@ dependencies: flutter: sdk: flutter - wechat_picker_library: ^1.0.0 + wechat_picker_library: ^1.0.2 camera: ^0.10.0 camera_android: '>=0.10.0 <0.10.9+4' camera_platform_interface: ^2.1.5 + collection: '>=1.18.0 <2.0.0' path: ^1.8.0 - photo_manager: ^3.0.0 + photo_manager: ^3.2.3 photo_manager_image_provider: ^2.0.0 sensors_plus: '>=4.0.0 <6.0.0' video_player: ^2.7.0