info: use slivers for dynamic building of metadata widgets

This commit is contained in:
Thibault Deckers 2019-12-31 18:51:24 +09:00
parent 73663b8e46
commit 5fdbe0887b
5 changed files with 150 additions and 140 deletions

View file

@ -51,31 +51,47 @@ class InfoPageState extends State<InfoPage> {
body: SafeArea( body: SafeArea(
child: NotificationListener( child: NotificationListener(
onNotification: _handleTopScroll, onNotification: _handleTopScroll,
child: Selector<MediaQueryData, Tuple2<Orientation, double>>( child: Selector<MediaQueryData, Tuple2<double, double>>(
selector: (c, mq) => Tuple2(mq.orientation, mq.viewInsets.bottom), selector: (c, mq) => Tuple2(mq.size.width, mq.viewInsets.bottom),
builder: (c, mq, child) { builder: (c, mq, child) {
final mqOrientation = mq.item1; final mqWidth = mq.item1;
final mqViewInsetsBottom = mq.item2; final mqViewInsetsBottom = mq.item2;
final split = mqWidth > 400;
return ListView( return Padding(
padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: mqViewInsetsBottom), padding: const EdgeInsets.symmetric(horizontal: 8),
children: [ child: CustomScrollView(
if (mqOrientation == Orientation.landscape && entry.hasGps) slivers: [
Row( const SliverPadding(
padding: EdgeInsets.only(top: 8),
),
if (split && entry.hasGps)
SliverToBoxAdapter(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded(child: BasicSection(entry: entry)), Expanded(child: BasicSection(entry: entry)),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: LocationSection(entry: entry, showTitle: false)), Expanded(child: LocationSection(entry: entry, showTitle: false)),
], ],
),
) )
else ...[ else
SliverList(
delegate: SliverChildListDelegate(
[
BasicSection(entry: entry), BasicSection(entry: entry),
LocationSection(entry: entry, showTitle: true), LocationSection(entry: entry, showTitle: true),
], ],
XmpTagSection(collection: widget.collection, entry: entry), ),
MetadataSection(entry: entry), ),
XmpTagSectionSliver(collection: widget.collection, entry: entry),
MetadataSectionSliver(entry: entry, columnCount: split ? 2 : 1),
SliverPadding(
padding: EdgeInsets.only(bottom: 8 + mqViewInsetsBottom),
),
], ],
),
); );
}, },
), ),

View file

