1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-10 23:08:59 +00:00
Files
samples/experimental/desktop_photo_search/lib/src/widgets/data_tree.dart
2019-12-11 07:22:42 +11:00

351 lines
9.4 KiB
Dart

// Copyright 2019 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class DataTreeRule extends StatelessWidget {
const DataTreeRule({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 2,
color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1),
);
}
}
class DataTreeInkWell extends StatefulWidget {
const DataTreeInkWell({Key key, this.onTap, this.isSelected, this.child})
: super(key: key);
final VoidCallback onTap;
final bool isSelected;
final Widget child;
@override
_DataTreeInkWellState createState() => _DataTreeInkWellState();
}
class _DataTreeInkWellState extends State<DataTreeInkWell>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation selectionColor;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 100), vsync: this);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(DataTreeInkWell oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isSelected) {
controller.forward();
} else {
controller.reverse();
}
}
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final Animation<Color> backgroundColor = controller
.drive(CurveTween(curve: Curves.fastOutSlowIn))
.drive(ColorTween(
begin: colorScheme.primary.withOpacity(0.0),
end: colorScheme.primary.withOpacity(0.08),
));
final Animation<Color> iconColor = controller
.drive(CurveTween(curve: Curves.fastOutSlowIn))
.drive(ColorTween(
begin: colorScheme.onBackground.withOpacity(0.54),
end: colorScheme.onBackground.withOpacity(0.87),
));
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
return IconTheme(
data: IconThemeData(color: iconColor.value),
child: Container(
color: backgroundColor.value,
child: child,
),
);
},
child: InkWell(
onTap: widget.onTap,
splashFactory: InkRipple.splashFactory,
splashColor: colorScheme.primary.withOpacity(0.14),
highlightColor: Colors.transparent,
child: widget.child,
),
);
}
}
class DataTreeNode extends StatefulWidget {
const DataTreeNode({
Key key,
this.leading,
@required this.title,
this.backgroundColor,
this.onExpansionChanged,
this.onSelectionChanged,
this.children = const <Widget>[],
this.initiallyExpanded = false,
this.indent = 0,
this.height = 36,
}) : assert(initiallyExpanded != null),
assert(indent != null && indent >= 0),
super(key: key);
final Widget leading;
final Widget title;
final ValueChanged<bool> onExpansionChanged;
final ValueChanged<bool> onSelectionChanged;
final List<Widget> children;
final Color backgroundColor;
final bool initiallyExpanded;
final double indent;
final double height;
@override
_DataTreeNodeState createState() => _DataTreeNodeState();
}
class _DataTreeNodeState extends State<DataTreeNode>
with SingleTickerProviderStateMixin {
static final Animatable<double> _easeInTween =
CurveTween(curve: Curves.easeIn);
static final Animatable<double> _halfTween =
Tween<double>(begin: 0.0, end: 0.25);
AnimationController _controller;
Animation<double> _iconTurns;
Animation<double> _heightFactor;
bool _isExpanded = false;
bool _isSelected = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 200), vsync: this);
_heightFactor = _controller.drive(_easeInTween);
_iconTurns = _controller.drive(_halfTween.chain(_easeInTween));
_isExpanded = (PageStorage.of(context)?.readState(context) ??
widget.initiallyExpanded) as bool;
if (_isExpanded) {
_controller.value = 1.0;
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleNodeTap() {
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_controller.forward();
} else {
_controller.reverse().then<void>((value) {
if (!mounted) {
return;
}
setState(() {
// Rebuild without widget.children.
});
});
}
PageStorage.of(context)?.writeState(context, _isExpanded);
});
if (widget.onExpansionChanged != null) {
widget.onExpansionChanged(_isExpanded);
}
}
void _handleLeafTap() {
_isSelected = !_isSelected;
if (widget.onSelectionChanged != null) {
widget.onSelectionChanged(_isSelected);
}
}
Widget _buildChildren(BuildContext context, Widget child) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
DataTreeInkWell(
onTap: _handleNodeTap,
isSelected: _isExpanded,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
SizedBox(width: widget.indent),
RotationTransition(
turns: _iconTurns,
child: Icon(
Icons.arrow_right,
size: 20,
),
),
const SizedBox(width: 8),
Icon(
Icons.folder,
size: 20,
),
const SizedBox(width: 16),
widget.title,
],
),
),
if (child != null) // If child == null, then this DataNode is closed.
const DataTreeRule(),
if (child != null)
ClipRect(
child: Align(
alignment: Alignment.topLeft,
heightFactor: _heightFactor.value,
child: child,
),
),
],
);
}
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final Color textColor = colorScheme.onBackground.withOpacity(0.87);
final bool closed = !_isExpanded && _controller.isDismissed;
return widget.children.isEmpty
// Leaf node.
? DataTreeInkWell(
onTap: _handleLeafTap,
isSelected: _isSelected,
child: Row(
children: <Widget>[
SizedBox(width: widget.indent),
Icon(
Icons.web_asset,
size: 20,
),
const SizedBox(width: 16),
DefaultTextStyle(
style: Theme.of(context).textTheme.body1.copyWith(
color: textColor,
),
child: widget.title,
),
],
),
)
// Not a leaf node.
: AnimatedBuilder(
animation: _controller.view,
builder: _buildChildren,
child: closed
? null
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
for (int index = 0;
index < (widget.children.length * 2) - 1;
index += 1)
(index % 2 == 1)
? const DataTreeRule()
: widget.children[index ~/ 2],
],
),
);
}
}
// One entry in the multilevel list displayed by this app.
class Entry {
Entry(this.title, [this.children = const <Entry>[]]);
final String title;
final List<Entry> children;
bool isSelected = false;
bool isEnabled = true;
}
// A visualization of one Entry based on DataTreeNode.
class EntryItem extends StatefulWidget {
const EntryItem({Key key, this.entry}) : super(key: key);
final Entry entry;
@override
_EntryItemState createState() => _EntryItemState();
}
class _EntryItemState extends State<EntryItem> {
Widget _buildNodes(Entry root, double indent) {
return DataTreeNode(
key: PageStorageKey<Entry>(root),
onSelectionChanged: (isSelected) {
setState(() {
root.isSelected = isSelected;
});
},
title: Container(
alignment: AlignmentDirectional.centerStart,
height: 36,
child: Text(root.title),
),
indent: indent,
children: root.children.map<Widget>((entry) {
return _buildNodes(entry, indent + 28);
}).toList(),
);
}
@override
Widget build(BuildContext context) {
return _buildNodes(widget.entry, 16);
}
}
class DataTree extends StatelessWidget {
DataTree(this.entries)
: assert(entries != null),
assert(entries.isNotEmpty);
final List<Entry> entries;
@override
Widget build(BuildContext context) => ListView.builder(
itemCount: entries.length * 2 - 1,
itemBuilder: (context, index) {
if (index % 2 == 1) {
return const DataTreeRule();
}
return EntryItem(
entry: entries[index ~/ 2],
);
},
);
}