Refactoring: extract page viewer logic

This commit is contained in:
Ming Ming 2021-07-31 23:38:26 +08:00
parent 80a64b35bc
commit ed19b1bd82
2 changed files with 280 additions and 212 deletions

View file

@ -0,0 +1,253 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/platform/k.dart' as platform_k;
class HorizontalPageViewer extends StatefulWidget {
HorizontalPageViewer({
Key? key,
required this.pageCount,
required this.pageBuilder,
this.initialPage = 0,
HorizontalPageViewerController? controller,
this.viewportFraction = 1,
this.canSwitchPage = true,
}) : controller = controller ?? HorizontalPageViewerController(),
super(key: key) {
this.controller._pageController = PageController(
initialPage: initialPage,
viewportFraction: viewportFraction,
keepPage: false,
);
}
@override
createState() => _HorizontalPageViewerState();
final int initialPage;
final int pageCount;
final Widget Function(BuildContext context, int index) pageBuilder;
final HorizontalPageViewerController controller;
final double viewportFraction;
final bool canSwitchPage;
}
class _HorizontalPageViewerState extends State<HorizontalPageViewer> {
@override
void initState() {
super.initState();
_pageFocus.requestFocus();
}
@override
build(BuildContext context) {
if (!_hasInit) {
_updateNavigationState(widget.initialPage);
_hasInit = true;
}
return platform_k.isWeb
? _buildWebContent(context)
: _buildContent(context);
}
Widget _buildWebContent(BuildContext context) {
assert(platform_k.isWeb);
// support switching pages with keyboard on web
return RawKeyboardListener(
onKey: (ev) {
if (!widget.canSwitchPage) {
return;
}
if (ev.isKeyPressed(LogicalKeyboardKey.arrowLeft)) {
_switchToLeft();
} else if (ev.isKeyPressed(LogicalKeyboardKey.arrowRight)) {
_switchToRight();
}
},
focusNode: _pageFocus,
child: _buildContent(context),
);
}
Widget _buildContent(BuildContext context) {
return Stack(
children: [
PageView.builder(
controller: widget.controller._pageController,
itemCount: widget.pageCount,
itemBuilder: widget.pageBuilder,
physics: !platform_k.isWeb && widget.canSwitchPage
? null
: const NeverScrollableScrollPhysics(),
),
if (platform_k.isWeb) ..._buildNavigationButtons(context),
],
);
}
List<Widget> _buildNavigationButtons(BuildContext context) {
return [
if (_canSwitchRight)
Align(
alignment: Alignment.centerRight,
child: Material(
type: MaterialType.transparency,
child: Visibility(
visible: widget.canSwitchPage,
child: AnimatedOpacity(
opacity: _isShowRight ? 1.0 : 0.0,
duration: k.animationDurationShort,
child: MouseRegion(
onEnter: (details) {
setState(() {
_isShowRight = true;
});
},
onExit: (details) {
setState(() {
_isShowRight = false;
});
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 36),
child: IconButton(
icon: Icon(
Icons.arrow_forward_ios_outlined,
color: Colors.white,
),
onPressed: _switchToRight,
),
),
),
),
),
),
),
if (_canSwitchLeft)
Align(
alignment: Alignment.centerLeft,
child: Material(
type: MaterialType.transparency,
child: Visibility(
visible: widget.canSwitchPage,
child: AnimatedOpacity(
opacity: _isShowLeft ? 1.0 : 0.0,
duration: k.animationDurationShort,
child: MouseRegion(
onEnter: (details) {
setState(() {
_isShowLeft = true;
});
},
onExit: (details) {
setState(() {
_isShowLeft = false;
});
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 36),
child: IconButton(
icon: Icon(
Icons.arrow_back_ios_outlined,
color: Colors.white,
),
onPressed: _switchToLeft,
),
),
),
),
),
),
),
];
}
/// Switch to the previous image in the stream
void _switchToPrev() {
widget.controller._pageController
.previousPage(
duration: k.animationDurationNormal, curve: Curves.easeInOut)
.whenComplete(() => _updateNavigationState(
widget.controller._pageController.page!.round()));
}
/// Switch to the next image in the stream
void _switchToNext() {
widget.controller._pageController
.nextPage(duration: k.animationDurationNormal, curve: Curves.easeInOut)
.whenComplete(() => _updateNavigationState(
widget.controller._pageController.page!.round()));
}
/// Switch to the image on the "left", what that means depend on the current
/// text direction
void _switchToLeft() {
if (Directionality.of(context) == TextDirection.ltr) {
_switchToPrev();
} else {
_switchToNext();
}
}
/// Switch to the image on the "right", what that means depend on the current
/// text direction
void _switchToRight() {
if (Directionality.of(context) == TextDirection.ltr) {
_switchToNext();
} else {
_switchToPrev();
}
}
/// Update the navigation state for [page]
void _updateNavigationState(int page) {
// currently useless to run on non-web platform
if (!platform_k.isWeb) {
return;
}
final hasNext = page < widget.pageCount - 1;
final hasPrev = page > 0;
final hasLeft =
Directionality.of(context) == TextDirection.ltr ? hasPrev : hasNext;
if (_canSwitchLeft != hasLeft) {
setState(() {
_canSwitchLeft = hasLeft;
if (!_canSwitchLeft) {
_isShowLeft = false;
}
});
}
final hasRight =
Directionality.of(context) == TextDirection.ltr ? hasNext : hasPrev;
if (_canSwitchRight != hasRight) {
setState(() {
_canSwitchRight = hasRight;
if (!_canSwitchRight) {
_isShowRight = false;
}
});
}
}
var _hasInit = false;
var _canSwitchRight = true;
var _canSwitchLeft = true;
var _isShowRight = false;
var _isShowLeft = false;
/// used to gain focus on web for keyboard support
final _pageFocus = FocusNode();
}
class HorizontalPageViewerController {
int get currentPage => _pageController.hasClients
? _pageController.page!.round()
: _pageController.initialPage;
late PageController _pageController;
}