@ -2,128 +2,103 @@ import 'dart:async';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/model/metadata_service.dart'; import 'package:aves/model/metadata_service.dart';
import 'package:aves/utils/color_utils.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
class MetadataSection extends StatefulWidget { class MetadataSectionSliver extends StatefulWidget {
final ImageEntry entry; final ImageEntry entry;
final int columnCount;
const MetadataSection({this.entry}); const MetadataSectionSliver({
@required this.entry,
@required this.columnCount,
});
@override @override
State<StatefulWidget> createState() => MetadataSectionState(); State<StatefulWidget> createState() => _MetadataSectionSliverState();
} }
class MetadataSectionState extends State<MetadataSection> { class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
Future<Map> _metadataLoader; Map _metadata;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initMetadataLoader(); _getMetadata();
} }
@override @override
void didUpdateWidget(MetadataSection oldWidget) { void didUpdateWidget(MetadataSectionSliver oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
_initMetadataLoader(); _getMetadata();
}
Future<void> _initMetadataLoader() async {
_metadataLoader = MetadataService.getAllMetadata(widget.entry);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<MediaQueryData, double>( debugPrint('$runtimeType build');
selector: (c, mq) => mq.size.width, final directoryNames = (_metadata?.keys?.toList() ?? [])..sort();
builder: (c, mqWidth, child) => FutureBuilder( return SliverStaggeredGrid.countBuilder(
future: _metadataLoader, crossAxisCount: widget.columnCount,
builder: (futureContext, AsyncSnapshot<Map> snapshot) { staggeredTileBuilder: (index) => StaggeredTile.fit(index == 0 ? widget.columnCount : 1),
if (snapshot.hasError) return Text(snapshot.error.toString()); itemBuilder: (context, index) {
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink(); return index == 0
final metadataMap = snapshot.data.cast<String, Map>(); ? const SectionRow('Metadata')
: _Directory(
return Column( metadataMap: _metadata,
crossAxisAlignment: CrossAxisAlignment.stretch, directoryName: directoryNames[index - 1],
children: [
const SectionRow('Metadata'),
_MetadataSectionContent(
metadataMap: metadataMap,
split: mqWidth > 400,
),
],
); );
}, },
), itemCount: directoryNames.isEmpty ? 0 : directoryNames.length + 1,
mainAxisSpacing: 0,
crossAxisSpacing: 8,
); );
} }
}
class _MetadataSectionContent extends StatelessWidget { Future<void> _getMetadata() async {
final Map<String, Map> metadataMap; debugPrint('$runtimeType _getMetadata');
final List<String> directoryNames; _metadata = await MetadataService.getAllMetadata(widget.entry);
final bool split; if (mounted) setState(() {});
_MetadataSectionContent({@required this.metadataMap, @required this.split}) : directoryNames = metadataMap.keys.toList()..sort();
@override
Widget build(BuildContext context) {
if (split) {
final first = <String>[], second = <String>[];
var firstItemCount = 0, secondItemCount = 0;
var firstIndex = 0, secondIndex = directoryNames.length - 1;
while (firstIndex <= secondIndex) {
if (firstItemCount <= secondItemCount) {
final directoryName = directoryNames[firstIndex++];
first.add(directoryName);
firstItemCount += 2 + metadataMap[directoryName].length;
} else {
final directoryName = directoryNames[secondIndex--];
second.insert(0, directoryName);
secondItemCount += 2 + metadataMap[directoryName].length;
}
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _MetadataColumn(metadataMap: metadataMap, directoryNames: first)),
const SizedBox(width: 8),
Expanded(child: _MetadataColumn(metadataMap: metadataMap, directoryNames: second)),
],
);
} else {
return _MetadataColumn(metadataMap: metadataMap, directoryNames: directoryNames);
}
} }
} }
class _MetadataColumn extends StatelessWidget { class _Directory extends StatelessWidget {
final Map<String, Map> metadataMap; final Map metadataMap;
final List<String> directoryNames; final String directoryName;
const _MetadataColumn({@required this.metadataMap, @required this.directoryNames});
static const int maxValueLength = 140; static const int maxValueLength = 140;
const _Directory({@required this.metadataMap, @required this.directoryName});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final directory = metadataMap[directoryName];
final tagKeys = directory.keys.toList()..sort();
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
...directoryNames.expand((directoryName) {
final directory = metadataMap[directoryName];
final tagKeys = directory.keys.toList()..sort();
return [
if (directoryName.isNotEmpty) if (directoryName.isNotEmpty)
Padding( Container(
padding: const EdgeInsets.symmetric(vertical: 4.0), decoration: _DirectoryTitleDecoration(
child: Text(directoryName, color: stringToColor(directoryName),
),
margin: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(
directoryName,
style: const TextStyle( style: const TextStyle(
shadows: [
Shadow(
color: Colors.black,
offset: Offset(1, 1),
blurRadius: 2,
)
],
fontSize: 18, fontSize: 18,
fontFamily: 'Concourse Caps', fontFamily: 'Concourse Caps',
)), ),
),
), ),
...tagKeys.map((tagKey) { ...tagKeys.map((tagKey) {
final value = directory[tagKey] as String; final value = directory[tagKey] as String;
@ -131,9 +106,19 @@ class _MetadataColumn extends StatelessWidget {
return InfoRow(tagKey, value.length > maxValueLength ? '${value.substring(0, maxValueLength)}' : value); return InfoRow(tagKey, value.length > maxValueLength ? '${value.substring(0, maxValueLength)}' : value);
}), }),
const SizedBox(height: 16), const SizedBox(height: 16),
];
}),
], ],
); );
} }
} }
class _DirectoryTitleDecoration extends BoxDecoration {
_DirectoryTitleDecoration({@required Color color})
: super(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
stops: [0, .4, .4],
colors: [color, color, Colors.transparent],
),
);
}

View file

@ -5,11 +5,11 @@ import 'package:aves/widgets/album/filtered_collection_page.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class XmpTagSection extends AnimatedWidget { class XmpTagSectionSliver extends AnimatedWidget {
final ImageCollection collection; final ImageCollection collection;
final ImageEntry entry; final ImageEntry entry;
XmpTagSection({ XmpTagSectionSliver({
Key key, Key key,
@required this.collection, @required this.collection,
@required this.entry, @required this.entry,
@ -18,11 +18,11 @@ class XmpTagSection extends AnimatedWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tags = entry.xmpSubjects; final tags = entry.xmpSubjects;
return tags.isEmpty return SliverList(
? const SizedBox.shrink() delegate: SliverChildListDelegate(
: Column( tags.isEmpty
crossAxisAlignment: CrossAxisAlignment.start, ? []
children: [ : [
const SectionRow('XMP Tags'), const SectionRow('XMP Tags'),
Wrap( Wrap(
spacing: 8, spacing: 8,
@ -40,6 +40,7 @@ class XmpTagSection extends AnimatedWidget {
.toList(), .toList(),
), ),
], ],
),
); );
} }

View file

@ -99,6 +99,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
flutter_staggered_grid_view:
dependency: "direct main"
description:
name: flutter_staggered_grid_view
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
flutter_sticky_header: flutter_sticky_header:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -23,6 +23,7 @@ dependencies:
url: git://github.com/deckerst/flutter-draggable-scrollbar.git url: git://github.com/deckerst/flutter-draggable-scrollbar.git
flushbar: flushbar:
flutter_native_timezone: flutter_native_timezone:
flutter_staggered_grid_view:
flutter_sticky_header: flutter_sticky_header:
git: git:
url: git://github.com/deckerst/flutter_sticky_header.git url: git://github.com/deckerst/flutter_sticky_header.git