rebuild performance review
This commit is contained in:
parent
1def93bd1e
commit
df474a3f66
14 changed files with 455 additions and 357 deletions
|
@ -145,7 +145,7 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
double getTileExtent(String routeName) => _prefs.getDouble(tileExtentPrefixKey + routeName) ?? 0;
|
double getTileExtent(String routeName) => _prefs.getDouble(tileExtentPrefixKey + routeName) ?? 0;
|
||||||
|
|
||||||
// do not notify, as tile extents are only used internally by `TileExtentManager`
|
// do not notify, as tile extents are only used internally by `TileExtentController`
|
||||||
// and should not trigger rebuilding by change notification
|
// and should not trigger rebuilding by change notification
|
||||||
void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue, notify: false);
|
void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue, notify: false);
|
||||||
|
|
||||||
|
|
|
@ -24,127 +24,143 @@ import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||||
import 'package:aves/widgets/common/grid/sliver.dart';
|
import 'package:aves/widgets/common/grid/sliver.dart';
|
||||||
import 'package:aves/widgets/common/identity/empty.dart';
|
import 'package:aves/widgets/common/identity/empty.dart';
|
||||||
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
||||||
|
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
|
||||||
import 'package:aves/widgets/common/scaling.dart';
|
import 'package:aves/widgets/common/scaling.dart';
|
||||||
import 'package:aves/widgets/common/tile_extent_manager.dart';
|
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class ThumbnailCollection extends StatelessWidget {
|
class ThumbnailCollection extends StatelessWidget {
|
||||||
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
|
|
||||||
final ValueNotifier<double> _tileExtentNotifier = ValueNotifier(0);
|
final ValueNotifier<double> _tileExtentNotifier = ValueNotifier(0);
|
||||||
final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false);
|
|
||||||
final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable');
|
|
||||||
|
|
||||||
static const columnCountDefault = 4;
|
|
||||||
static const extentMin = 46.0;
|
|
||||||
static const spacing = 0.0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: LayoutBuilder(
|
child: TileExtentControllerProvider(
|
||||||
builder: (context, constraints) {
|
controller: TileExtentController(
|
||||||
final viewportSize = constraints.biggest;
|
settingsRouteKey: context.currentRouteName,
|
||||||
assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.');
|
extentNotifier: _tileExtentNotifier,
|
||||||
if (viewportSize.isEmpty) return SizedBox.shrink();
|
columnCountDefault: 4,
|
||||||
|
extentMin: 46,
|
||||||
final tileExtentManager = TileExtentManager(
|
spacing: 0,
|
||||||
settingsRouteKey: context.currentRouteName,
|
),
|
||||||
extentNotifier: _tileExtentNotifier,
|
child: _ThumbnailCollectionContent(),
|
||||||
columnCountDefault: columnCountDefault,
|
|
||||||
extentMin: extentMin,
|
|
||||||
spacing: spacing,
|
|
||||||
)..applyTileExtent(viewportSize: viewportSize);
|
|
||||||
final cacheExtent = tileExtentManager.getEffectiveExtentMax(viewportSize) * 2;
|
|
||||||
final scrollController = PrimaryScrollController.of(context);
|
|
||||||
|
|
||||||
// do not replace by Provider.of<CollectionLens>
|
|
||||||
// so that view updates on collection filter changes
|
|
||||||
return Consumer<CollectionLens>(
|
|
||||||
builder: (context, collection, child) {
|
|
||||||
final scrollView = AnimationLimiter(
|
|
||||||
child: CollectionScrollView(
|
|
||||||
scrollableKey: _scrollableKey,
|
|
||||||
collection: collection,
|
|
||||||
appBar: CollectionAppBar(
|
|
||||||
appBarHeightNotifier: _appBarHeightNotifier,
|
|
||||||
collection: collection,
|
|
||||||
),
|
|
||||||
appBarHeightNotifier: _appBarHeightNotifier,
|
|
||||||
isScrollingNotifier: _isScrollingNotifier,
|
|
||||||
scrollController: scrollController,
|
|
||||||
cacheExtent: cacheExtent,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final scaler = GridScaleGestureDetector<AvesEntry>(
|
|
||||||
tileExtentManager: tileExtentManager,
|
|
||||||
scrollableKey: _scrollableKey,
|
|
||||||
appBarHeightNotifier: _appBarHeightNotifier,
|
|
||||||
viewportSize: viewportSize,
|
|
||||||
gridBuilder: (center, extent, child) => CustomPaint(
|
|
||||||
// painting the thumbnail half-border on top of the grid yields artifacts,
|
|
||||||
// so we use a `foregroundPainter` to cover them instead
|
|
||||||
foregroundPainter: GridPainter(
|
|
||||||
center: center,
|
|
||||||
extent: extent,
|
|
||||||
spacing: tileExtentManager.spacing,
|
|
||||||
strokeWidth: DecoratedThumbnail.borderWidth * 2,
|
|
||||||
color: DecoratedThumbnail.borderColor,
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
scaledBuilder: (entry, extent) => DecoratedThumbnail(
|
|
||||||
entry: entry,
|
|
||||||
extent: extent,
|
|
||||||
selectable: false,
|
|
||||||
highlightable: false,
|
|
||||||
),
|
|
||||||
getScaledItemTileRect: (context, entry) {
|
|
||||||
final sectionedListLayout = context.read<SectionedListLayout<AvesEntry>>();
|
|
||||||
return sectionedListLayout.getTileRect(entry) ?? Rect.zero;
|
|
||||||
},
|
|
||||||
onScaled: (entry) => context.read<HighlightInfo>().set(entry),
|
|
||||||
child: scrollView,
|
|
||||||
);
|
|
||||||
|
|
||||||
final selector = GridSelectionGestureDetector(
|
|
||||||
selectable: AvesApp.mode == AppMode.main,
|
|
||||||
collection: collection,
|
|
||||||
scrollController: scrollController,
|
|
||||||
appBarHeightNotifier: _appBarHeightNotifier,
|
|
||||||
child: scaler,
|
|
||||||
);
|
|
||||||
|
|
||||||
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
|
||||||
valueListenable: _tileExtentNotifier,
|
|
||||||
builder: (context, tileExtent, child) => SectionedEntryListLayoutProvider(
|
|
||||||
collection: collection,
|
|
||||||
scrollableWidth: viewportSize.width,
|
|
||||||
tileExtent: tileExtent,
|
|
||||||
columnCount: tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent),
|
|
||||||
tileBuilder: (entry) => InteractiveThumbnail(
|
|
||||||
key: ValueKey(entry.contentId),
|
|
||||||
collection: collection,
|
|
||||||
entry: entry,
|
|
||||||
tileExtent: tileExtent,
|
|
||||||
isScrollingNotifier: _isScrollingNotifier,
|
|
||||||
),
|
|
||||||
child: selector,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return sectionedListLayoutProvider;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ThumbnailCollectionContent extends StatelessWidget {
|
||||||
|
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
|
||||||
|
final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false);
|
||||||
|
final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final scrollController = PrimaryScrollController.of(context);
|
||||||
|
return Consumer<CollectionLens>(
|
||||||
|
builder: (context, collection, child) {
|
||||||
|
final scrollView = AnimationLimiter(
|
||||||
|
child: CollectionScrollView(
|
||||||
|
scrollableKey: _scrollableKey,
|
||||||
|
collection: collection,
|
||||||
|
appBar: CollectionAppBar(
|
||||||
|
appBarHeightNotifier: _appBarHeightNotifier,
|
||||||
|
collection: collection,
|
||||||
|
),
|
||||||
|
appBarHeightNotifier: _appBarHeightNotifier,
|
||||||
|
isScrollingNotifier: _isScrollingNotifier,
|
||||||
|
scrollController: scrollController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final scaler = _ThumbnailGridScaleGestureDetector(
|
||||||
|
scrollableKey: _scrollableKey,
|
||||||
|
appBarHeightNotifier: _appBarHeightNotifier,
|
||||||
|
child: scrollView,
|
||||||
|
);
|
||||||
|
|
||||||
|
final selector = GridSelectionGestureDetector(
|
||||||
|
selectable: AvesApp.mode == AppMode.main,
|
||||||
|
collection: collection,
|
||||||
|
scrollController: scrollController,
|
||||||
|
appBarHeightNotifier: _appBarHeightNotifier,
|
||||||
|
child: scaler,
|
||||||
|
);
|
||||||
|
|
||||||
|
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
||||||
|
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
|
||||||
|
builder: (context, tileExtent, child) {
|
||||||
|
return SectionedEntryListLayoutProvider(
|
||||||
|
collection: collection,
|
||||||
|
scrollableWidth: context.select<TileExtentController, double>((controller) => controller.viewportSize.width),
|
||||||
|
tileExtent: tileExtent,
|
||||||
|
columnCount: context.select<TileExtentController, int>((controller) => controller.getEffectiveColumnCountForExtent(tileExtent)),
|
||||||
|
tileBuilder: (entry) => InteractiveThumbnail(
|
||||||
|
key: ValueKey(entry.contentId),
|
||||||
|
collection: collection,
|
||||||
|
entry: entry,
|
||||||
|
tileExtent: tileExtent,
|
||||||
|
isScrollingNotifier: _isScrollingNotifier,
|
||||||
|
),
|
||||||
|
child: selector,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return sectionedListLayoutProvider;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ThumbnailGridScaleGestureDetector extends StatelessWidget {
|
||||||
|
final GlobalKey scrollableKey;
|
||||||
|
final ValueNotifier<double> appBarHeightNotifier;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _ThumbnailGridScaleGestureDetector({
|
||||||
|
@required this.scrollableKey,
|
||||||
|
@required this.appBarHeightNotifier,
|
||||||
|
@required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
|
||||||
|
return GridScaleGestureDetector<AvesEntry>(
|
||||||
|
scrollableKey: scrollableKey,
|
||||||
|
appBarHeightNotifier: appBarHeightNotifier,
|
||||||
|
gridBuilder: (center, extent, child) => CustomPaint(
|
||||||
|
// painting the thumbnail half-border on top of the grid yields artifacts,
|
||||||
|
// so we use a `foregroundPainter` to cover them instead
|
||||||
|
foregroundPainter: GridPainter(
|
||||||
|
center: center,
|
||||||
|
extent: extent,
|
||||||
|
spacing: tileSpacing,
|
||||||
|
strokeWidth: DecoratedThumbnail.borderWidth * 2,
|
||||||
|
color: DecoratedThumbnail.borderColor,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
scaledBuilder: (entry, extent) => DecoratedThumbnail(
|
||||||
|
entry: entry,
|
||||||
|
extent: extent,
|
||||||
|
selectable: false,
|
||||||
|
highlightable: false,
|
||||||
|
),
|
||||||
|
getScaledItemTileRect: (context, entry) {
|
||||||
|
final sectionedListLayout = context.read<SectionedListLayout<AvesEntry>>();
|
||||||
|
return sectionedListLayout.getTileRect(entry) ?? Rect.zero;
|
||||||
|
},
|
||||||
|
onScaled: (entry) => context.read<HighlightInfo>().set(entry),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CollectionScrollView extends StatefulWidget {
|
class CollectionScrollView extends StatefulWidget {
|
||||||
final GlobalKey scrollableKey;
|
final GlobalKey scrollableKey;
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
|
@ -152,7 +168,6 @@ class CollectionScrollView extends StatefulWidget {
|
||||||
final ValueNotifier<double> appBarHeightNotifier;
|
final ValueNotifier<double> appBarHeightNotifier;
|
||||||
final ValueNotifier<bool> isScrollingNotifier;
|
final ValueNotifier<bool> isScrollingNotifier;
|
||||||
final ScrollController scrollController;
|
final ScrollController scrollController;
|
||||||
final double cacheExtent;
|
|
||||||
|
|
||||||
const CollectionScrollView({
|
const CollectionScrollView({
|
||||||
@required this.scrollableKey,
|
@required this.scrollableKey,
|
||||||
|
@ -161,7 +176,6 @@ class CollectionScrollView extends StatefulWidget {
|
||||||
@required this.appBarHeightNotifier,
|
@required this.appBarHeightNotifier,
|
||||||
@required this.isScrollingNotifier,
|
@required this.isScrollingNotifier,
|
||||||
@required this.scrollController,
|
@required this.scrollController,
|
||||||
@required this.cacheExtent,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -216,7 +230,7 @@ class _CollectionScrollViewState extends State<CollectionScrollView> {
|
||||||
// workaround to prevent scrolling the app bar away
|
// workaround to prevent scrolling the app bar away
|
||||||
// when there is no content and we use `SliverFillRemaining`
|
// when there is no content and we use `SliverFillRemaining`
|
||||||
physics: collection.isEmpty ? NeverScrollableScrollPhysics() : SloppyScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
|
physics: collection.isEmpty ? NeverScrollableScrollPhysics() : SloppyScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
|
||||||
cacheExtent: widget.cacheExtent,
|
cacheExtent: context.select<TileExtentController, double>((controller) => controller.effectiveExtentMax * 2),
|
||||||
slivers: [
|
slivers: [
|
||||||
appBar,
|
appBar,
|
||||||
collection.isEmpty
|
collection.isEmpty
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class AvesCircleBorder {
|
class AvesCircleBorder {
|
||||||
static const borderColor = Colors.white30;
|
static const borderColor = Colors.white30;
|
||||||
|
|
||||||
static double _borderWidth(BuildContext context) => MediaQuery.of(context).devicePixelRatio > 2 ? 0.5 : 1.0;
|
static double _borderWidth(BuildContext context) => context.read<MediaQueryData>().devicePixelRatio > 2 ? 0.5 : 1.0;
|
||||||
|
|
||||||
static Border build(BuildContext context) {
|
static Border build(BuildContext context) {
|
||||||
return Border.fromBorderSide(buildSide(context));
|
return Border.fromBorderSide(buildSide(context));
|
||||||
|
|
|
@ -27,6 +27,11 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ProxyProvider0<SectionedListLayout<T>>(
|
return ProxyProvider0<SectionedListLayout<T>>(
|
||||||
update: (context, _) => _updateLayouts(context),
|
update: (context, _) => _updateLayouts(context),
|
||||||
|
updateShouldNotify: (previous, current) {
|
||||||
|
final previousLayouts = previous.sectionLayouts;
|
||||||
|
final currentLayouts = current.sectionLayouts;
|
||||||
|
return previousLayouts.length != currentLayouts.length || !previousLayouts.every(currentLayouts.contains);
|
||||||
|
},
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -138,6 +143,16 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
||||||
double getHeaderExtent(BuildContext context, SectionKey sectionKey);
|
double getHeaderExtent(BuildContext context, SectionKey sectionKey);
|
||||||
|
|
||||||
Widget buildHeader(BuildContext context, SectionKey sectionKey, double headerExtent);
|
Widget buildHeader(BuildContext context, SectionKey sectionKey, double headerExtent);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(DoubleProperty('scrollableWidth', scrollableWidth));
|
||||||
|
properties.add(IntProperty('columnCount', columnCount));
|
||||||
|
properties.add(DoubleProperty('spacing', spacing));
|
||||||
|
properties.add(DoubleProperty('tileExtent', tileExtent));
|
||||||
|
properties.add(DiagnosticsProperty<bool>('showHeaders', showHeaders));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SectionedListLayout<T> {
|
class SectionedListLayout<T> {
|
||||||
|
@ -237,6 +252,12 @@ class SectionLayout {
|
||||||
return bodyFirstIndex + (scrollOffset / mainAxisStride).ceil() - 1;
|
return bodyFirstIndex + (scrollOffset / mainAxisStride).ceil() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SectionLayout && runtimeType == other.runtimeType && sectionKey == other.sectionKey && firstIndex == other.firstIndex && lastIndex == other.lastIndex && minOffset == other.minOffset && maxOffset == other.maxOffset && headerExtent == other.headerExtent && tileExtent == other.tileExtent && spacing == other.spacing;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => hashValues(sectionKey, firstIndex, lastIndex, minOffset, maxOffset, headerExtent, tileExtent, spacing);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$runtimeType#${shortHash(this)}{sectionKey=$sectionKey, firstIndex=$firstIndex, lastIndex=$lastIndex, minOffset=$minOffset, maxOffset=$maxOffset, headerExtent=$headerExtent, tileExtent=$tileExtent, spacing=$spacing}';
|
String toString() => '$runtimeType#${shortHash(this)}{sectionKey=$sectionKey, firstIndex=$firstIndex, lastIndex=$lastIndex, minOffset=$minOffset, maxOffset=$maxOffset, headerExtent=$headerExtent, tileExtent=$tileExtent, spacing=$spacing}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class TileExtentControllerProvider extends StatelessWidget {
|
||||||
|
final TileExtentController controller;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const TileExtentControllerProvider({
|
||||||
|
@required this.controller,
|
||||||
|
@required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ProxyProvider0(
|
||||||
|
update: (_, __) => constraints.biggest,
|
||||||
|
),
|
||||||
|
ProxyProvider<Size, TileExtentController>(
|
||||||
|
update: (_, viewportSize, __) => controller..applyTileExtent(viewportSize: viewportSize),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,10 @@ import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/common/tile_extent_manager.dart';
|
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
// metadata to identify entry from RenderObject hit test during collection scaling
|
// metadata to identify entry from RenderObject hit test during collection scaling
|
||||||
class ScalerMetadata<T> {
|
class ScalerMetadata<T> {
|
||||||
|
@ -14,10 +15,8 @@ class ScalerMetadata<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class GridScaleGestureDetector<T> extends StatefulWidget {
|
class GridScaleGestureDetector<T> extends StatefulWidget {
|
||||||
final TileExtentManager tileExtentManager;
|
|
||||||
final GlobalKey scrollableKey;
|
final GlobalKey scrollableKey;
|
||||||
final ValueNotifier<double> appBarHeightNotifier;
|
final ValueNotifier<double> appBarHeightNotifier;
|
||||||
final Size viewportSize;
|
|
||||||
final Widget Function(Offset center, double extent, Widget child) gridBuilder;
|
final Widget Function(Offset center, double extent, Widget child) gridBuilder;
|
||||||
final Widget Function(T item, double extent) scaledBuilder;
|
final Widget Function(T item, double extent) scaledBuilder;
|
||||||
final Rect Function(BuildContext context, T item) getScaledItemTileRect;
|
final Rect Function(BuildContext context, T item) getScaledItemTileRect;
|
||||||
|
@ -25,10 +24,8 @@ class GridScaleGestureDetector<T> extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const GridScaleGestureDetector({
|
const GridScaleGestureDetector({
|
||||||
@required this.tileExtentManager,
|
|
||||||
@required this.scrollableKey,
|
@required this.scrollableKey,
|
||||||
@required this.appBarHeightNotifier,
|
@required this.appBarHeightNotifier,
|
||||||
@required this.viewportSize,
|
|
||||||
this.gridBuilder,
|
this.gridBuilder,
|
||||||
@required this.scaledBuilder,
|
@required this.scaledBuilder,
|
||||||
@required this.getScaledItemTileRect,
|
@required this.getScaledItemTileRect,
|
||||||
|
@ -47,10 +44,6 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
||||||
OverlayEntry _overlayEntry;
|
OverlayEntry _overlayEntry;
|
||||||
ScalerMetadata<T> _metadata;
|
ScalerMetadata<T> _metadata;
|
||||||
|
|
||||||
TileExtentManager get tileExtentManager => widget.tileExtentManager;
|
|
||||||
|
|
||||||
Size get viewportSize => widget.viewportSize;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
@ -76,8 +69,9 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
||||||
// not the same as `MediaQuery.size.width`, because of screen insets/padding
|
// not the same as `MediaQuery.size.width`, because of screen insets/padding
|
||||||
final gridWidth = scrollableBox.size.width;
|
final gridWidth = scrollableBox.size.width;
|
||||||
|
|
||||||
_extentMin = tileExtentManager.getEffectiveExtentMin(viewportSize);
|
final tileExtentController = context.read<TileExtentController>();
|
||||||
_extentMax = tileExtentManager.getEffectiveExtentMax(viewportSize);
|
_extentMin = tileExtentController.effectiveExtentMin;
|
||||||
|
_extentMax = tileExtentController.effectiveExtentMax;
|
||||||
|
|
||||||
final halfExtent = _startExtent / 2;
|
final halfExtent = _startExtent / 2;
|
||||||
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent));
|
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent));
|
||||||
|
@ -105,10 +99,10 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyingScale = true;
|
_applyingScale = true;
|
||||||
final oldExtent = tileExtentManager.extentNotifier.value;
|
final tileExtentController = context.read<TileExtentController>();
|
||||||
|
final oldExtent = tileExtentController.extentNotifier.value;
|
||||||
// sanitize and update grid layout if necessary
|
// sanitize and update grid layout if necessary
|
||||||
final newExtent = tileExtentManager.applyTileExtent(
|
final newExtent = tileExtentController.applyTileExtent(
|
||||||
viewportSize: widget.viewportSize,
|
|
||||||
userPreferredExtent: _scaledExtentNotifier.value,
|
userPreferredExtent: _scaledExtentNotifier.value,
|
||||||
);
|
);
|
||||||
_scaledExtentNotifier = null;
|
_scaledExtentNotifier = null;
|
||||||
|
@ -195,59 +189,61 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MediaQueryDataProvider(
|
return MediaQueryDataProvider(
|
||||||
child: IgnorePointer(
|
child: Builder(
|
||||||
child: AnimatedContainer(
|
builder: (context) => IgnorePointer(
|
||||||
decoration: _init
|
child: AnimatedContainer(
|
||||||
? BoxDecoration(
|
decoration: _init
|
||||||
gradient: RadialGradient(
|
? BoxDecoration(
|
||||||
center: FractionalOffset.fromOffsetAndSize(center, MediaQuery.of(context).size),
|
gradient: RadialGradient(
|
||||||
radius: 1,
|
center: FractionalOffset.fromOffsetAndSize(center, context.select<MediaQueryData, Size>((mq) => mq.size)),
|
||||||
colors: [
|
radius: 1,
|
||||||
Colors.black,
|
colors: [
|
||||||
Colors.black54,
|
Colors.black,
|
||||||
],
|
Colors.black54,
|
||||||
),
|
],
|
||||||
)
|
),
|
||||||
: BoxDecoration(
|
)
|
||||||
// provide dummy gradient to lerp to the other one during animation
|
: BoxDecoration(
|
||||||
gradient: RadialGradient(
|
// provide dummy gradient to lerp to the other one during animation
|
||||||
colors: [
|
gradient: RadialGradient(
|
||||||
Colors.transparent,
|
colors: [
|
||||||
Colors.transparent,
|
Colors.transparent,
|
||||||
],
|
Colors.transparent,
|
||||||
),
|
],
|
||||||
),
|
|
||||||
duration: Durations.collectionScalingBackgroundAnimation,
|
|
||||||
child: ValueListenableBuilder<double>(
|
|
||||||
valueListenable: widget.scaledExtentNotifier,
|
|
||||||
builder: (context, extent, child) {
|
|
||||||
// keep scaled thumbnail within the screen
|
|
||||||
final xMin = MediaQuery.of(context).padding.left;
|
|
||||||
final xMax = xMin + gridWidth;
|
|
||||||
var dx = .0;
|
|
||||||
if (center.dx - extent / 2 < xMin) {
|
|
||||||
dx = xMin - (center.dx - extent / 2);
|
|
||||||
} else if (center.dx + extent / 2 > xMax) {
|
|
||||||
dx = xMax - (center.dx + extent / 2);
|
|
||||||
}
|
|
||||||
final clampedCenter = center.translate(dx, 0);
|
|
||||||
|
|
||||||
var child = widget.builder(extent);
|
|
||||||
child = Stack(
|
|
||||||
children: [
|
|
||||||
Positioned(
|
|
||||||
left: clampedCenter.dx - extent / 2,
|
|
||||||
top: clampedCenter.dy - extent / 2,
|
|
||||||
child: DefaultTextStyle(
|
|
||||||
style: TextStyle(),
|
|
||||||
child: child,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
duration: Durations.collectionScalingBackgroundAnimation,
|
||||||
);
|
child: ValueListenableBuilder<double>(
|
||||||
child = widget.gridBuilder?.call(clampedCenter, extent, child) ?? child;
|
valueListenable: widget.scaledExtentNotifier,
|
||||||
return child;
|
builder: (context, extent, child) {
|
||||||
},
|
// keep scaled thumbnail within the screen
|
||||||
|
final xMin = context.select<MediaQueryData, double>((mq) => mq.padding.left);
|
||||||
|
final xMax = xMin + gridWidth;
|
||||||
|
var dx = .0;
|
||||||
|
if (center.dx - extent / 2 < xMin) {
|
||||||
|
dx = xMin - (center.dx - extent / 2);
|
||||||
|
} else if (center.dx + extent / 2 > xMax) {
|
||||||
|
dx = xMax - (center.dx + extent / 2);
|
||||||
|
}
|
||||||
|
final clampedCenter = center.translate(dx, 0);
|
||||||
|
|
||||||
|
var child = widget.builder(extent);
|
||||||
|
child = Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: clampedCenter.dx - extent / 2,
|
||||||
|
top: clampedCenter.dy - extent / 2,
|
||||||
|
child: DefaultTextStyle(
|
||||||
|
style: TextStyle(),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
child = widget.gridBuilder?.call(clampedCenter, extent, child) ?? child;
|
||||||
|
return child;
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
77
lib/widgets/common/tile_extent_controller.dart
Normal file
77
lib/widgets/common/tile_extent_controller.dart
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class TileExtentController {
|
||||||
|
final String settingsRouteKey;
|
||||||
|
final int columnCountMin, columnCountDefault;
|
||||||
|
final double spacing, extentMin, extentMax;
|
||||||
|
final ValueNotifier<double> extentNotifier;
|
||||||
|
|
||||||
|
Size _viewportSize;
|
||||||
|
|
||||||
|
Size get viewportSize => _viewportSize;
|
||||||
|
|
||||||
|
TileExtentController({
|
||||||
|
@required this.settingsRouteKey,
|
||||||
|
@required this.extentNotifier,
|
||||||
|
this.columnCountMin = 2,
|
||||||
|
@required this.columnCountDefault,
|
||||||
|
@required this.extentMin,
|
||||||
|
this.extentMax = 300,
|
||||||
|
@required this.spacing,
|
||||||
|
});
|
||||||
|
|
||||||
|
double applyTileExtent({
|
||||||
|
Size viewportSize,
|
||||||
|
double userPreferredExtent = 0,
|
||||||
|
}) {
|
||||||
|
if (viewportSize != null) {
|
||||||
|
// sanitize screen size (useful when reloading while screen is off, reporting a 0,0 size)
|
||||||
|
final viewportSizeMin = Size.square(extentMin * columnCountMin);
|
||||||
|
_viewportSize = Size(max(viewportSize.width, viewportSizeMin.width), max(viewportSize.height, viewportSizeMin.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
final oldUserPreferredExtent = settings.getTileExtent(settingsRouteKey);
|
||||||
|
final currentExtent = extentNotifier.value;
|
||||||
|
final targetExtent = userPreferredExtent > 0
|
||||||
|
? userPreferredExtent
|
||||||
|
: oldUserPreferredExtent > 0
|
||||||
|
? oldUserPreferredExtent
|
||||||
|
: currentExtent;
|
||||||
|
|
||||||
|
final columnCount = getEffectiveColumnCountForExtent(targetExtent);
|
||||||
|
final newExtent = _extentForColumnCount(columnCount);
|
||||||
|
|
||||||
|
if (userPreferredExtent > 0 || oldUserPreferredExtent == 0) {
|
||||||
|
settings.setTileExtent(settingsRouteKey, newExtent);
|
||||||
|
}
|
||||||
|
if (extentNotifier.value != newExtent) {
|
||||||
|
extentNotifier.value = newExtent;
|
||||||
|
}
|
||||||
|
return newExtent;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _extentMax() => min(extentMax, (viewportSize.shortestSide - spacing * (columnCountMin - 1)) / columnCountMin);
|
||||||
|
|
||||||
|
double _columnCountForExtent(double extent) => (viewportSize.width + spacing) / (extent + spacing);
|
||||||
|
|
||||||
|
double _extentForColumnCount(int columnCount) => (viewportSize.width - spacing * (columnCount - 1)) / columnCount;
|
||||||
|
|
||||||
|
int _effectiveColumnCountMin() => _columnCountForExtent(_extentMax()).ceil();
|
||||||
|
|
||||||
|
int _effectiveColumnCountMax() => _columnCountForExtent(extentMin).floor();
|
||||||
|
|
||||||
|
double get effectiveExtentMin => _extentForColumnCount(_effectiveColumnCountMax());
|
||||||
|
|
||||||
|
double get effectiveExtentMax => _extentForColumnCount(_effectiveColumnCountMin());
|
||||||
|
|
||||||
|
int getEffectiveColumnCountForExtent(double extent) {
|
||||||
|
if (extent > 0) {
|
||||||
|
final columnCount = _columnCountForExtent(extent);
|
||||||
|
return columnCount.clamp(_effectiveColumnCountMin(), _effectiveColumnCountMax()).round();
|
||||||
|
}
|
||||||
|
return columnCountDefault;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,71 +0,0 @@
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:aves/model/settings/settings.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class TileExtentManager {
|
|
||||||
final String settingsRouteKey;
|
|
||||||
final int columnCountMin, columnCountDefault;
|
|
||||||
final double spacing, extentMin, extentMax;
|
|
||||||
final ValueNotifier<double> extentNotifier;
|
|
||||||
|
|
||||||
const TileExtentManager({
|
|
||||||
@required this.settingsRouteKey,
|
|
||||||
@required this.extentNotifier,
|
|
||||||
this.columnCountMin = 2,
|
|
||||||
@required this.columnCountDefault,
|
|
||||||
@required this.extentMin,
|
|
||||||
this.extentMax = 300,
|
|
||||||
@required this.spacing,
|
|
||||||
});
|
|
||||||
|
|
||||||
double applyTileExtent({
|
|
||||||
@required Size viewportSize,
|
|
||||||
double userPreferredExtent = 0,
|
|
||||||
}) {
|
|
||||||
// sanitize screen size (useful when reloading while screen is off, reporting a 0,0 size)
|
|
||||||
final viewportSizeMin = Size.square(extentMin * columnCountMin);
|
|
||||||
viewportSize = Size(max(viewportSize.width, viewportSizeMin.width), max(viewportSize.height, viewportSizeMin.height));
|
|
||||||
|
|
||||||
final oldUserPreferredExtent = settings.getTileExtent(settingsRouteKey);
|
|
||||||
final currentExtent = extentNotifier.value;
|
|
||||||
final targetExtent = userPreferredExtent > 0
|
|
||||||
? userPreferredExtent
|
|
||||||
: oldUserPreferredExtent > 0
|
|
||||||
? oldUserPreferredExtent
|
|
||||||
: currentExtent;
|
|
||||||
|
|
||||||
final columnCount = getEffectiveColumnCountForExtent(viewportSize, targetExtent);
|
|
||||||
final newExtent = _extentForColumnCount(viewportSize, columnCount);
|
|
||||||
|
|
||||||
if (userPreferredExtent > 0 || oldUserPreferredExtent == 0) {
|
|
||||||
settings.setTileExtent(settingsRouteKey, newExtent);
|
|
||||||
}
|
|
||||||
if (extentNotifier.value != newExtent) {
|
|
||||||
extentNotifier.value = newExtent;
|
|
||||||
}
|
|
||||||
return newExtent;
|
|
||||||
}
|
|
||||||
|
|
||||||
double _extentMax(Size viewportSize) => min(extentMax, (viewportSize.shortestSide - spacing * (columnCountMin - 1)) / columnCountMin);
|
|
||||||
|
|
||||||
double _columnCountForExtent(Size viewportSize, double extent) => (viewportSize.width + spacing) / (extent + spacing);
|
|
||||||
|
|
||||||
double _extentForColumnCount(Size viewportSize, int columnCount) => (viewportSize.width - spacing * (columnCount - 1)) / columnCount;
|
|
||||||
|
|
||||||
int _effectiveColumnCountMin(Size viewportSize) => _columnCountForExtent(viewportSize, _extentMax(viewportSize)).ceil();
|
|
||||||
|
|
||||||
int _effectiveColumnCountMax(Size viewportSize) => _columnCountForExtent(viewportSize, extentMin).floor();
|
|
||||||
|
|
||||||
double getEffectiveExtentMin(Size viewportSize) => _extentForColumnCount(viewportSize, _effectiveColumnCountMax(viewportSize));
|
|
||||||
|
|
||||||
double getEffectiveExtentMax(Size viewportSize) => _extentForColumnCount(viewportSize, _effectiveColumnCountMin(viewportSize));
|
|
||||||
|
|
||||||
int getEffectiveColumnCountForExtent(Size viewportSize, double extent) {
|
|
||||||
if (extent > 0) {
|
|
||||||
final columnCount = _columnCountForExtent(viewportSize, extent);
|
|
||||||
return columnCount.clamp(_effectiveColumnCountMin(viewportSize), _effectiveColumnCountMax(viewportSize)).round();
|
|
||||||
}
|
|
||||||
return columnCountDefault;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:aves/services/service_policy.dart';
|
import 'package:aves/services/service_policy.dart';
|
||||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class DebugTaskQueueOverlay extends StatelessWidget {
|
class DebugTaskQueueOverlay extends StatelessWidget {
|
||||||
|
@ -13,9 +12,6 @@ class DebugTaskQueueOverlay extends StatelessWidget {
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.indigo[900].withAlpha(0xCC),
|
color: Colors.indigo[900].withAlpha(0xCC),
|
||||||
margin: EdgeInsets.only(
|
|
||||||
bottom: MediaQuery.of(context).effectiveBottomPadding,
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.all(8),
|
padding: EdgeInsets.all(8),
|
||||||
child: StreamBuilder<QueueState>(
|
child: StreamBuilder<QueueState>(
|
||||||
stream: servicePolicy.queueStream,
|
stream: servicePolicy.queueStream,
|
||||||
|
|
|
@ -14,8 +14,9 @@ import 'package:aves/widgets/common/grid/sliver.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||||
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
|
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
|
||||||
import 'package:aves/widgets/common/scaling.dart';
|
import 'package:aves/widgets/common/scaling.dart';
|
||||||
import 'package:aves/widgets/common/tile_extent_manager.dart';
|
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||||
import 'package:aves/widgets/drawer/app_drawer.dart';
|
import 'package:aves/widgets/drawer/app_drawer.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/decorated_filter_chip.dart';
|
import 'package:aves/widgets/filter_grids/common/decorated_filter_chip.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
|
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
|
||||||
|
@ -25,6 +26,8 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
typedef QueryTest<T extends CollectionFilter> = Iterable<FilterGridItem<T>> Function(Iterable<FilterGridItem<T>> filters, String query);
|
||||||
|
|
||||||
class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
final Widget appBar;
|
final Widget appBar;
|
||||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
|
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
|
||||||
|
@ -32,17 +35,13 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
final ValueNotifier<String> queryNotifier;
|
final ValueNotifier<String> queryNotifier;
|
||||||
final Widget Function() emptyBuilder;
|
final Widget Function() emptyBuilder;
|
||||||
final String settingsRouteKey;
|
final String settingsRouteKey;
|
||||||
final Iterable<FilterGridItem<T>> Function(Iterable<FilterGridItem<T>> filters, String query) applyQuery;
|
final double appBarHeight;
|
||||||
|
final QueryTest<T> applyQuery;
|
||||||
final FilterCallback onTap;
|
final FilterCallback onTap;
|
||||||
final OffsetFilterCallback onLongPress;
|
final OffsetFilterCallback onLongPress;
|
||||||
|
|
||||||
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
|
|
||||||
final ValueNotifier<double> _tileExtentNotifier = ValueNotifier(0);
|
final ValueNotifier<double> _tileExtentNotifier = ValueNotifier(0);
|
||||||
|
|
||||||
static const columnCountDefault = 2;
|
|
||||||
static const extentMin = 60.0;
|
|
||||||
static const spacing = 8.0;
|
|
||||||
|
|
||||||
FilterGridPage({
|
FilterGridPage({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.appBar,
|
@required this.appBar,
|
||||||
|
@ -52,12 +51,10 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
this.applyQuery,
|
this.applyQuery,
|
||||||
@required this.emptyBuilder,
|
@required this.emptyBuilder,
|
||||||
this.settingsRouteKey,
|
this.settingsRouteKey,
|
||||||
double appBarHeight = kToolbarHeight,
|
this.appBarHeight = kToolbarHeight,
|
||||||
@required this.onTap,
|
@required this.onTap,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
}) : super(key: key) {
|
}) : super(key: key);
|
||||||
_appBarHeightNotifier.value = appBarHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const Color detailColor = Color(0xFFE0E0E0);
|
static const Color detailColor = Color(0xFFE0E0E0);
|
||||||
|
|
||||||
|
@ -69,76 +66,25 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
child: GestureAreaProtectorStack(
|
child: GestureAreaProtectorStack(
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: LayoutBuilder(
|
child: TileExtentControllerProvider(
|
||||||
builder: (context, constraints) {
|
controller: TileExtentController(
|
||||||
final viewportSize = constraints.biggest;
|
settingsRouteKey: settingsRouteKey ?? context.currentRouteName,
|
||||||
assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.');
|
extentNotifier: _tileExtentNotifier,
|
||||||
if (viewportSize.isEmpty) return SizedBox.shrink();
|
columnCountDefault: 2,
|
||||||
|
extentMin: 60,
|
||||||
final tileExtentManager = TileExtentManager(
|
spacing: 8,
|
||||||
settingsRouteKey: settingsRouteKey ?? context.currentRouteName,
|
),
|
||||||
extentNotifier: _tileExtentNotifier,
|
child: _FilterGridPageContent<T>(
|
||||||
columnCountDefault: columnCountDefault,
|
appBar: appBar,
|
||||||
extentMin: extentMin,
|
filterSections: filterSections,
|
||||||
spacing: spacing,
|
showHeaders: showHeaders,
|
||||||
)..applyTileExtent(viewportSize: viewportSize);
|
queryNotifier: queryNotifier,
|
||||||
|
applyQuery: applyQuery,
|
||||||
return ValueListenableBuilder<String>(
|
emptyBuilder: emptyBuilder,
|
||||||
valueListenable: queryNotifier,
|
appBarHeight: appBarHeight,
|
||||||
builder: (context, query, child) {
|
onTap: onTap,
|
||||||
Map<ChipSectionKey, List<FilterGridItem<T>>> visibleFilterSections;
|
onLongPress: onLongPress,
|
||||||
if (applyQuery == null) {
|
),
|
||||||
visibleFilterSections = filterSections;
|
|
||||||
} else {
|
|
||||||
visibleFilterSections = {};
|
|
||||||
filterSections.forEach((sectionKey, sectionFilters) {
|
|
||||||
final visibleFilters = applyQuery(sectionFilters, query);
|
|
||||||
if (visibleFilters.isNotEmpty) {
|
|
||||||
visibleFilterSections[sectionKey] = visibleFilters.toList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final pinnedFilters = settings.pinnedFilters;
|
|
||||||
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
|
||||||
valueListenable: _tileExtentNotifier,
|
|
||||||
builder: (context, tileExtent, child) => SectionedFilterListLayoutProvider<T>(
|
|
||||||
sections: visibleFilterSections,
|
|
||||||
showHeaders: showHeaders,
|
|
||||||
scrollableWidth: viewportSize.width,
|
|
||||||
tileExtent: tileExtent,
|
|
||||||
columnCount: tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent),
|
|
||||||
spacing: spacing,
|
|
||||||
tileBuilder: (gridItem) {
|
|
||||||
final filter = gridItem.filter;
|
|
||||||
final entry = gridItem.entry;
|
|
||||||
return MetaData(
|
|
||||||
metaData: ScalerMetadata(FilterGridItem<T>(filter, entry)),
|
|
||||||
child: DecoratedFilterChip(
|
|
||||||
key: Key(filter.key),
|
|
||||||
filter: filter,
|
|
||||||
extent: _tileExtentNotifier.value,
|
|
||||||
pinned: pinnedFilters.contains(filter),
|
|
||||||
onTap: onTap,
|
|
||||||
onLongPress: onLongPress,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: _SectionedContent<T>(
|
|
||||||
appBar: appBar,
|
|
||||||
appBarHeightNotifier: _appBarHeightNotifier,
|
|
||||||
visibleFilterSections: visibleFilterSections,
|
|
||||||
emptyBuilder: emptyBuilder,
|
|
||||||
viewportSize: viewportSize,
|
|
||||||
tileExtentManager: tileExtentManager,
|
|
||||||
scrollController: PrimaryScrollController.of(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return sectionedListLayoutProvider;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -150,13 +96,100 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _FilterGridPageContent<T extends CollectionFilter> extends StatelessWidget {
|
||||||
|
final Widget appBar;
|
||||||
|
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
|
||||||
|
final bool showHeaders;
|
||||||
|
final ValueNotifier<String> queryNotifier;
|
||||||
|
final Widget Function() emptyBuilder;
|
||||||
|
final QueryTest<T> applyQuery;
|
||||||
|
final FilterCallback onTap;
|
||||||
|
final OffsetFilterCallback onLongPress;
|
||||||
|
|
||||||
|
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
|
||||||
|
|
||||||
|
_FilterGridPageContent({
|
||||||
|
Key key,
|
||||||
|
@required this.appBar,
|
||||||
|
@required this.filterSections,
|
||||||
|
@required this.showHeaders,
|
||||||
|
@required this.queryNotifier,
|
||||||
|
@required this.applyQuery,
|
||||||
|
@required this.emptyBuilder,
|
||||||
|
@required double appBarHeight,
|
||||||
|
@required this.onTap,
|
||||||
|
@required this.onLongPress,
|
||||||
|
}) : super(key: key) {
|
||||||
|
_appBarHeightNotifier.value = appBarHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<String>(
|
||||||
|
valueListenable: queryNotifier,
|
||||||
|
builder: (context, query, child) {
|
||||||
|
Map<ChipSectionKey, List<FilterGridItem<T>>> visibleFilterSections;
|
||||||
|
if (applyQuery == null) {
|
||||||
|
visibleFilterSections = filterSections;
|
||||||
|
} else {
|
||||||
|
visibleFilterSections = {};
|
||||||
|
filterSections.forEach((sectionKey, sectionFilters) {
|
||||||
|
final visibleFilters = applyQuery(sectionFilters, query);
|
||||||
|
if (visibleFilters.isNotEmpty) {
|
||||||
|
visibleFilterSections[sectionKey] = visibleFilters.toList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final pinnedFilters = settings.pinnedFilters;
|
||||||
|
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
||||||
|
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
|
||||||
|
builder: (context, tileExtent, child) {
|
||||||
|
final columnCount = context.select<TileExtentController, int>((controller) => controller.getEffectiveColumnCountForExtent(tileExtent));
|
||||||
|
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
|
||||||
|
return SectionedFilterListLayoutProvider<T>(
|
||||||
|
sections: visibleFilterSections,
|
||||||
|
showHeaders: showHeaders,
|
||||||
|
scrollableWidth: context.select<TileExtentController, double>((controller) => controller.viewportSize.width),
|
||||||
|
tileExtent: tileExtent,
|
||||||
|
columnCount: columnCount,
|
||||||
|
spacing: tileSpacing,
|
||||||
|
tileBuilder: (gridItem) {
|
||||||
|
final filter = gridItem.filter;
|
||||||
|
final entry = gridItem.entry;
|
||||||
|
return MetaData(
|
||||||
|
metaData: ScalerMetadata(FilterGridItem<T>(filter, entry)),
|
||||||
|
child: DecoratedFilterChip(
|
||||||
|
key: Key(filter.key),
|
||||||
|
filter: filter,
|
||||||
|
extent: tileExtent,
|
||||||
|
pinned: pinnedFilters.contains(filter),
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: _SectionedContent<T>(
|
||||||
|
appBar: appBar,
|
||||||
|
appBarHeightNotifier: _appBarHeightNotifier,
|
||||||
|
visibleFilterSections: visibleFilterSections,
|
||||||
|
emptyBuilder: emptyBuilder,
|
||||||
|
scrollController: PrimaryScrollController.of(context),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return sectionedListLayoutProvider;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _SectionedContent<T extends CollectionFilter> extends StatefulWidget {
|
class _SectionedContent<T extends CollectionFilter> extends StatefulWidget {
|
||||||
final Widget appBar;
|
final Widget appBar;
|
||||||
final ValueNotifier<double> appBarHeightNotifier;
|
final ValueNotifier<double> appBarHeightNotifier;
|
||||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> visibleFilterSections;
|
final Map<ChipSectionKey, List<FilterGridItem<T>>> visibleFilterSections;
|
||||||
final Widget Function() emptyBuilder;
|
final Widget Function() emptyBuilder;
|
||||||
final Size viewportSize;
|
|
||||||
final TileExtentManager tileExtentManager;
|
|
||||||
final ScrollController scrollController;
|
final ScrollController scrollController;
|
||||||
|
|
||||||
const _SectionedContent({
|
const _SectionedContent({
|
||||||
|
@ -164,8 +197,6 @@ class _SectionedContent<T extends CollectionFilter> extends StatefulWidget {
|
||||||
@required this.appBarHeightNotifier,
|
@required this.appBarHeightNotifier,
|
||||||
@required this.visibleFilterSections,
|
@required this.visibleFilterSections,
|
||||||
@required this.emptyBuilder,
|
@required this.emptyBuilder,
|
||||||
@required this.viewportSize,
|
|
||||||
@required this.tileExtentManager,
|
|
||||||
@required this.scrollController,
|
@required this.scrollController,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -182,10 +213,6 @@ class _SectionedContentState<T extends CollectionFilter> extends State<_Sectione
|
||||||
|
|
||||||
Widget Function() get emptyBuilder => widget.emptyBuilder;
|
Widget Function() get emptyBuilder => widget.emptyBuilder;
|
||||||
|
|
||||||
Size get viewportSize => widget.viewportSize;
|
|
||||||
|
|
||||||
TileExtentManager get tileExtentManager => widget.tileExtentManager;
|
|
||||||
|
|
||||||
ScrollController get scrollController => widget.scrollController;
|
ScrollController get scrollController => widget.scrollController;
|
||||||
|
|
||||||
final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable');
|
final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable');
|
||||||
|
@ -232,17 +259,15 @@ class _SectionedContentState<T extends CollectionFilter> extends State<_Sectione
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final pinnedFilters = settings.pinnedFilters;
|
final pinnedFilters = settings.pinnedFilters;
|
||||||
|
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
|
||||||
return GridScaleGestureDetector<FilterGridItem<T>>(
|
return GridScaleGestureDetector<FilterGridItem<T>>(
|
||||||
tileExtentManager: tileExtentManager,
|
|
||||||
scrollableKey: _scrollableKey,
|
scrollableKey: _scrollableKey,
|
||||||
appBarHeightNotifier: appBarHeightNotifier,
|
appBarHeightNotifier: appBarHeightNotifier,
|
||||||
viewportSize: viewportSize,
|
|
||||||
gridBuilder: (center, extent, child) => CustomPaint(
|
gridBuilder: (center, extent, child) => CustomPaint(
|
||||||
painter: GridPainter(
|
painter: GridPainter(
|
||||||
center: center,
|
center: center,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
spacing: tileExtentManager.spacing,
|
spacing: tileSpacing,
|
||||||
color: Colors.grey.shade700,
|
color: Colors.grey.shade700,
|
||||||
),
|
),
|
||||||
child: child,
|
child: child,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:aves/model/settings/enums.dart';
|
import 'package:aves/model/settings/enums.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
@ -170,7 +171,7 @@ class OSMHotLayer extends StatelessWidget {
|
||||||
options: TileLayerOptions(
|
options: TileLayerOptions(
|
||||||
urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
|
urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
|
||||||
subdomains: ['a', 'b', 'c'],
|
subdomains: ['a', 'b', 'c'],
|
||||||
retinaMode: MediaQuery.of(context).devicePixelRatio > 1,
|
retinaMode: context.select<MediaQueryData, double>((mq) => mq.devicePixelRatio) > 1,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -183,7 +184,7 @@ class StamenTonerLayer extends StatelessWidget {
|
||||||
options: TileLayerOptions(
|
options: TileLayerOptions(
|
||||||
urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png',
|
urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png',
|
||||||
subdomains: ['a', 'b', 'c', 'd'],
|
subdomains: ['a', 'b', 'c', 'd'],
|
||||||
retinaMode: MediaQuery.of(context).devicePixelRatio > 1,
|
retinaMode: context.select<MediaQueryData, double>((mq) => mq.devicePixelRatio) > 1,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -196,7 +197,7 @@ class StamenWatercolorLayer extends StatelessWidget {
|
||||||
options: TileLayerOptions(
|
options: TileLayerOptions(
|
||||||
urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg',
|
urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg',
|
||||||
subdomains: ['a', 'b', 'c', 'd'],
|
subdomains: ['a', 'b', 'c', 'd'],
|
||||||
retinaMode: MediaQuery.of(context).devicePixelRatio > 1,
|
retinaMode: context.select<MediaQueryData, double>((mq) => mq.devicePixelRatio) > 1,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:aves/widgets/collection/thumbnail/raster.dart';
|
||||||
import 'package:aves/widgets/collection/thumbnail/vector.dart';
|
import 'package:aves/widgets/collection/thumbnail/vector.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class ImageMarker extends StatelessWidget {
|
class ImageMarker extends StatelessWidget {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
|
@ -153,7 +154,7 @@ class _MarkerGeneratorWidgetState extends State<MarkerGeneratorWidget> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Transform.translate(
|
return Transform.translate(
|
||||||
offset: Offset(MediaQuery.of(context).size.width, 0),
|
offset: Offset(context.select<MediaQueryData, double>((mq) => mq.size.width), 0),
|
||||||
child: Material(
|
child: Material(
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
@ -171,7 +172,7 @@ class _MarkerGeneratorWidgetState extends State<MarkerGeneratorWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Uint8List>> _getBitmaps(BuildContext context) async {
|
Future<List<Uint8List>> _getBitmaps(BuildContext context) async {
|
||||||
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final pixelRatio = context.read<MediaQueryData>().devicePixelRatio;
|
||||||
return Future.wait(_globalKeys.map((key) async {
|
return Future.wait(_globalKeys.map((key) async {
|
||||||
RenderRepaintBoundary boundary = key.currentContext.findRenderObject();
|
RenderRepaintBoundary boundary = key.currentContext.findRenderObject();
|
||||||
final image = await boundary.toImage(pixelRatio: pixelRatio);
|
final image = await boundary.toImage(pixelRatio: pixelRatio);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/services/metadata_service.dart';
|
import 'package:aves/services/metadata_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
enum MetadataThumbnailSource { embedded, exif }
|
enum MetadataThumbnailSource { embedded, exif }
|
||||||
|
|
||||||
|
@ -47,7 +48,6 @@ class _MetadataThumbnailsState extends State<MetadataThumbnails> {
|
||||||
future: _loader,
|
future: _loader,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data.isNotEmpty) {
|
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data.isNotEmpty) {
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
|
||||||
return Container(
|
return Container(
|
||||||
alignment: AlignmentDirectional.topStart,
|
alignment: AlignmentDirectional.topStart,
|
||||||
padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 4),
|
padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 4),
|
||||||
|
@ -55,7 +55,7 @@ class _MetadataThumbnailsState extends State<MetadataThumbnails> {
|
||||||
children: snapshot.data.map((bytes) {
|
children: snapshot.data.map((bytes) {
|
||||||
return Image.memory(
|
return Image.memory(
|
||||||
bytes,
|
bytes,
|
||||||
scale: devicePixelRatio,
|
scale: context.select<MediaQueryData, double>((mq) => mq.devicePixelRatio),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -3,12 +3,14 @@ import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/basic/labeled_checkbox.dart';
|
import 'package:aves/widgets/common/basic/labeled_checkbox.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_logo.dart';
|
import 'package:aves/widgets/common/identity/aves_logo.dart';
|
||||||
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/home_page.dart';
|
import 'package:aves/widgets/home_page.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class WelcomePage extends StatefulWidget {
|
class WelcomePage extends StatefulWidget {
|
||||||
|
@ -30,12 +32,13 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return MediaQueryDataProvider(
|
||||||
body: SafeArea(
|
child: Scaffold(
|
||||||
child: Container(
|
body: SafeArea(
|
||||||
alignment: Alignment.center,
|
child: Container(
|
||||||
padding: EdgeInsets.all(16.0),
|
alignment: Alignment.center,
|
||||||
child: FutureBuilder<String>(
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: FutureBuilder<String>(
|
||||||
future: _termsLoader,
|
future: _termsLoader,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
|
if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
|
||||||
|
@ -59,7 +62,9 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -71,7 +76,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
style: Theme.of(context).textTheme.headline5,
|
style: Theme.of(context).textTheme.headline5,
|
||||||
);
|
);
|
||||||
return [
|
return [
|
||||||
...(MediaQuery.of(context).orientation == Orientation.portrait
|
...(context.select<MediaQueryData, Orientation>((mq) => mq.orientation) == Orientation.portrait
|
||||||
? [
|
? [
|
||||||
AvesLogo(size: 64),
|
AvesLogo(size: 64),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
|
@ -126,7 +131,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
child: Text(context.l10n.continueButtonLabel),
|
child: Text(context.l10n.continueButtonLabel),
|
||||||
);
|
);
|
||||||
|
|
||||||
return MediaQuery.of(context).orientation == Orientation.portrait
|
return context.select<MediaQueryData, Orientation>((mq) => mq.orientation) == Orientation.portrait
|
||||||
? [
|
? [
|
||||||
checkboxes,
|
checkboxes,
|
||||||
button,
|
button,
|
||||||
|
|
Loading…
Reference in a new issue