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 bool removable;
final bool showGenericIcon;
final Decoration decoration;
final Widget background;
final Widget details;
final HeroType heroType;
final FilterCallback onPressed;
@ -28,7 +28,7 @@ class AvesFilterChip extends StatefulWidget {
this.filter,
this.removable = false,
this.showGenericIcon = true,
this.decoration,
this.background,
this.details,
this.heroType = HeroType.onTap,
@required this.onPressed,
@ -64,11 +64,12 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
@override
Widget build(BuildContext context) {
final hasBackground = widget.background != null;
final leading = filter.iconBuilder(context, AvesFilterChip.iconSize, showGenericIcon: widget.showGenericIcon);
final trailing = widget.removable ? const Icon(AIcons.clear, size: AvesFilterChip.iconSize) : null;
Widget content = Row(
mainAxisSize: widget.decoration != null ? MainAxisSize.max : MainAxisSize.min,
mainAxisSize: hasBackground ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (leading != null) ...[
@ -105,7 +106,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
child: content,
);
if (widget.decoration != null) {
if (hasBackground) {
content = Center(
child: ColoredBox(
color: Colors.black54,
@ -132,45 +133,54 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
maxWidth: AvesFilterChip.maxChipWidth,
minHeight: AvesFilterChip.minChipHeight,
),
decoration: widget.decoration,
child: Tooltip(
message: filter.tooltip,
preferBelow: false,
child: Material(
color: widget.decoration != null ? Colors.transparent : Theme.of(context).scaffoldBackgroundColor,
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
),
child: InkWell(
onTap: widget.onPressed != null
? () {
WidgetsBinding.instance.addPostFrameCallback((_) => widget.onPressed(filter));
setState(() => _tapped = true);
}
: null,
borderRadius: borderRadius,
child: FutureBuilder(
future: _colorFuture,
builder: (context, AsyncSnapshot<Color> snapshot) {
final outlineColor = snapshot.hasData ? snapshot.data : Colors.transparent;
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: outlineColor,
width: AvesFilterChip.outlineWidth,
),
borderRadius: borderRadius,
),
position: DecorationPosition.foreground,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: content,
),
);
},
child: Stack(
fit: StackFit.passthrough,
children: [
if (widget.background != null)
ClipRRect(
borderRadius: borderRadius,
child: widget.background,
),
Tooltip(
message: filter.tooltip,
preferBelow: false,
child: Material(
color: hasBackground ? Colors.transparent : Theme.of(context).scaffoldBackgroundColor,
shape: RoundedRectangleBorder(
borderRadius: borderRadius,
),
child: InkWell(
onTap: widget.onPressed != null
? () {
WidgetsBinding.instance.addPostFrameCallback((_) => widget.onPressed(filter));
setState(() => _tapped = true);
}
: null,
borderRadius: borderRadius,
child: FutureBuilder(
future: _colorFuture,
builder: (context, AsyncSnapshot<Color> snapshot) {
final outlineColor = snapshot.hasData ? snapshot.data : Colors.transparent;
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: outlineColor,
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 '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/image_entry.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/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/common/aves_filter_chip.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/image_providers/thumbnail_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
class FilterNavigationPage extends StatelessWidget {
@ -158,7 +156,7 @@ class FilterGridPage extends StatelessWidget {
}
}
class DecoratedFilterChip extends StatefulWidget {
class DecoratedFilterChip extends StatelessWidget {
final CollectionSource source;
final CollectionFilter filter;
final ImageEntry entry;
@ -171,92 +169,26 @@ class DecoratedFilterChip extends StatefulWidget {
@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
Widget build(BuildContext context) {
if (entry == null || !entry.isSvg) {
Decoration decoration;
if (entry != null) {
decoration = BoxDecoration(
image: DecorationImage(
image: ThumbnailProvider(
Widget backgroundImage;
if (entry != null) {
backgroundImage = entry.isSvg
? ThumbnailVectorImage(
entry: entry,
extent: FilterGridPage.maxCrossAxisExtent,
),
fit: BoxFit.cover,
),
borderRadius: AvesFilterChip.borderRadius,
);
}
return _buildChip(decoration);
)
: ThumbnailRasterImage(
entry: entry,
extent: FilterGridPage.maxCrossAxisExtent,
);
}
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(
filter: filter,
showGenericIcon: false,
decoration: decoration,
background: backgroundImage,
details: _buildDetails(filter),
onPressed: widget.onPressed,
onPressed: onPressed,
);
}