View file

@ -23,6 +23,7 @@ import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/remove.dart'; import 'package:nc_photos/use_case/remove.dart';
import 'package:nc_photos/widget/animated_visibility.dart'; import 'package:nc_photos/widget/animated_visibility.dart';
import 'package:nc_photos/widget/horizontal_page_viewer.dart';
import 'package:nc_photos/widget/image_viewer.dart'; import 'package:nc_photos/widget/image_viewer.dart';
import 'package:nc_photos/widget/video_viewer.dart'; import 'package:nc_photos/widget/video_viewer.dart';
import 'package:nc_photos/widget/viewer_bottom_app_bar.dart'; import 'package:nc_photos/widget/viewer_bottom_app_bar.dart';
@ -67,28 +68,13 @@ class Viewer extends StatefulWidget {
} }
class _ViewerState extends State<Viewer> { class _ViewerState extends State<Viewer> {
@override
void initState() {
super.initState();
_pageController = PageController(
initialPage: widget.startIndex,
viewportFraction: 1.05,
keepPage: false);
_pageFocus.requestFocus();
}
@override @override
build(BuildContext context) { build(BuildContext context) {
if (!_hasInit) {
_updateNavigationState(widget.startIndex);
_hasInit = true;
}
return AppTheme( return AppTheme(
child: Scaffold( child: Scaffold(
body: Builder( body: Builder(
builder: (context) => platform_k.isWeb builder: _buildContent,
? _buildWebContent(context) ),
: _buildContent(context)),
), ),
); );
} }
@ -99,25 +85,6 @@ class _ViewerState extends State<Viewer> {
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values); SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
} }
Widget _buildWebContent(BuildContext context) {
assert(platform_k.isWeb);
// support switching pages with keyboard on web
return RawKeyboardListener(
onKey: (ev) {
if (!_canSwitchPage()) {
return;
}
if (ev.isKeyPressed(LogicalKeyboardKey.arrowLeft)) {
_switchToLeftImage();
} else if (ev.isKeyPressed(LogicalKeyboardKey.arrowRight)) {
_switchToRightImage();
}
},
focusNode: _pageFocus,
child: _buildContent(context),
);
}
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
@ -128,21 +95,20 @@ class _ViewerState extends State<Viewer> {
child: Stack( child: Stack(
children: [ children: [
Container(color: Colors.black), Container(color: Colors.black),
if (!_pageController.hasClients || if (!_isViewerLoaded ||
!_pageStates[_pageController.page!.round()]!.hasLoaded) !_pageStates[_viewerController.currentPage]!.hasLoaded)
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: const CircularProgressIndicator(), child: const CircularProgressIndicator(),
), ),
PageView.builder( HorizontalPageViewer(
controller: _pageController, pageCount: widget.streamFiles.length,
itemCount: widget.streamFiles.length, pageBuilder: _buildPage,
itemBuilder: _buildPage, initialPage: widget.startIndex,
physics: !platform_k.isWeb && _canSwitchPage() controller: _viewerController,
? null viewportFraction: _viewportFraction,
: const NeverScrollableScrollPhysics(), canSwitchPage: _canSwitchPage(),
), ),
if (platform_k.isWeb) ..._buildNavigationButtons(context),
_buildBottomAppBar(context), _buildBottomAppBar(context),
_buildAppBar(context), _buildAppBar(context),
], ],
@ -150,85 +116,6 @@ class _ViewerState extends State<Viewer> {
); );
} }
List<Widget> _buildNavigationButtons(BuildContext context) {
return [
if (_canSwitchRight)
Align(
alignment: Alignment.centerRight,
child: Material(
type: MaterialType.transparency,
child: Visibility(
visible: _canSwitchPage(),
child: AnimatedOpacity(
opacity: _isShowRight ? 1.0 : 0.0,
duration: k.animationDurationShort,
child: MouseRegion(
onEnter: (details) {
setState(() {
_isShowRight = true;
});
},
onExit: (details) {
setState(() {
_isShowRight = false;
});
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 36),
child: IconButton(
icon: Icon(
Icons.arrow_forward_ios_outlined,
color: Colors.white,
),
onPressed: _switchToRightImage,
),
),
),
),
),
),
),
if (_canSwitchLeft)
Align(
alignment: Alignment.centerLeft,
child: Material(
type: MaterialType.transparency,
child: Visibility(
visible: _canSwitchPage(),
child: AnimatedOpacity(
opacity: _isShowLeft ? 1.0 : 0.0,
duration: k.animationDurationShort,
child: MouseRegion(
onEnter: (details) {
setState(() {
_isShowLeft = true;
});
},
onExit: (details) {
setState(() {
_isShowLeft = false;
});
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 36),
child: IconButton(
icon: Icon(
Icons.arrow_back_ios_outlined,
color: Colors.white,
),
onPressed: _switchToLeftImage,
),
),
),
),
),
),
),
];
}
Widget _buildAppBar(BuildContext context) { Widget _buildAppBar(BuildContext context) {
return Wrap( return Wrap(
children: [ children: [
@ -311,7 +198,7 @@ class _ViewerState extends State<Viewer> {
} }
return FractionallySizedBox( return FractionallySizedBox(
widthFactor: 1 / _pageController.viewportFraction, widthFactor: 1 / _viewportFraction,
child: NotificationListener<ScrollNotification>( child: NotificationListener<ScrollNotification>(
onNotification: (notif) => _onPageContentScrolled(notif, index), onNotification: (notif) => _onPageContentScrolled(notif, index),
child: SingleChildScrollView( child: SingleChildScrollView(
@ -419,14 +306,14 @@ class _ViewerState extends State<Viewer> {
// upward, open the pane to its minimal size // upward, open the pane to its minimal size
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
setState(() { setState(() {
_openDetailPane(_pageController.page!.toInt(), _openDetailPane(_viewerController.currentPage,
shouldAnimate: true); shouldAnimate: true);
}); });
}); });
} else if (scrollPos.userScrollDirection == ScrollDirection.forward) { } else if (scrollPos.userScrollDirection == ScrollDirection.forward) {
// downward, close the pane // downward, close the pane
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
_closeDetailPane(_pageController.page!.toInt(), _closeDetailPane(_viewerController.currentPage,
shouldAnimate: true); shouldAnimate: true);
}); });
} }
@ -438,7 +325,7 @@ class _ViewerState extends State<Viewer> {
void _onImageLoaded(int index) { void _onImageLoaded(int index) {
// currently pageview doesn't pre-load pages, we do it manually // currently pageview doesn't pre-load pages, we do it manually
// don't pre-load if user already navigated away // don't pre-load if user already navigated away
if (_pageController.page!.round() == index && if (_viewerController.currentPage == index &&
!_pageStates[index]!.hasLoaded) { !_pageStates[index]!.hasLoaded) {
_log.info("[_onImageLoaded] Pre-loading nearby images"); _log.info("[_onImageLoaded] Pre-loading nearby images");
if (index > 0) { if (index > 0) {
@ -455,15 +342,17 @@ class _ViewerState extends State<Viewer> {
} }
setState(() { setState(() {
_pageStates[index]!.hasLoaded = true; _pageStates[index]!.hasLoaded = true;
_isViewerLoaded = true;
}); });
} }
} }
void _onVideoLoaded(int index) { void _onVideoLoaded(int index) {
if (_pageController.page!.round() == index && if (_viewerController.currentPage == index &&
!_pageStates[index]!.hasLoaded) { !_pageStates[index]!.hasLoaded) {
setState(() { setState(() {
_pageStates[index]!.hasLoaded = true; _pageStates[index]!.hasLoaded = true;
_isViewerLoaded = true;
}); });
} }
} }
@ -508,19 +397,19 @@ class _ViewerState extends State<Viewer> {
void _onDetailsPressed() { void _onDetailsPressed() {
if (!_isDetailPaneActive) { if (!_isDetailPaneActive) {
setState(() { setState(() {
_openDetailPane(_pageController.page!.toInt(), shouldAnimate: true); _openDetailPane(_viewerController.currentPage, shouldAnimate: true);
}); });
} }
} }
void _onSharePressed(BuildContext context) { void _onSharePressed(BuildContext context) {
assert(platform_k.isAndroid); assert(platform_k.isAndroid);
final file = widget.streamFiles[_pageController.page!.round()]; final file = widget.streamFiles[_viewerController.currentPage];
ShareHandler().shareFiles(context, widget.account, [file]); ShareHandler().shareFiles(context, widget.account, [file]);
} }
void _onDownloadPressed(BuildContext context) async { void _onDownloadPressed(BuildContext context) async {
final file = widget.streamFiles[_pageController.page!.round()]; final file = widget.streamFiles[_viewerController.currentPage];
_log.info("[_onDownloadPressed] Downloading file: ${file.path}"); _log.info("[_onDownloadPressed] Downloading file: ${file.path}");
var controller = SnackBarManager().showSnackBar(SnackBar( var controller = SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.of(context).downloadProcessingNotification), content: Text(L10n.of(context).downloadProcessingNotification),
@ -582,7 +471,7 @@ class _ViewerState extends State<Viewer> {
} }
void _onDeletePressed(BuildContext context) async { void _onDeletePressed(BuildContext context) async {
final file = widget.streamFiles[_pageController.page!.round()]; final file = widget.streamFiles[_viewerController.currentPage];
_log.info("[_onDeletePressed] Removing file: ${file.path}"); _log.info("[_onDeletePressed] Removing file: ${file.path}");
var controller = SnackBarManager().showSnackBar(SnackBar( var controller = SnackBarManager().showSnackBar(SnackBar(
content: Text(L10n.of(context).deleteProcessingNotification), content: Text(L10n.of(context).deleteProcessingNotification),
@ -687,99 +576,25 @@ class _ViewerState extends State<Viewer> {
_isClosingDetailPane = false; _isClosingDetailPane = false;
} }
/// Switch to the previous image in the stream
void _switchToPrevImage() {
_pageController
.previousPage(
duration: k.animationDurationNormal, curve: Curves.easeInOut)
.whenComplete(
() => _updateNavigationState(_pageController.page!.round()));
}
/// Switch to the next image in the stream
void _switchToNextImage() {
_pageController
.nextPage(duration: k.animationDurationNormal, curve: Curves.easeInOut)
.whenComplete(
() => _updateNavigationState(_pageController.page!.round()));
}
/// Switch to the image on the "left", what that means depend on the current
/// text direction
void _switchToLeftImage() {
if (Directionality.of(context) == TextDirection.ltr) {
_switchToPrevImage();
} else {
_switchToNextImage();
}
}
/// Switch to the image on the "right", what that means depend on the current
/// text direction
void _switchToRightImage() {
if (Directionality.of(context) == TextDirection.ltr) {
_switchToNextImage();
} else {
_switchToPrevImage();
}
}
/// Update the navigation state for [page]
void _updateNavigationState(int page) {
// currently useless to run on non-web platform
if (!platform_k.isWeb) {
return;
}
final hasNext = page < widget.streamFiles.length - 1;
final hasPrev = page > 0;
final hasLeft =
Directionality.of(context) == TextDirection.ltr ? hasPrev : hasNext;
if (_canSwitchLeft != hasLeft) {
setState(() {
_canSwitchLeft = hasLeft;
if (!_canSwitchLeft) {
_isShowLeft = false;
}
});
}
final hasRight =
Directionality.of(context) == TextDirection.ltr ? hasNext : hasPrev;
if (_canSwitchRight != hasRight) {
setState(() {
_canSwitchRight = hasRight;
if (!_canSwitchRight) {
_isShowRight = false;
}
});
}
}
bool _canSwitchPage() => !_isZoomed; bool _canSwitchPage() => !_isZoomed;
bool _canOpenDetailPane() => !_isZoomed; bool _canOpenDetailPane() => !_isZoomed;
bool _canZoom() => !_isDetailPaneActive; bool _canZoom() => !_isDetailPaneActive;
var _hasInit = false;
var _isShowAppBar = true; var _isShowAppBar = true;
var _isShowDetailPane = false; var _isShowDetailPane = false;
var _isDetailPaneActive = false; var _isDetailPaneActive = false;
var _isClosingDetailPane = false; var _isClosingDetailPane = false;
var _canSwitchRight = true;
var _canSwitchLeft = true;
var _isShowRight = false;
var _isShowLeft = false;
var _isZoomed = false; var _isZoomed = false;
late PageController _pageController; final _viewerController = HorizontalPageViewerController();
bool _isViewerLoaded = false;
final _pageStates = <int, _PageState>{}; final _pageStates = <int, _PageState>{};
/// used to gain focus on web for keyboard support
final _pageFocus = FocusNode();
static final _log = Logger("widget.viewer._ViewerState"); static final _log = Logger("widget.viewer._ViewerState");
static const _viewportFraction = 1.05;
} }
class _PageState { class _PageState {