filter grid: background image progressive loading

This commit is contained in:
Thibault Deckers 2020-06-09 11:00:35 +09:00
parent 20c40020c0
commit ff9420fce7
2 changed files with 65 additions and 123 deletions

View file

@ -10,7 +10,7 @@ class AvesFilterChip extends StatefulWidget {
final CollectionFilter filter; final CollectionFilter filter;
final bool removable; final bool removable;
final bool showGenericIcon; final bool showGenericIcon;
final Decoration decoration; final Widget background;
final Widget details; final Widget details;
final HeroType heroType; final HeroType heroType;
final FilterCallback onPressed; final FilterCallback onPressed;
@ -28,7 +28,7 @@ class AvesFilterChip extends StatefulWidget {
this.filter, this.filter,
this.removable = false, this.removable = false,
this.showGenericIcon = true, this.showGenericIcon = true,
this.decoration, this.background,
this.details, this.details,
this.heroType = HeroType.onTap, this.heroType = HeroType.onTap,
@required this.onPressed, @required this.onPressed,
@ -64,11 +64,12 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final hasBackground = widget.background != null;
final leading = filter.iconBuilder(context, AvesFilterChip.iconSize, showGenericIcon: widget.showGenericIcon); final leading = filter.iconBuilder(context, AvesFilterChip.iconSize, showGenericIcon: widget.showGenericIcon);
final trailing = widget.removable ? const Icon(AIcons.clear, size: AvesFilterChip.iconSize) : null; final trailing = widget.removable ? const Icon(AIcons.clear, size: AvesFilterChip.iconSize) : null;
Widget content = Row( Widget content = Row(
mainAxisSize: widget.decoration != null ? MainAxisSize.max : MainAxisSize.min, mainAxisSize: hasBackground ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (leading != null) ...[ if (leading != null) ...[
@ -105,7 +106,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
child: content, child: content,
); );
if (widget.decoration != null) { if (hasBackground) {
content = Center( content = Center(
child: ColoredBox( child: ColoredBox(
color: Colors.black54, color: Colors.black54,
@ -132,45 +133,54 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
maxWidth: AvesFilterChip.maxChipWidth, maxWidth: AvesFilterChip.maxChipWidth,
minHeight: AvesFilterChip.minChipHeight, minHeight: AvesFilterChip.minChipHeight,
), ),
decoration: widget.decoration, child: Stack(
child: Tooltip( fit: StackFit.passthrough,
message: filter.tooltip, children: [
preferBelow: false, if (widget.background != null)
child: Material( ClipRRect(
color: widget.decoration != null ? Colors.transparent : Theme.of(context).scaffoldBackgroundColor, borderRadius: borderRadius,
shape: RoundedRectangleBorder( child: widget.background,
borderRadius: borderRadius, ),
), Tooltip(
child: InkWell( message: filter.tooltip,
onTap: widget.onPressed != null preferBelow: false,
? () { child: Material(
WidgetsBinding.instance.addPostFrameCallback((_) => widget.onPressed(filter)); color: hasBackground ? Colors.transparent : Theme.of(context).scaffoldBackgroundColor,
setState(() => _tapped = true); shape: RoundedRectangleBorder(
} borderRadius: borderRadius,
: null, ),
borderRadius: borderRadius, child: InkWell(
child: FutureBuilder( onTap: widget.onPressed != null
future: _colorFuture, ? () {
builder: (context, AsyncSnapshot<Color> snapshot) { WidgetsBinding.instance.addPostFrameCallback((_) => widget.onPressed(filter));
final outlineColor = snapshot.hasData ? snapshot.data : Colors.transparent; setState(() => _tapped = true);
return DecoratedBox( }
decoration: BoxDecoration( : null,
border: Border.all( borderRadius: borderRadius,
color: outlineColor, child: FutureBuilder(
width: AvesFilterChip.outlineWidth, future: _colorFuture,
), builder: (context, AsyncSnapshot<Color> snapshot) {
borderRadius: borderRadius, final outlineColor = snapshot.hasData ? snapshot.data : Colors.transparent;
), return DecoratedBox(
position: DecorationPosition.foreground, decoration: BoxDecoration(
child: Padding( border: Border.all(
padding: const EdgeInsets.symmetric(vertical: 8), color: outlineColor,
child: content, width: AvesFilterChip.outlineWidth,
), ),
); borderRadius: borderRadius,
}, ),
position: DecorationPosition.foreground,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: content,
),
);
},
),
),
), ),
), ),
), ],
), ),
); );

View file

@ -1,4 +1,3 @@
import 'dart:typed_data';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/collection_lens.dart';
@ -7,17 +6,16 @@ import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/model/settings.dart'; import 'package:aves/model/settings.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/album/collection_page.dart';
import 'package:aves/widgets/album/thumbnail/raster.dart';
import 'package:aves/widgets/album/thumbnail/vector.dart';
import 'package:aves/widgets/app_drawer.dart'; import 'package:aves/widgets/app_drawer.dart';
import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
import 'package:flutter/material.dart'; 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:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class FilterNavigationPage extends StatelessWidget { class FilterNavigationPage extends StatelessWidget {
@ -158,7 +156,7 @@ class FilterGridPage extends StatelessWidget {
} }
} }
class DecoratedFilterChip extends StatefulWidget { class DecoratedFilterChip extends StatelessWidget {
final CollectionSource source; final CollectionSource source;
final CollectionFilter filter; final CollectionFilter filter;
final ImageEntry entry; final ImageEntry entry;
@ -171,92 +169,26 @@ class DecoratedFilterChip extends StatefulWidget {
@required this.onPressed, @required this.onPressed,
}); });
@override
_DecoratedFilterChipState createState() => _DecoratedFilterChipState();
}
class _DecoratedFilterChipState extends State<DecoratedFilterChip> {
CollectionSource get source => widget.source;
CollectionFilter get filter => widget.filter;
ImageEntry get entry => widget.entry;
Future<Uint8List> _svgByteLoader;
@override
void initState() {
super.initState();
_svgByteLoader = _initSvgByteLoader();
}
@override
void didUpdateWidget(DecoratedFilterChip oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.entry != entry) {
_svgByteLoader = _initSvgByteLoader();
}
}
Future<Uint8List> _initSvgByteLoader() async {
if (entry == null || !entry.isSvg) return null;
final uri = entry.uri;
final bytes = await ImageFileService.getImage(uri, entry.mimeType);
if (bytes == null || bytes.isEmpty) return bytes;
final svgRoot = await svg.fromSvgBytes(bytes, uri);
const extent = FilterGridPage.maxCrossAxisExtent;
final picture = svgRoot.toPicture(size: const Size(extent, extent));
final uiImage = await picture.toImage(extent.ceil(), extent.ceil());
final data = await uiImage.toByteData(format: ImageByteFormat.png);
return data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (entry == null || !entry.isSvg) { Widget backgroundImage;
Decoration decoration; if (entry != null) {
if (entry != null) { backgroundImage = entry.isSvg
decoration = BoxDecoration( ? ThumbnailVectorImage(
image: DecorationImage(
image: ThumbnailProvider(
entry: entry, entry: entry,
extent: FilterGridPage.maxCrossAxisExtent, extent: FilterGridPage.maxCrossAxisExtent,
), )
fit: BoxFit.cover, : ThumbnailRasterImage(
), entry: entry,
borderRadius: AvesFilterChip.borderRadius, extent: FilterGridPage.maxCrossAxisExtent,
); );
}
return _buildChip(decoration);
} }
return FutureBuilder(
future: _svgByteLoader,
builder: (context, AsyncSnapshot<Uint8List> snapshot) {
Decoration decoration;
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) {
decoration = BoxDecoration(
image: DecorationImage(
image: MemoryImage(snapshot.data),
fit: BoxFit.cover,
),
borderRadius: AvesFilterChip.borderRadius,
);
}
return _buildChip(decoration);
},
);
}
AvesFilterChip _buildChip(Decoration decoration) {
return AvesFilterChip( return AvesFilterChip(
filter: filter, filter: filter,
showGenericIcon: false, showGenericIcon: false,
decoration: decoration, background: backgroundImage,
details: _buildDetails(filter), details: _buildDetails(filter),
onPressed: widget.onPressed, onPressed: onPressed,
); );
} }