mod: 图片预览
This commit is contained in:
77
lib/pages/preview/controller.dart
Normal file
77
lib/pages/preview/controller.dart
Normal file
@ -0,0 +1,77 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:typed_data';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class PreviewController extends GetxController {
|
||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
RxInt initialPage = 0.obs;
|
||||
RxInt currentPage = 1.obs;
|
||||
RxList imgList = [].obs;
|
||||
bool storage = true;
|
||||
bool videos = true;
|
||||
bool photos = true;
|
||||
bool visiable = true;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
if (Get.arguments != null) {
|
||||
initialPage.value = Get.arguments['initialPage']!;
|
||||
currentPage.value = Get.arguments['initialPage']! + 1;
|
||||
imgList.value = Get.arguments['imgList'];
|
||||
}
|
||||
}
|
||||
|
||||
requestPermission() async {
|
||||
Map<Permission, PermissionStatus> statuses = await [
|
||||
Permission.storage,
|
||||
// Permission.photos
|
||||
].request();
|
||||
|
||||
final info = statuses[Permission.storage].toString();
|
||||
// final photosInfo = statuses[Permission.photos].toString();
|
||||
|
||||
print('授权状态:$info');
|
||||
}
|
||||
|
||||
// 图片保存
|
||||
void onSaveImg() async {
|
||||
var response = await Dio().get(imgList[initialPage.value],
|
||||
options: Options(responseType: ResponseType.bytes));
|
||||
final result = await ImageGallerySaver.saveImage(
|
||||
Uint8List.fromList(response.data),
|
||||
quality: 100,
|
||||
name: "pic_vvex${DateTime.now().toString().split('-').join()}");
|
||||
if (result != null) {
|
||||
if (result['isSuccess']) {
|
||||
print('已保存到相册');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图片分享
|
||||
void onShareImg() async {
|
||||
requestPermission();
|
||||
var response = await Dio().get(imgList[initialPage.value],
|
||||
options: Options(responseType: ResponseType.bytes));
|
||||
final temp = await getTemporaryDirectory();
|
||||
String imgName =
|
||||
"pic_vvex${DateTime.now().toString().split('-').join()}.jpg";
|
||||
var path = '${temp.path}/$imgName';
|
||||
File(path).writeAsBytesSync(response.data);
|
||||
Share.shareXFiles([XFile(path)], subject: imgList[initialPage.value]);
|
||||
}
|
||||
|
||||
// 浏览器中查看
|
||||
void onBrowserImg() async {
|
||||
Utils.openURL(imgList[initialPage.value]);
|
||||
}
|
||||
}
|
4
lib/pages/preview/index.dart
Normal file
4
lib/pages/preview/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library preview;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
188
lib/pages/preview/view.dart
Normal file
188
lib/pages/preview/view.dart
Normal file
@ -0,0 +1,188 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:pilipala/common/widgets/appbar.dart';
|
||||
import 'controller.dart';
|
||||
|
||||
typedef DoubleClickAnimationListener = void Function();
|
||||
|
||||
class ImagePreview extends StatefulWidget {
|
||||
const ImagePreview({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ImagePreviewState createState() => _ImagePreviewState();
|
||||
}
|
||||
|
||||
class _ImagePreviewState extends State<ImagePreview>
|
||||
with TickerProviderStateMixin {
|
||||
final PreviewController _previewController = Get.put(PreviewController());
|
||||
late AnimationController animationController;
|
||||
late AnimationController _doubleClickAnimationController;
|
||||
Animation<double>? _doubleClickAnimation;
|
||||
late DoubleClickAnimationListener _doubleClickAnimationListener;
|
||||
List<double> doubleTapScales = <double>[1.0, 2.0];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
animationController = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 400));
|
||||
_doubleClickAnimationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 250), vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animationController.dispose();
|
||||
_doubleClickAnimationController.dispose();
|
||||
clearGestureDetailsCache();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBarWidget(
|
||||
controller: animationController,
|
||||
visible: _previewController.visiable,
|
||||
child: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
title: Obx(
|
||||
() => Text.rich(
|
||||
TextSpan(children: [
|
||||
TextSpan(text: _previewController.currentPage.toString()),
|
||||
const TextSpan(text: ' / '),
|
||||
TextSpan(text: _previewController.imgList.length.toString()),
|
||||
]),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
tooltip: 'action',
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
|
||||
PopupMenuItem(
|
||||
value: 'share',
|
||||
onTap: _previewController.onShareImg,
|
||||
child: const Text('分享'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'save',
|
||||
onTap: _previewController.onSaveImg,
|
||||
child: const Text('保存'),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'browser',
|
||||
onTap: _previewController.onBrowserImg,
|
||||
child: const Text('浏览器中查看'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: GestureDetector(
|
||||
onTap: () {
|
||||
_previewController.visiable = !_previewController.visiable;
|
||||
setState(() {});
|
||||
},
|
||||
child: ExtendedImageGesturePageView.builder(
|
||||
controller: ExtendedPageController(
|
||||
initialPage: _previewController.initialPage.value,
|
||||
pageSpacing: 0,
|
||||
),
|
||||
onPageChanged: (int index) {
|
||||
_previewController.initialPage.value = index;
|
||||
_previewController.currentPage.value = index + 1;
|
||||
},
|
||||
canScrollPage: (GestureDetails? gestureDetails) =>
|
||||
gestureDetails!.totalScale! <= 1.0,
|
||||
preloadPagesCount: 2,
|
||||
itemCount: _previewController.imgList.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return ExtendedImage.network(
|
||||
_previewController.imgList[index],
|
||||
fit: BoxFit.contain,
|
||||
mode: ExtendedImageMode.gesture,
|
||||
onDoubleTap: (ExtendedImageGestureState state) {
|
||||
final Offset? pointerDownPosition = state.pointerDownPosition;
|
||||
final double? begin = state.gestureDetails!.totalScale;
|
||||
double end;
|
||||
|
||||
//remove old
|
||||
_doubleClickAnimation
|
||||
?.removeListener(_doubleClickAnimationListener);
|
||||
|
||||
//stop pre
|
||||
_doubleClickAnimationController.stop();
|
||||
|
||||
//reset to use
|
||||
_doubleClickAnimationController.reset();
|
||||
|
||||
if (begin == doubleTapScales[0]) {
|
||||
end = doubleTapScales[1];
|
||||
} else {
|
||||
end = doubleTapScales[0];
|
||||
}
|
||||
|
||||
_doubleClickAnimationListener = () {
|
||||
state.handleDoubleTap(
|
||||
scale: _doubleClickAnimation!.value,
|
||||
doubleTapPosition: pointerDownPosition);
|
||||
};
|
||||
_doubleClickAnimation = _doubleClickAnimationController
|
||||
.drive(Tween<double>(begin: begin, end: end));
|
||||
|
||||
_doubleClickAnimation!
|
||||
.addListener(_doubleClickAnimationListener);
|
||||
|
||||
_doubleClickAnimationController.forward();
|
||||
},
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
if (state.extendedImageLoadState == LoadState.loading) {
|
||||
final ImageChunkEvent? loadingProgress =
|
||||
state.loadingProgress;
|
||||
final double? progress =
|
||||
loadingProgress?.expectedTotalBytes != null
|
||||
? loadingProgress!.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: 150.0,
|
||||
child: LinearProgressIndicator(value: progress),
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Text('${((progress ?? 0.0) * 100).toInt()}%'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
initGestureConfigHandler: (ExtendedImageState state) {
|
||||
return GestureConfig(
|
||||
inPageView: true,
|
||||
initialScale: 1.0,
|
||||
maxScale: 5.0,
|
||||
animationMaxScale: 6.0,
|
||||
initialAlignment: InitialAlignment.center,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _previewController.onSaveImg(),
|
||||
child: const Icon(Icons.save_alt_rounded),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/video/reply/item.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
@ -367,25 +368,66 @@ InlineSpan buildContent(BuildContext context, content) {
|
||||
|
||||
// 图片渲染
|
||||
if (content.pictures.isNotEmpty) {
|
||||
spanChilds.add(
|
||||
const TextSpan(text: '\n'),
|
||||
);
|
||||
for (var i = 0; i < content.pictures.length; i++) {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: SizedBox(
|
||||
height: 180,
|
||||
child: NetworkImgLayer(
|
||||
src: content.pictures[i]['img_src'],
|
||||
width: 200,
|
||||
height: 200 *
|
||||
content.pictures[i]['img_height'] /
|
||||
content.pictures[i]['img_width'],
|
||||
),
|
||||
),
|
||||
List<Widget> list = [];
|
||||
List picList = [];
|
||||
int len = content.pictures.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
picList.add(content.pictures[i]['img_src']);
|
||||
list.add(
|
||||
LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed('/preview',
|
||||
arguments: {'initialPage': i, 'imgList': picList});
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
src: content.pictures[i]['img_src'],
|
||||
width: box.maxWidth,
|
||||
height: box.maxWidth,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
double maxWidth = box.maxWidth;
|
||||
double crossCount = len < 3 ? 2 : 3;
|
||||
double height = maxWidth /
|
||||
crossCount *
|
||||
(len % crossCount == 0
|
||||
? len ~/ crossCount
|
||||
: len ~/ crossCount + 1) +
|
||||
6;
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
height: height,
|
||||
child: GridView(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// 子Item排列规则
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//横轴元素个数
|
||||
crossAxisCount: crossCount.toInt(),
|
||||
//纵轴间距
|
||||
mainAxisSpacing: 4.0,
|
||||
//横轴间距
|
||||
crossAxisSpacing: 4.0,
|
||||
//子组件宽高长度比例
|
||||
// childAspectRatio: 1,
|
||||
),
|
||||
//GridView中使用的子Widegt
|
||||
children: list,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// spanChilds.add(TextSpan(text: matchMember));
|
||||
return TextSpan(children: spanChilds);
|
||||
|
Reference in New Issue
Block a user