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;
|
||||
|
||||
// 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
|
||||
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/identity/empty.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/tile_extent_manager.dart';
|
||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ThumbnailCollection extends StatelessWidget {
|
||||
final ValueNotifier<double> _appBarHeightNotifier = 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
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final viewportSize = constraints.biggest;
|
||||
assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.');
|
||||
if (viewportSize.isEmpty) return SizedBox.shrink();
|
||||
|
||||
final tileExtentManager = TileExtentManager(
|
||||
settingsRouteKey: context.currentRouteName,
|
||||
extentNotifier: _tileExtentNotifier,
|
||||
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;
|
||||
},
|
||||
);
|
||||
},
|
||||
child: TileExtentControllerProvider(
|
||||
controller: TileExtentController(
|
||||
settingsRouteKey: context.currentRouteName,
|
||||
extentNotifier: _tileExtentNotifier,
|
||||
columnCountDefault: 4,
|
||||
extentMin: 46,
|
||||
spacing: 0,
|
||||
),
|
||||
child: _ThumbnailCollectionContent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
final GlobalKey scrollableKey;
|
||||
final CollectionLens collection;
|
||||
|
@ -152,7 +168,6 @@ class CollectionScrollView extends StatefulWidget {
|
|||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final ValueNotifier<bool> isScrollingNotifier;
|
||||
final ScrollController scrollController;
|
||||
final double cacheExtent;
|
||||
|
||||
const CollectionScrollView({
|
||||
@required this.scrollableKey,
|
||||
|
@ -161,7 +176,6 @@ class CollectionScrollView extends StatefulWidget {
|
|||
@required this.appBarHeightNotifier,
|
||||
@required this.isScrollingNotifier,
|
||||
@required this.scrollController,
|
||||
@required this.cacheExtent,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -216,7 +230,7 @@ class _CollectionScrollViewState extends State<CollectionScrollView> {
|
|||
// workaround to prevent scrolling the app bar away
|
||||
// when there is no content and we use `SliverFillRemaining`
|
||||
physics: collection.isEmpty ? NeverScrollableScrollPhysics() : SloppyScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
|
||||
cacheExtent: widget.cacheExtent,
|
||||
cacheExtent: context.select<TileExtentController, double>((controller) => controller.effectiveExtentMax * 2),
|
||||
slivers: [
|
||||
appBar,
|
||||
collection.isEmpty
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AvesCircleBorder {
|
||||
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) {
|
||||
return Border.fromBorderSide(buildSide(context));
|
||||
|
|
|
@ -27,6 +27,11 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return ProxyProvider0<SectionedListLayout<T>>(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
@ -138,6 +143,16 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
|||
double getHeaderExtent(BuildContext context, SectionKey sectionKey);
|
||||
|
||||
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> {
|
||||
|
@ -237,6 +252,12 @@ class SectionLayout {
|
|||
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
|
||||
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/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/rendering.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
// metadata to identify entry from RenderObject hit test during collection scaling
|
||||
class ScalerMetadata<T> {
|
||||
|
@ -14,10 +15,8 @@ class ScalerMetadata<T> {
|
|||
}
|
||||
|
||||
class GridScaleGestureDetector<T> extends StatefulWidget {
|
||||
final TileExtentManager tileExtentManager;
|
||||
final GlobalKey scrollableKey;
|
||||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final Size viewportSize;
|
||||
final Widget Function(Offset center, double extent, Widget child) gridBuilder;
|
||||
final Widget Function(T item, double extent) scaledBuilder;
|
||||
final Rect Function(BuildContext context, T item) getScaledItemTileRect;
|
||||
|
@ -25,10 +24,8 @@ class GridScaleGestureDetector<T> extends StatefulWidget {
|
|||
final Widget child;
|
||||
|
||||
const GridScaleGestureDetector({
|
||||
@required this.tileExtentManager,
|
||||
@required this.scrollableKey,
|
||||
@required this.appBarHeightNotifier,
|
||||
@required this.viewportSize,
|
||||
this.gridBuilder,
|
||||
@required this.scaledBuilder,
|
||||
@required this.getScaledItemTileRect,
|
||||
|
@ -47,10 +44,6 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
|||
OverlayEntry _overlayEntry;
|
||||
ScalerMetadata<T> _metadata;
|
||||
|
||||
TileExtentManager get tileExtentManager => widget.tileExtentManager;
|
||||
|
||||
Size get viewportSize => widget.viewportSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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
|
||||
final gridWidth = scrollableBox.size.width;
|
||||
|
||||
_extentMin = tileExtentManager.getEffectiveExtentMin(viewportSize);
|
||||
_extentMax = tileExtentManager.getEffectiveExtentMax(viewportSize);
|
||||
final tileExtentController = context.read<TileExtentController>();
|
||||
_extentMin = tileExtentController.effectiveExtentMin;
|
||||
_extentMax = tileExtentController.effectiveExtentMax;
|
||||
|
||||
final halfExtent = _startExtent / 2;
|
||||
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent));
|
||||
|
@ -105,10 +99,10 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
|||
}
|
||||
|
||||
_applyingScale = true;
|
||||
final oldExtent = tileExtentManager.extentNotifier.value;
|
||||
final tileExtentController = context.read<TileExtentController>();
|
||||
final oldExtent = tileExtentController.extentNotifier.value;
|
||||
// sanitize and update grid layout if necessary
|
||||
final newExtent = tileExtentManager.applyTileExtent(
|
||||
viewportSize: widget.viewportSize,
|
||||
final newExtent = tileExtentController.applyTileExtent(
|
||||
userPreferredExtent: _scaledExtentNotifier.value,
|
||||
);
|
||||
_scaledExtentNotifier = null;
|
||||
|
@ -195,59 +189,61 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: IgnorePointer(
|
||||
child: AnimatedContainer(
|
||||
decoration: _init
|
||||
? BoxDecoration(
|
||||
gradient: RadialGradient(
|
||||
center: FractionalOffset.fromOffsetAndSize(center, MediaQuery.of(context).size),
|
||||
radius: 1,
|
||||
colors: [
|
||||
Colors.black,
|
||||
Colors.black54,
|
||||
],
|
||||
),
|
||||
)
|
||||
: BoxDecoration(
|
||||
// provide dummy gradient to lerp to the other one during animation
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
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,
|
||||
child: Builder(
|
||||
builder: (context) => IgnorePointer(
|
||||
child: AnimatedContainer(
|
||||
decoration: _init
|
||||
? BoxDecoration(
|
||||
gradient: RadialGradient(
|
||||
center: FractionalOffset.fromOffsetAndSize(center, context.select<MediaQueryData, Size>((mq) => mq.size)),
|
||||
radius: 1,
|
||||
colors: [
|
||||
Colors.black,
|
||||
Colors.black54,
|
||||
],
|
||||
),
|
||||
)
|
||||
: BoxDecoration(
|
||||
// provide dummy gradient to lerp to the other one during animation
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
child = widget.gridBuilder?.call(clampedCenter, extent, child) ?? child;
|
||||
return child;
|
||||
},
|
||||
duration: Durations.collectionScalingBackgroundAnimation,
|
||||
child: ValueListenableBuilder<double>(
|
||||
valueListenable: widget.scaledExtentNotifier,
|
||||
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/widgets/common/extensions/media_query.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DebugTaskQueueOverlay extends StatelessWidget {
|
||||
|
@ -13,9 +12,6 @@ class DebugTaskQueueOverlay extends StatelessWidget {
|
|||
child: SafeArea(
|
||||
child: Container(
|
||||
color: Colors.indigo[900].withAlpha(0xCC),
|
||||
margin: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).effectiveBottomPadding,
|
||||
),
|
||||
padding: EdgeInsets.all(8),
|
||||
child: StreamBuilder<QueueState>(
|
||||
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/scroll_thumb.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/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/filter_grids/common/decorated_filter_chip.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: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 {
|
||||
final Widget appBar;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
|
||||
|
@ -32,17 +35,13 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
final ValueNotifier<String> queryNotifier;
|
||||
final Widget Function() emptyBuilder;
|
||||
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 OffsetFilterCallback onLongPress;
|
||||
|
||||
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
|
||||
final ValueNotifier<double> _tileExtentNotifier = ValueNotifier(0);
|
||||
|
||||
static const columnCountDefault = 2;
|
||||
static const extentMin = 60.0;
|
||||
static const spacing = 8.0;
|
||||
|
||||
FilterGridPage({
|
||||
Key key,
|
||||
@required this.appBar,
|
||||
|
@ -52,12 +51,10 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
this.applyQuery,
|
||||
@required this.emptyBuilder,
|
||||
this.settingsRouteKey,
|
||||
double appBarHeight = kToolbarHeight,
|
||||
this.appBarHeight = kToolbarHeight,
|
||||
@required this.onTap,
|
||||
this.onLongPress,
|
||||
}) : super(key: key) {
|
||||
_appBarHeightNotifier.value = appBarHeight;
|
||||
}
|
||||
}) : super(key: key);
|
||||
|
||||
static const Color detailColor = Color(0xFFE0E0E0);
|
||||
|
||||
|
@ -69,76 +66,25 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
child: GestureAreaProtectorStack(
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final viewportSize = constraints.biggest;
|
||||
assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.');
|
||||
if (viewportSize.isEmpty) return SizedBox.shrink();
|
||||
|
||||
final tileExtentManager = TileExtentManager(
|
||||
settingsRouteKey: settingsRouteKey ?? context.currentRouteName,
|
||||
extentNotifier: _tileExtentNotifier,
|
||||
columnCountDefault: columnCountDefault,
|
||||
extentMin: extentMin,
|
||||
spacing: spacing,
|
||||
)..applyTileExtent(viewportSize: viewportSize);
|
||||
|
||||
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: _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;
|
||||
},
|
||||
);
|
||||
},
|
||||
child: TileExtentControllerProvider(
|
||||
controller: TileExtentController(
|
||||
settingsRouteKey: settingsRouteKey ?? context.currentRouteName,
|
||||
extentNotifier: _tileExtentNotifier,
|
||||
columnCountDefault: 2,
|
||||
extentMin: 60,
|
||||
spacing: 8,
|
||||
),
|
||||
child: _FilterGridPageContent<T>(
|
||||
appBar: appBar,
|
||||
filterSections: filterSections,
|
||||
showHeaders: showHeaders,
|
||||
queryNotifier: queryNotifier,
|
||||
applyQuery: applyQuery,
|
||||
emptyBuilder: emptyBuilder,
|
||||
appBarHeight: appBarHeight,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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 {
|
||||
final Widget appBar;
|
||||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final Map<ChipSectionKey, List<FilterGridItem<T>>> visibleFilterSections;
|
||||
final Widget Function() emptyBuilder;
|
||||
final Size viewportSize;
|
||||
final TileExtentManager tileExtentManager;
|
||||
final ScrollController scrollController;
|
||||
|
||||
const _SectionedContent({
|
||||
|
@ -164,8 +197,6 @@ class _SectionedContent<T extends CollectionFilter> extends StatefulWidget {
|
|||
@required this.appBarHeightNotifier,
|
||||
@required this.visibleFilterSections,
|
||||
@required this.emptyBuilder,
|
||||
@required this.viewportSize,
|
||||
@required this.tileExtentManager,
|
||||
@required this.scrollController,
|
||||
});
|
||||
|
||||
|
@ -182,10 +213,6 @@ class _SectionedContentState<T extends CollectionFilter> extends State<_Sectione
|
|||
|
||||
Widget Function() get emptyBuilder => widget.emptyBuilder;
|
||||
|
||||
Size get viewportSize => widget.viewportSize;
|
||||
|
||||
TileExtentManager get tileExtentManager => widget.tileExtentManager;
|
||||
|
||||
ScrollController get scrollController => widget.scrollController;
|
||||
|
||||
final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable');
|
||||
|
@ -232,17 +259,15 @@ class _SectionedContentState<T extends CollectionFilter> extends State<_Sectione
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pinnedFilters = settings.pinnedFilters;
|
||||
|
||||
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
|
||||
return GridScaleGestureDetector<FilterGridItem<T>>(
|
||||
tileExtentManager: tileExtentManager,
|
||||
scrollableKey: _scrollableKey,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
viewportSize: viewportSize,
|
||||
gridBuilder: (center, extent, child) => CustomPaint(
|
||||
painter: GridPainter(
|
||||
center: center,
|
||||
extent: extent,
|
||||
spacing: tileExtentManager.spacing,
|
||||
spacing: tileSpacing,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
child: child,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:provider/provider.dart';
|
||||
import 'package:aves/model/settings/enums.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
|
@ -170,7 +171,7 @@ class OSMHotLayer extends StatelessWidget {
|
|||
options: TileLayerOptions(
|
||||
urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
|
||||
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(
|
||||
urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png',
|
||||
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(
|
||||
urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg',
|
||||
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:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ImageMarker extends StatelessWidget {
|
||||
final AvesEntry entry;
|
||||
|
@ -153,7 +154,7 @@ class _MarkerGeneratorWidgetState extends State<MarkerGeneratorWidget> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Transform.translate(
|
||||
offset: Offset(MediaQuery.of(context).size.width, 0),
|
||||
offset: Offset(context.select<MediaQueryData, double>((mq) => mq.size.width), 0),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Stack(
|
||||
|
@ -171,7 +172,7 @@ class _MarkerGeneratorWidgetState extends State<MarkerGeneratorWidget> {
|
|||
}
|
||||
|
||||
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 {
|
||||
RenderRepaintBoundary boundary = key.currentContext.findRenderObject();
|
||||
final image = await boundary.toImage(pixelRatio: pixelRatio);
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/services/metadata_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
enum MetadataThumbnailSource { embedded, exif }
|
||||
|
||||
|
@ -47,7 +48,6 @@ class _MetadataThumbnailsState extends State<MetadataThumbnails> {
|
|||
future: _loader,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data.isNotEmpty) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
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) {
|
||||
return Image.memory(
|
||||
bytes,
|
||||
scale: devicePixelRatio,
|
||||
scale: context.select<MediaQueryData, double>((mq) => mq.devicePixelRatio),
|
||||
);
|
||||
}).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/extensions/build_context.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:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class WelcomePage extends StatefulWidget {
|
||||
|
@ -30,12 +32,13 @@ class _WelcomePageState extends State<WelcomePage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: FutureBuilder<String>(
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: FutureBuilder<String>(
|
||||
future: _termsLoader,
|
||||
builder: (context, snapshot) {
|
||||
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,
|
||||
);
|
||||
return [
|
||||
...(MediaQuery.of(context).orientation == Orientation.portrait
|
||||
...(context.select<MediaQueryData, Orientation>((mq) => mq.orientation) == Orientation.portrait
|
||||
? [
|
||||
AvesLogo(size: 64),
|
||||
SizedBox(height: 16),
|
||||
|
@ -126,7 +131,7 @@ class _WelcomePageState extends State<WelcomePage> {
|
|||
child: Text(context.l10n.continueButtonLabel),
|
||||
);
|
||||
|
||||
return MediaQuery.of(context).orientation == Orientation.portrait
|
||||
return context.select<MediaQueryData, Orientation>((mq) => mq.orientation) == Orientation.portrait
|
||||
? [
|
||||
checkboxes,
|
||||
button,
|
||||
|
|
Loading…
Reference in a new issue