mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-13 18:58:53 +01:00
Add scrollbar in photos page
This commit is contained in:
parent
80147c35ed
commit
835426e534
6 changed files with 376 additions and 62 deletions
|
@ -54,7 +54,10 @@ class AlbumViewer extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _AlbumViewerState extends State<AlbumViewer>
|
||||
with SelectableItemStreamListMixin<AlbumViewer>, TickerProviderStateMixin {
|
||||
with
|
||||
WidgetsBindingObserver,
|
||||
SelectableItemStreamListMixin<AlbumViewer>,
|
||||
TickerProviderStateMixin {
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
@ -24,6 +25,7 @@ import 'package:nc_photos/use_case/remove.dart';
|
|||
import 'package:nc_photos/use_case/update_album.dart';
|
||||
import 'package:nc_photos/widget/album_picker_dialog.dart';
|
||||
import 'package:nc_photos/widget/home_app_bar.dart';
|
||||
import 'package:nc_photos/widget/measure.dart';
|
||||
import 'package:nc_photos/widget/photo_list_item.dart';
|
||||
import 'package:nc_photos/widget/popup_menu_zoom.dart';
|
||||
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
|
||||
|
@ -42,7 +44,7 @@ class HomePhotos extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _HomePhotosState extends State<HomePhotos>
|
||||
with SelectableItemStreamListMixin<HomePhotos> {
|
||||
with WidgetsBindingObserver, SelectableItemStreamListMixin<HomePhotos> {
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
|
@ -62,6 +64,11 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onMaxExtentChanged(double maxExtent) {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
int get itemStreamListCellSize => _thumbSize;
|
||||
|
||||
|
@ -77,32 +84,50 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, ScanDirBlocState state) {
|
||||
return Stack(
|
||||
children: [
|
||||
buildItemStreamListOuter(
|
||||
context,
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
accentColor: AppTheme.getOverscrollIndicatorColor(context),
|
||||
),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
_buildAppBar(context),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: buildItemStreamList(context),
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
if (_prevListWidth == null) {
|
||||
_prevListWidth = constraints.maxWidth;
|
||||
}
|
||||
if (constraints.maxWidth != _prevListWidth) {
|
||||
_log.info(
|
||||
"[_buildContent] updateListHeight: list viewport width changed");
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => updateListHeight());
|
||||
_prevListWidth = constraints.maxWidth;
|
||||
}
|
||||
|
||||
final scrollExtent = _getScrollViewExtent(constraints);
|
||||
return Stack(
|
||||
children: [
|
||||
buildItemStreamListOuter(
|
||||
context,
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
accentColor: AppTheme.getOverscrollIndicatorColor(context),
|
||||
),
|
||||
child: DraggableScrollbar.semicircle(
|
||||
controller: _scrollController,
|
||||
overrideMaxScrollExtent: scrollExtent,
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
_buildAppBar(context),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: buildItemStreamList(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state is ScanDirBlocLoading)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: const LinearProgressIndicator(),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (state is ScanDirBlocLoading)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: const LinearProgressIndicator(),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildAppBar(BuildContext context) {
|
||||
|
@ -152,40 +177,45 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
}
|
||||
|
||||
Widget _buildNormalAppBar(BuildContext context) {
|
||||
return HomeSliverAppBar(
|
||||
account: widget.account,
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.zoom_in),
|
||||
tooltip: AppLocalizations.of(context).zoomTooltip,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuZoom(
|
||||
initialValue: _thumbZoomLevel,
|
||||
minValue: -1,
|
||||
maxValue: 2,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_setThumbZoomLevel(value.round());
|
||||
});
|
||||
Pref.inst().setHomePhotosZoomLevel(_thumbZoomLevel);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
menuActions: [
|
||||
PopupMenuItem(
|
||||
value: _menuValueRefresh,
|
||||
child: Text(AppLocalizations.of(context).refreshMenuLabel),
|
||||
),
|
||||
],
|
||||
onSelectedMenuActions: (option) {
|
||||
switch (option) {
|
||||
case _menuValueRefresh:
|
||||
_onRefreshSelected();
|
||||
break;
|
||||
}
|
||||
return SliverMeasureExtent(
|
||||
onChange: (extent) {
|
||||
_appBarExtent = extent;
|
||||
},
|
||||
child: HomeSliverAppBar(
|
||||
account: widget.account,
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.zoom_in),
|
||||
tooltip: AppLocalizations.of(context).zoomTooltip,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuZoom(
|
||||
initialValue: _thumbZoomLevel,
|
||||
minValue: -1,
|
||||
maxValue: 2,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_setThumbZoomLevel(value.round());
|
||||
});
|
||||
Pref.inst().setHomePhotosZoomLevel(_thumbZoomLevel);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
menuActions: [
|
||||
PopupMenuItem(
|
||||
value: _menuValueRefresh,
|
||||
child: Text(AppLocalizations.of(context).refreshMenuLabel),
|
||||
),
|
||||
],
|
||||
onSelectedMenuActions: (option) {
|
||||
switch (option) {
|
||||
case _menuValueRefresh:
|
||||
_onRefreshSelected();
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -411,6 +441,22 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
}
|
||||
}
|
||||
|
||||
/// Return the estimated scroll extent of the custom scroll view, or null
|
||||
double _getScrollViewExtent(BoxConstraints constraints) {
|
||||
if (calculatedMaxExtent != null &&
|
||||
constraints.hasBoundedHeight &&
|
||||
_appBarExtent != null) {
|
||||
// scroll extent = list height - widget viewport height + sliver app bar height
|
||||
final scrollExtent =
|
||||
calculatedMaxExtent - constraints.maxHeight + _appBarExtent;
|
||||
_log.info(
|
||||
"[_getScrollViewExtent] $calculatedMaxExtent - ${constraints.maxHeight} + $_appBarExtent = $scrollExtent");
|
||||
return scrollExtent;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
int get _thumbSize {
|
||||
switch (_thumbZoomLevel) {
|
||||
case -1:
|
||||
|
@ -434,6 +480,11 @@ class _HomePhotosState extends State<HomePhotos>
|
|||
|
||||
var _thumbZoomLevel = 0;
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
double _prevListWidth;
|
||||
double _appBarExtent;
|
||||
|
||||
static final _log = Logger("widget.home_photos._HomePhotosState");
|
||||
static const _menuValueRefresh = 0;
|
||||
}
|
||||
|
|
76
lib/widget/measure.dart
Normal file
76
lib/widget/measure.dart
Normal file
|
@ -0,0 +1,76 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
typedef void OnWidgetSizeChanged(Size size);
|
||||
|
||||
/// See: https://stackoverflow.com/a/60868972
|
||||
class MeasureSize extends SingleChildRenderObjectWidget {
|
||||
final OnWidgetSizeChanged onChange;
|
||||
|
||||
const MeasureSize({
|
||||
Key key,
|
||||
@required this.onChange,
|
||||
@required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _MeasureSizeRenderObject(onChange);
|
||||
}
|
||||
}
|
||||
|
||||
class _MeasureSizeRenderObject extends RenderProxyBox {
|
||||
Size oldSize;
|
||||
final OnWidgetSizeChanged onChange;
|
||||
|
||||
_MeasureSizeRenderObject(this.onChange);
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
super.performLayout();
|
||||
|
||||
Size newSize = child.size;
|
||||
if (oldSize == newSize) return;
|
||||
|
||||
oldSize = newSize;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
onChange(newSize);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class SliverMeasureExtent extends SingleChildRenderObjectWidget {
|
||||
const SliverMeasureExtent({
|
||||
Key key,
|
||||
@required this.onChange,
|
||||
@required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _SliverMeasureExtentRenderObject(onChange);
|
||||
}
|
||||
|
||||
final void Function(double) onChange;
|
||||
}
|
||||
|
||||
class _SliverMeasureExtentRenderObject extends RenderProxySliver {
|
||||
_SliverMeasureExtentRenderObject(this.onChange);
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
super.performLayout();
|
||||
|
||||
double newExent = child.geometry.scrollExtent;
|
||||
if (_oldExtent == newExent) {
|
||||
return;
|
||||
}
|
||||
|
||||
_oldExtent = newExent;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => onChange(newExent));
|
||||
}
|
||||
|
||||
final void Function(double) onChange;
|
||||
|
||||
double _oldExtent;
|
||||
}
|
|
@ -27,11 +27,36 @@ abstract class SelectableItemStreamListItem {
|
|||
final StaggeredTile staggeredTile;
|
||||
}
|
||||
|
||||
mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
||||
mixin SelectableItemStreamListMixin<T extends StatefulWidget>
|
||||
on State<T>, WidgetsBindingObserver {
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_keyboardFocus.requestFocus();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_prevOrientation = MediaQuery.of(context).orientation;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
didChangeMetrics() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final orientation = MediaQuery.of(context).orientation;
|
||||
if (orientation != _prevOrientation) {
|
||||
_log.info(
|
||||
"[didChangeMetrics] updateListHeight: orientation changed: $orientation");
|
||||
_prevOrientation = orientation;
|
||||
updateListHeight();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildItemStreamListOuter(
|
||||
|
@ -53,9 +78,16 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
|
||||
Widget buildItemStreamList(BuildContext context) {
|
||||
return SliverStaggeredGrid.extentBuilder(
|
||||
// need to rebuild grid after cell size changed
|
||||
key: ValueKey(itemStreamListCellSize),
|
||||
// need to rebuild grid after cell size changed
|
||||
final cellSize = itemStreamListCellSize;
|
||||
if (cellSize != _prevItemStreamListCellSize) {
|
||||
_log.info("[buildItemStreamList] updateListHeight: cell size changed");
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => updateListHeight());
|
||||
_prevItemStreamListCellSize = cellSize;
|
||||
}
|
||||
_gridKey = _GridKey(cellSize);
|
||||
return _SliverStaggeredGrid.extentBuilder(
|
||||
key: _gridKey,
|
||||
maxCrossAxisExtent: itemStreamListCellSize.toDouble(),
|
||||
itemCount: _items.length,
|
||||
itemBuilder: _buildItem,
|
||||
|
@ -63,11 +95,34 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
);
|
||||
}
|
||||
|
||||
void onMaxExtentChanged(double newExtent) {}
|
||||
|
||||
@protected
|
||||
void clearSelectedItems() {
|
||||
_selectedItems.clear();
|
||||
}
|
||||
|
||||
@protected
|
||||
void updateListHeight() {
|
||||
try {
|
||||
final renderObj = _gridKey.currentContext.findRenderObject()
|
||||
as _RenderSliverStaggeredGrid;
|
||||
final maxExtent = renderObj.calculateExtent();
|
||||
_log.info("[updateListHeight] Max extent: $maxExtent");
|
||||
if (maxExtent == 0) {
|
||||
// ?
|
||||
_calculatedMaxExtent = null;
|
||||
} else {
|
||||
_calculatedMaxExtent = maxExtent;
|
||||
onMaxExtentChanged(maxExtent);
|
||||
}
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout("[updateListHeight] Failed while calculateMaxScrollExtent", e,
|
||||
stacktrace);
|
||||
_calculatedMaxExtent = null;
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
bool get isSelectionMode => _selectedItems.isNotEmpty;
|
||||
|
||||
|
@ -98,11 +153,17 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
} catch (_) {}
|
||||
_lastSelectPosition = newLastSelectPosition;
|
||||
|
||||
_log.info("[itemStreamListItems] updateListHeight: list item changed");
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => updateListHeight());
|
||||
}
|
||||
|
||||
@protected
|
||||
int get itemStreamListCellSize;
|
||||
|
||||
@protected
|
||||
double get calculatedMaxExtent => _calculatedMaxExtent;
|
||||
|
||||
Widget _buildItem(BuildContext context, int index) {
|
||||
final item = _items[index];
|
||||
if (item.isSelectable) {
|
||||
|
@ -211,10 +272,15 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
|
||||
int _lastSelectPosition;
|
||||
bool _isRangeSelectionMode = false;
|
||||
int _prevItemStreamListCellSize;
|
||||
double _calculatedMaxExtent;
|
||||
Orientation _prevOrientation;
|
||||
|
||||
final _items = <SelectableItemStreamListItem>[];
|
||||
final _selectedItems = <SelectableItemStreamListItem>{};
|
||||
|
||||
GlobalObjectKey _gridKey;
|
||||
|
||||
/// used to gain focus on web for keyboard support
|
||||
final _keyboardFocus = FocusNode();
|
||||
|
||||
|
@ -222,6 +288,10 @@ mixin SelectableItemStreamListMixin<T extends StatefulWidget> on State<T> {
|
|||
"widget.selectable_item_stream_list_mixin.SelectableItemStreamListMixin");
|
||||
}
|
||||
|
||||
class _GridKey extends GlobalObjectKey {
|
||||
const _GridKey(Object value) : super(value);
|
||||
}
|
||||
|
||||
class _SelectableItemWidget extends StatelessWidget {
|
||||
_SelectableItemWidget({
|
||||
Key key,
|
||||
|
@ -275,3 +345,104 @@ class _SelectableItemWidget extends StatelessWidget {
|
|||
final VoidCallback onLongPress;
|
||||
final Widget child;
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class _SliverStaggeredGrid extends SliverStaggeredGrid {
|
||||
_SliverStaggeredGrid.extentBuilder({
|
||||
Key key,
|
||||
@required double maxCrossAxisExtent,
|
||||
@required IndexedStaggeredTileBuilder staggeredTileBuilder,
|
||||
@required IndexedWidgetBuilder itemBuilder,
|
||||
@required int itemCount,
|
||||
double mainAxisSpacing = 0,
|
||||
double crossAxisSpacing = 0,
|
||||
}) : super(
|
||||
key: key,
|
||||
gridDelegate: SliverStaggeredGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||
mainAxisSpacing: mainAxisSpacing,
|
||||
crossAxisSpacing: crossAxisSpacing,
|
||||
staggeredTileBuilder: staggeredTileBuilder,
|
||||
staggeredTileCount: itemCount,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
itemBuilder,
|
||||
childCount: itemCount,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
RenderSliverStaggeredGrid createRenderObject(BuildContext context) {
|
||||
final element = context as SliverVariableSizeBoxAdaptorElement;
|
||||
_renderObject = _RenderSliverStaggeredGrid(
|
||||
childManager: element, gridDelegate: gridDelegate);
|
||||
return _renderObject;
|
||||
}
|
||||
|
||||
_RenderSliverStaggeredGrid get renderObject => _renderObject;
|
||||
|
||||
_RenderSliverStaggeredGrid _renderObject;
|
||||
}
|
||||
|
||||
class _RenderSliverStaggeredGrid extends RenderSliverStaggeredGrid
|
||||
with WidgetsBindingObserver {
|
||||
_RenderSliverStaggeredGrid({
|
||||
@required RenderSliverVariableSizeBoxChildManager childManager,
|
||||
@required SliverStaggeredGridDelegate gridDelegate,
|
||||
}) : super(childManager: childManager, gridDelegate: gridDelegate);
|
||||
|
||||
/// Calculate the height of this staggered grid view
|
||||
///
|
||||
/// This basically requires a complete layout of every child, so only call
|
||||
/// when necessary
|
||||
double calculateExtent() {
|
||||
childManager.didStartLayout();
|
||||
childManager.setDidUnderflow(false);
|
||||
|
||||
double product = 0;
|
||||
final configuration = gridDelegate.getConfiguration(constraints);
|
||||
final mainAxisOffsets = configuration.generateMainAxisOffsets();
|
||||
|
||||
// Iterate through all children
|
||||
for (var index = 0; true; index++) {
|
||||
var geometry = RenderSliverStaggeredGrid.getSliverStaggeredGeometry(
|
||||
index, configuration, mainAxisOffsets);
|
||||
if (geometry == null) {
|
||||
// There are either no children, or we are past the end of all our children.
|
||||
break;
|
||||
}
|
||||
|
||||
final bool hasTrailingScrollOffset = geometry.hasTrailingScrollOffset;
|
||||
RenderBox child;
|
||||
if (!hasTrailingScrollOffset) {
|
||||
// Layout the child to compute its tailingScrollOffset.
|
||||
final constraints =
|
||||
BoxConstraints.tightFor(width: geometry.crossAxisExtent);
|
||||
child = addAndLayoutChild(index, constraints, parentUsesSize: true);
|
||||
geometry = geometry.copyWith(mainAxisExtent: paintExtentOf(child));
|
||||
}
|
||||
|
||||
if (child != null) {
|
||||
final childParentData =
|
||||
child.parentData as SliverVariableSizeBoxAdaptorParentData;
|
||||
childParentData.layoutOffset = geometry.scrollOffset;
|
||||
childParentData.crossAxisOffset = geometry.crossAxisOffset;
|
||||
assert(childParentData.index == index);
|
||||
}
|
||||
|
||||
final double endOffset =
|
||||
geometry.trailingScrollOffset + configuration.mainAxisSpacing;
|
||||
for (var i = 0; i < geometry.crossAxisCellCount; i++) {
|
||||
mainAxisOffsets[i + geometry.blockIndex] = endOffset;
|
||||
}
|
||||
if (endOffset > product) {
|
||||
product = endOffset;
|
||||
}
|
||||
}
|
||||
childManager.didFinishLayout();
|
||||
return product;
|
||||
}
|
||||
|
||||
static final _log = Logger(
|
||||
"widget.selectable_item_stream_list_mixin._RenderSliverStaggeredGrid");
|
||||
}
|
||||
|
|
|
@ -148,6 +148,15 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
draggable_scrollbar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "v0.1.0-nc-photos-1"
|
||||
resolved-ref: d97c03ddea7b6bae0e170430e0cb7c27e725ff2a
|
||||
url: "https://gitlab.com/nkming2/flutter-draggable-scrollbar"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -31,6 +31,10 @@ dependencies:
|
|||
bloc: ^7.0.0
|
||||
cached_network_image: ^3.0.0
|
||||
connectivity: ^3.0.2
|
||||
draggable_scrollbar:
|
||||
git:
|
||||
url: https://gitlab.com/nkming2/flutter-draggable-scrollbar
|
||||
ref: v0.1.0-nc-photos-1
|
||||
equatable: ^2.0.0
|
||||
event_bus: ^2.0.0
|
||||
exifdart:
|
||||
|
|
Loading…
Reference in a new issue