Merge pull request #122 from HansMuller/shrinking-card
Card Collection dismiss animation
This commit is contained in:
commit
c3a8df1d68
@ -15,6 +15,7 @@ import 'package:sky/widgets/widget.dart';
|
|||||||
|
|
||||||
class BlockViewportApp extends App {
|
class BlockViewportApp extends App {
|
||||||
|
|
||||||
|
BlockViewportLayoutState layoutState = new BlockViewportLayoutState();
|
||||||
List<double> lengths = <double>[];
|
List<double> lengths = <double>[];
|
||||||
double offset = 0.0;
|
double offset = 0.0;
|
||||||
|
|
||||||
@ -96,7 +97,8 @@ class BlockViewportApp extends App {
|
|||||||
child: new BlockViewport(
|
child: new BlockViewport(
|
||||||
builder: builder,
|
builder: builder,
|
||||||
startOffset: offset,
|
startOffset: offset,
|
||||||
token: lengths.length
|
token: lengths.length,
|
||||||
|
layoutState: layoutState
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -2,62 +2,145 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:sky/animation/animation_performance.dart';
|
||||||
|
import 'package:sky/animation/curves.dart';
|
||||||
import 'package:sky/base/lerp.dart';
|
import 'package:sky/base/lerp.dart';
|
||||||
import 'package:sky/painting/text_style.dart';
|
import 'package:sky/painting/text_style.dart';
|
||||||
import 'package:sky/theme/colors.dart';
|
import 'package:sky/theme/colors.dart';
|
||||||
|
import 'package:sky/widgets/animated_component.dart';
|
||||||
import 'package:sky/widgets/basic.dart';
|
import 'package:sky/widgets/basic.dart';
|
||||||
|
import 'package:sky/widgets/block_viewport.dart';
|
||||||
import 'package:sky/widgets/card.dart';
|
import 'package:sky/widgets/card.dart';
|
||||||
import 'package:sky/widgets/dismissable.dart';
|
import 'package:sky/widgets/dismissable.dart';
|
||||||
import 'package:sky/widgets/scaffold.dart';
|
|
||||||
import 'package:sky/widgets/variable_height_scrollable.dart';
|
import 'package:sky/widgets/variable_height_scrollable.dart';
|
||||||
|
import 'package:sky/widgets/scaffold.dart';
|
||||||
import 'package:sky/widgets/theme.dart';
|
import 'package:sky/widgets/theme.dart';
|
||||||
import 'package:sky/widgets/tool_bar.dart';
|
import 'package:sky/widgets/tool_bar.dart';
|
||||||
import 'package:sky/widgets/widget.dart';
|
import 'package:sky/widgets/widget.dart';
|
||||||
import 'package:sky/theme/colors.dart' as colors;
|
|
||||||
import 'package:sky/widgets/task_description.dart';
|
import 'package:sky/widgets/task_description.dart';
|
||||||
|
|
||||||
|
class CardModel {
|
||||||
|
CardModel(this.value, this.height, this.color);
|
||||||
|
int value;
|
||||||
|
double height;
|
||||||
|
Color color;
|
||||||
|
AnimationPerformance performance;
|
||||||
|
String get label => "Item $value";
|
||||||
|
String get key => value.toString();
|
||||||
|
bool operator ==(other) => other is CardModel && other.value == value;
|
||||||
|
int get hashCode => 373 * 37 * value.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShrinkingCard extends AnimatedComponent {
|
||||||
|
|
||||||
|
ShrinkingCard({
|
||||||
|
String key,
|
||||||
|
CardModel this.card,
|
||||||
|
Function this.onUpdated,
|
||||||
|
Function this.onCompleted
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
CardModel card;
|
||||||
|
Function onUpdated;
|
||||||
|
Function onCompleted;
|
||||||
|
|
||||||
|
double get currentHeight => card.performance.variable.value;
|
||||||
|
|
||||||
|
void initState() {
|
||||||
|
assert(card.performance != null);
|
||||||
|
card.performance.addListener(handleAnimationProgress);
|
||||||
|
watch(card.performance);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleAnimationProgress() {
|
||||||
|
if (card.performance.isCompleted) {
|
||||||
|
if (onCompleted != null)
|
||||||
|
onCompleted();
|
||||||
|
} else if (onUpdated != null) {
|
||||||
|
onUpdated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void syncFields(ShrinkingCard source) {
|
||||||
|
card = source.card;
|
||||||
|
onCompleted = source.onCompleted;
|
||||||
|
onUpdated = source.onUpdated;
|
||||||
|
super.syncFields(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget build() => new Container(height: currentHeight);
|
||||||
|
}
|
||||||
|
|
||||||
class CardCollectionApp extends App {
|
class CardCollectionApp extends App {
|
||||||
|
|
||||||
final TextStyle cardLabelStyle =
|
final TextStyle cardLabelStyle =
|
||||||
new TextStyle(color: white, fontSize: 18.0, fontWeight: bold);
|
new TextStyle(color: white, fontSize: 18.0, fontWeight: bold);
|
||||||
|
|
||||||
final List<double> cardHeights = [
|
BlockViewportLayoutState layoutState = new BlockViewportLayoutState();
|
||||||
48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0,
|
List<CardModel> cardModels;
|
||||||
48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0,
|
|
||||||
48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0,
|
|
||||||
48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0
|
|
||||||
];
|
|
||||||
|
|
||||||
List<int> visibleCardIndices;
|
|
||||||
|
|
||||||
void initState() {
|
void initState() {
|
||||||
visibleCardIndices = new List.generate(cardHeights.length, (i) => i);
|
List<double> cardHeights = <double>[
|
||||||
|
48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0,
|
||||||
|
48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0,
|
||||||
|
48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0
|
||||||
|
];
|
||||||
|
cardModels = new List.generate(cardHeights.length, (i) {
|
||||||
|
Color color = lerpColor(Red[300], Blue[900], i / cardHeights.length);
|
||||||
|
return new CardModel(i, cardHeights[i], color);
|
||||||
|
});
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void dismissCard(int cardIndex) {
|
void shrinkCard(CardModel card, int index) {
|
||||||
|
if (card.performance != null)
|
||||||
|
return;
|
||||||
|
layoutState.invalidate([index]);
|
||||||
setState(() {
|
setState(() {
|
||||||
visibleCardIndices.remove(cardIndex);
|
assert(card.performance == null);
|
||||||
|
card.performance = new AnimationPerformance()
|
||||||
|
..duration = const Duration(milliseconds: 300)
|
||||||
|
..variable = new AnimatedType<double>(
|
||||||
|
card.height + kCardMargins.top + kCardMargins.bottom,
|
||||||
|
end: 0.0,
|
||||||
|
curve: ease,
|
||||||
|
interval: new Interval(0.5, 1.0)
|
||||||
|
)
|
||||||
|
..play();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _builder(int index) {
|
void dismissCard(CardModel card) {
|
||||||
if (index >= visibleCardIndices.length)
|
if (cardModels.contains(card)) {
|
||||||
return null;
|
setState(() {
|
||||||
|
cardModels.remove(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget builder(int index) {
|
||||||
|
if (index >= cardModels.length)
|
||||||
|
return null;
|
||||||
|
CardModel card = cardModels[index];
|
||||||
|
|
||||||
|
if (card.performance != null) {
|
||||||
|
return new ShrinkingCard(
|
||||||
|
key: card.key,
|
||||||
|
card: card,
|
||||||
|
onUpdated: () { layoutState.invalidate([index]); },
|
||||||
|
onCompleted: () { dismissCard(card); }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
int cardIndex = visibleCardIndices[index];
|
|
||||||
Color color = lerpColor(Red[500], Blue[500], cardIndex / cardHeights.length);
|
|
||||||
Widget label = new Text("Item ${cardIndex}", style: cardLabelStyle);
|
|
||||||
return new Dismissable(
|
return new Dismissable(
|
||||||
key: cardIndex.toString(),
|
key: card.key,
|
||||||
onDismissed: () { dismissCard(cardIndex); },
|
onDismissed: () { shrinkCard(card, index); },
|
||||||
child: new Card(
|
child: new Card(
|
||||||
color: color,
|
color: card.color,
|
||||||
child: new Container(
|
child: new Container(
|
||||||
height: cardHeights[cardIndex],
|
height: card.height,
|
||||||
padding: const EdgeDims.all(8.0),
|
padding: const EdgeDims.all(8.0),
|
||||||
child: new Center(child: label)
|
child: new Center(child: new Text(card.label, style: cardLabelStyle))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -68,16 +151,17 @@ class CardCollectionApp extends App {
|
|||||||
padding: const EdgeDims.symmetric(vertical: 12.0, horizontal: 8.0),
|
padding: const EdgeDims.symmetric(vertical: 12.0, horizontal: 8.0),
|
||||||
decoration: new BoxDecoration(backgroundColor: Theme.of(this).primarySwatch[50]),
|
decoration: new BoxDecoration(backgroundColor: Theme.of(this).primarySwatch[50]),
|
||||||
child: new VariableHeightScrollable(
|
child: new VariableHeightScrollable(
|
||||||
builder: _builder,
|
builder: builder,
|
||||||
token: visibleCardIndices.length
|
token: cardModels.length,
|
||||||
|
layoutState: layoutState
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return new Theme(
|
return new Theme(
|
||||||
data: new ThemeData(
|
data: new ThemeData(
|
||||||
brightness: ThemeBrightness.light,
|
brightness: ThemeBrightness.light,
|
||||||
primarySwatch: colors.Blue,
|
primarySwatch: Blue,
|
||||||
accentColor: colors.RedAccent[200]
|
accentColor: RedAccent[200]
|
||||||
),
|
),
|
||||||
child: new TaskDescription(
|
child: new TaskDescription(
|
||||||
label: 'Cards',
|
label: 'Cards',
|
||||||
|
@ -12,13 +12,6 @@ import 'package:sky/widgets/widget.dart';
|
|||||||
// return null if index is greater than index of last entry
|
// return null if index is greater than index of last entry
|
||||||
typedef Widget IndexedBuilder(int index);
|
typedef Widget IndexedBuilder(int index);
|
||||||
|
|
||||||
typedef void LayoutChangedCallback(
|
|
||||||
int firstVisibleChildIndex,
|
|
||||||
int visibleChildCount,
|
|
||||||
UnmodifiableListView<double> childOffsets,
|
|
||||||
bool didReachLastChild
|
|
||||||
);
|
|
||||||
|
|
||||||
class _Key {
|
class _Key {
|
||||||
const _Key(this.type, this.key);
|
const _Key(this.type, this.key);
|
||||||
factory _Key.fromWidget(Widget widget) => new _Key(widget.runtimeType, widget.key);
|
factory _Key.fromWidget(Widget widget) => new _Key(widget.runtimeType, widget.key);
|
||||||
@ -26,24 +19,80 @@ class _Key {
|
|||||||
final String key;
|
final String key;
|
||||||
bool operator ==(other) => other is _Key && other.type == type && other.key == key;
|
bool operator ==(other) => other is _Key && other.type == type && other.key == key;
|
||||||
int get hashCode => 373 * 37 * type.hashCode + key.hashCode;
|
int get hashCode => 373 * 37 * type.hashCode + key.hashCode;
|
||||||
|
String toString() => "_Key(type: $type, key: $key)";
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef void LayoutChangedCallback();
|
||||||
|
|
||||||
|
class BlockViewportLayoutState {
|
||||||
|
BlockViewportLayoutState()
|
||||||
|
: _childOffsets = <double>[0.0],
|
||||||
|
_firstVisibleChildIndex = 0,
|
||||||
|
_visibleChildCount = 0,
|
||||||
|
_didReachLastChild = false
|
||||||
|
{
|
||||||
|
_readOnlyChildOffsets = new UnmodifiableListView<double>(_childOffsets);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<_Key, Widget> _childrenByKey = new Map<_Key, Widget>();
|
||||||
|
bool _dirty = true;
|
||||||
|
|
||||||
|
int _firstVisibleChildIndex;
|
||||||
|
int get firstVisibleChildIndex => _firstVisibleChildIndex;
|
||||||
|
|
||||||
|
int _visibleChildCount;
|
||||||
|
int get visibleChildCount => _visibleChildCount;
|
||||||
|
|
||||||
|
// childOffsets contains the offsets of each child from the top of the
|
||||||
|
// list up to the last one we've ever created, and the offset of the
|
||||||
|
// end of the last one. If there are no children, then the only offset
|
||||||
|
// is 0.0.
|
||||||
|
List<double> _childOffsets;
|
||||||
|
UnmodifiableListView<double> _readOnlyChildOffsets;
|
||||||
|
UnmodifiableListView<double> get childOffsets => _readOnlyChildOffsets;
|
||||||
|
double get contentsSize => _childOffsets.last;
|
||||||
|
|
||||||
|
bool _didReachLastChild;
|
||||||
|
bool get didReachLastChild => _didReachLastChild;
|
||||||
|
|
||||||
|
Set<int> _invalidIndices = new Set<int>();
|
||||||
|
bool get isValid => _invalidIndices.length == 0;
|
||||||
|
// Notify the BlockViewport that the children at indices have either
|
||||||
|
// changed size and/or changed type.
|
||||||
|
void invalidate(Iterable<int> indices) {
|
||||||
|
_invalidIndices.addAll(indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Function> _listeners = new List<Function>();
|
||||||
|
void addListener(Function listener) {
|
||||||
|
_listeners.add(listener);
|
||||||
|
}
|
||||||
|
void removeListener(Function listener) {
|
||||||
|
_listeners.remove(listener);
|
||||||
|
}
|
||||||
|
void _notifyListeners() {
|
||||||
|
List<Function> localListeners = new List<Function>.from(_listeners);
|
||||||
|
for (Function listener in localListeners)
|
||||||
|
listener();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BlockViewport extends RenderObjectWrapper {
|
class BlockViewport extends RenderObjectWrapper {
|
||||||
BlockViewport({ this.builder, this.startOffset, this.token, this.onLayoutChanged, String key })
|
BlockViewport({ this.builder, this.startOffset, this.token, this.layoutState, String key })
|
||||||
: super(key: key);
|
: super(key: key) {
|
||||||
|
assert(this.layoutState != null);
|
||||||
|
}
|
||||||
|
|
||||||
IndexedBuilder builder;
|
IndexedBuilder builder;
|
||||||
double startOffset;
|
double startOffset;
|
||||||
Object token;
|
Object token;
|
||||||
LayoutChangedCallback onLayoutChanged;
|
BlockViewportLayoutState layoutState;
|
||||||
|
|
||||||
RenderBlockViewport get root => super.root;
|
RenderBlockViewport get root => super.root;
|
||||||
RenderBlockViewport createNode() => new RenderBlockViewport();
|
RenderBlockViewport createNode() => new RenderBlockViewport();
|
||||||
|
|
||||||
Map<_Key, Widget> _childrenByKey = new Map<_Key, Widget>();
|
|
||||||
|
|
||||||
void walkChildren(WidgetTreeWalker walker) {
|
void walkChildren(WidgetTreeWalker walker) {
|
||||||
for (Widget child in _childrenByKey.values)
|
for (Widget child in layoutState._childrenByKey.values)
|
||||||
walker(child);
|
walker(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +118,7 @@ class BlockViewport extends RenderObjectWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void remove() {
|
void remove() {
|
||||||
for (Widget child in _childrenByKey.values) {
|
for (Widget child in layoutState._childrenByKey.values) {
|
||||||
assert(child != null);
|
assert(child != null);
|
||||||
removeChild(child);
|
removeChild(child);
|
||||||
}
|
}
|
||||||
@ -86,23 +135,15 @@ class BlockViewport extends RenderObjectWrapper {
|
|||||||
super.didUnmount();
|
super.didUnmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
// _offsets contains the offsets of each child from the top of the
|
|
||||||
// list up to the last one we've ever created, and the offset of the
|
|
||||||
// end of the last one. If there's no children, then the only offset
|
|
||||||
// is 0.0.
|
|
||||||
List<double> _offsets = <double>[0.0];
|
|
||||||
int _currentStartIndex = 0;
|
|
||||||
int _currentChildCount = 0;
|
|
||||||
bool _didReachLastChild = false;
|
|
||||||
|
|
||||||
int _findIndexForOffsetBeforeOrAt(double offset) {
|
int _findIndexForOffsetBeforeOrAt(double offset) {
|
||||||
|
final List<double> offsets = layoutState._childOffsets;
|
||||||
int left = 0;
|
int left = 0;
|
||||||
int right = _offsets.length - 1;
|
int right = offsets.length - 1;
|
||||||
while (right >= left) {
|
while (right >= left) {
|
||||||
int middle = left + ((right - left) ~/ 2);
|
int middle = left + ((right - left) ~/ 2);
|
||||||
if (_offsets[middle] < offset) {
|
if (offsets[middle] < offset) {
|
||||||
left = middle + 1;
|
left = middle + 1;
|
||||||
} else if (_offsets[middle] > offset) {
|
} else if (offsets[middle] > offset) {
|
||||||
right = middle - 1;
|
right = middle - 1;
|
||||||
} else {
|
} else {
|
||||||
return middle;
|
return middle;
|
||||||
@ -111,94 +152,154 @@ class BlockViewport extends RenderObjectWrapper {
|
|||||||
return right;
|
return right;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _dirty = true;
|
|
||||||
|
|
||||||
bool retainStatefulNodeIfPossible(BlockViewport newNode) {
|
bool retainStatefulNodeIfPossible(BlockViewport newNode) {
|
||||||
|
assert(layoutState == newNode.layoutState);
|
||||||
retainStatefulRenderObjectWrapper(newNode);
|
retainStatefulRenderObjectWrapper(newNode);
|
||||||
if (startOffset != newNode.startOffset) {
|
if (startOffset != newNode.startOffset) {
|
||||||
_dirty = true;
|
layoutState._dirty = true;
|
||||||
startOffset = newNode.startOffset;
|
startOffset = newNode.startOffset;
|
||||||
}
|
}
|
||||||
if (token != newNode.token || builder != newNode.builder) {
|
if (token != newNode.token || builder != newNode.builder) {
|
||||||
_dirty = true;
|
layoutState._dirty = true;
|
||||||
builder = newNode.builder;
|
builder = newNode.builder;
|
||||||
token = newNode.token;
|
token = newNode.token;
|
||||||
_offsets = <double>[0.0];
|
layoutState._didReachLastChild = false;
|
||||||
_didReachLastChild = false;
|
layoutState._childOffsets = <double>[0.0];
|
||||||
|
layoutState._invalidIndices = new Set<int>();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void syncRenderObject(BlockViewport old) {
|
void syncRenderObject(BlockViewport old) {
|
||||||
super.syncRenderObject(old);
|
super.syncRenderObject(old);
|
||||||
if (_dirty) {
|
if (layoutState._dirty || !layoutState.isValid) {
|
||||||
root.markNeedsLayout();
|
root.markNeedsLayout();
|
||||||
} else {
|
} else {
|
||||||
if (_currentChildCount > 0) {
|
if (layoutState._visibleChildCount > 0) {
|
||||||
assert(_currentStartIndex >= 0);
|
assert(layoutState.firstVisibleChildIndex >= 0);
|
||||||
assert(builder != null);
|
assert(builder != null);
|
||||||
assert(root != null);
|
assert(root != null);
|
||||||
int lastIndex = _currentStartIndex + _currentChildCount - 1;
|
final int startIndex = layoutState._firstVisibleChildIndex;
|
||||||
for (int index = _currentStartIndex; index <= lastIndex; index += 1) {
|
int lastIndex = startIndex + layoutState._visibleChildCount - 1;
|
||||||
|
for (int index = startIndex; index <= lastIndex; index += 1) {
|
||||||
Widget widget = builder(index);
|
Widget widget = builder(index);
|
||||||
assert(widget != null);
|
assert(widget != null);
|
||||||
assert(widget.key != null);
|
assert(widget.key != null);
|
||||||
_Key key = new _Key.fromWidget(widget);
|
_Key key = new _Key.fromWidget(widget);
|
||||||
Widget oldWidget = _childrenByKey[key];
|
Widget oldWidget = layoutState._childrenByKey[key];
|
||||||
assert(oldWidget != null);
|
assert(oldWidget != null);
|
||||||
assert(oldWidget.root.parent == root);
|
assert(oldWidget.root.parent == root);
|
||||||
widget = syncChild(widget, oldWidget, root.childAfter(oldWidget.root));
|
widget = syncChild(widget, oldWidget, root.childAfter(oldWidget.root));
|
||||||
assert(widget != null);
|
assert(widget != null);
|
||||||
_childrenByKey[key] = widget;
|
layoutState._childrenByKey[key] = widget;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the widget at index, and use its maxIntrinsicHeight to fix up
|
||||||
|
// the offsets from index+1 to endIndex. Return the newWidget.
|
||||||
|
Widget _getWidgetAndRecomputeOffsets(int index, int endIndex, BoxConstraints innerConstraints) {
|
||||||
|
final List<double> offsets = layoutState._childOffsets;
|
||||||
|
// Create the newWidget at index.
|
||||||
|
assert(index >= 0);
|
||||||
|
assert(endIndex > index);
|
||||||
|
assert(endIndex < offsets.length);
|
||||||
|
assert(builder != null);
|
||||||
|
Widget newWidget = builder(index);
|
||||||
|
assert(newWidget != null);
|
||||||
|
assert(newWidget.key != null);
|
||||||
|
final _Key key = new _Key.fromWidget(newWidget);
|
||||||
|
Widget oldWidget = layoutState._childrenByKey[key];
|
||||||
|
newWidget = syncChild(newWidget, oldWidget, _omit);
|
||||||
|
assert(newWidget != null);
|
||||||
|
// Update the offsets based on the newWidget's height.
|
||||||
|
RenderBox widgetRoot = newWidget.root;
|
||||||
|
assert(widgetRoot is RenderBox);
|
||||||
|
double newHeight = widgetRoot.getMaxIntrinsicHeight(innerConstraints);
|
||||||
|
double oldHeight = offsets[index + 1] - offsets[index];
|
||||||
|
double heightDelta = newHeight - oldHeight;
|
||||||
|
for (int i = index + 1; i <= endIndex; i++)
|
||||||
|
offsets[i] += heightDelta;
|
||||||
|
return newWidget;
|
||||||
|
}
|
||||||
|
|
||||||
Widget _getWidget(int index, BoxConstraints innerConstraints) {
|
Widget _getWidget(int index, BoxConstraints innerConstraints) {
|
||||||
LayoutCallbackBuilderHandle handle = enterLayoutCallbackBuilder();
|
final List<double> offsets = layoutState._childOffsets;
|
||||||
try {
|
|
||||||
assert(index >= 0);
|
assert(index >= 0);
|
||||||
Widget widget = builder == null ? null : builder(index);
|
Widget widget = builder == null ? null : builder(index);
|
||||||
if (widget == null)
|
if (widget == null)
|
||||||
return null;
|
return null;
|
||||||
assert(widget.key != null); // items in lists must have keys
|
assert(widget.key != null); // items in lists must have keys
|
||||||
final _Key key = new _Key.fromWidget(widget);
|
final _Key key = new _Key.fromWidget(widget);
|
||||||
Widget oldWidget = _childrenByKey[key];
|
Widget oldWidget = layoutState._childrenByKey[key];
|
||||||
widget = syncChild(widget, oldWidget, _omit);
|
widget = syncChild(widget, oldWidget, _omit);
|
||||||
if (oldWidget != null)
|
if (index >= offsets.length - 1) {
|
||||||
_childrenByKey[key] = widget;
|
assert(index == offsets.length - 1);
|
||||||
if (index >= _offsets.length - 1) {
|
final double widgetStartOffset = offsets[index];
|
||||||
assert(index == _offsets.length - 1);
|
|
||||||
final double widgetStartOffset = _offsets[index];
|
|
||||||
RenderBox widgetRoot = widget.root;
|
RenderBox widgetRoot = widget.root;
|
||||||
assert(widgetRoot is RenderBox);
|
assert(widgetRoot is RenderBox);
|
||||||
final double widgetEndOffset = widgetStartOffset + widgetRoot.getMaxIntrinsicHeight(innerConstraints);
|
final double widgetEndOffset = widgetStartOffset + widgetRoot.getMaxIntrinsicHeight(innerConstraints);
|
||||||
_offsets.add(widgetEndOffset);
|
offsets.add(widgetEndOffset);
|
||||||
}
|
}
|
||||||
return widget;
|
return widget;
|
||||||
} finally {
|
|
||||||
exitLayoutCallbackBuilder(handle);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void layout(BoxConstraints constraints) {
|
void layout(BoxConstraints constraints) {
|
||||||
if (!_dirty)
|
if (!layoutState._dirty && layoutState.isValid)
|
||||||
return;
|
return;
|
||||||
_dirty = false;
|
layoutState._dirty = false;
|
||||||
|
|
||||||
|
LayoutCallbackBuilderHandle handle = enterLayoutCallbackBuilder();
|
||||||
|
try {
|
||||||
|
_doLayout(constraints);
|
||||||
|
} finally {
|
||||||
|
exitLayoutCallbackBuilder(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutState._notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _doLayout(BoxConstraints constraints) {
|
||||||
Map<_Key, Widget> newChildren = new Map<_Key, Widget>();
|
Map<_Key, Widget> newChildren = new Map<_Key, Widget>();
|
||||||
Map<int, Widget> builtChildren = new Map<int, Widget>();
|
Map<int, Widget> builtChildren = new Map<int, Widget>();
|
||||||
|
|
||||||
|
final List<double> offsets = layoutState._childOffsets;
|
||||||
|
final Map<_Key, Widget> childrenByKey = layoutState._childrenByKey;
|
||||||
final double height = root.size.height;
|
final double height = root.size.height;
|
||||||
final double endOffset = startOffset + height;
|
final double endOffset = startOffset + height;
|
||||||
BoxConstraints innerConstraints = new BoxConstraints.tightFor(width: constraints.constrainWidth());
|
BoxConstraints innerConstraints = new BoxConstraints.tightFor(width: constraints.constrainWidth());
|
||||||
|
|
||||||
|
// Before doing the actual layout, fix the offsets for the widgets
|
||||||
|
// whose size or type has changed.
|
||||||
|
if (!layoutState.isValid && offsets.length > 0) {
|
||||||
|
List<int> invalidIndices = layoutState._invalidIndices.toList();
|
||||||
|
invalidIndices.sort();
|
||||||
|
// Ensure all of the offsets after invalidIndices[0] are updated.
|
||||||
|
if (invalidIndices.last < offsets.length - 1)
|
||||||
|
invalidIndices.add(offsets.length - 1);
|
||||||
|
for (int i = 0; i < invalidIndices.length - 1; i += 1) {
|
||||||
|
int index = invalidIndices[i];
|
||||||
|
int endIndex = invalidIndices[i + 1];
|
||||||
|
Widget widget = _getWidgetAndRecomputeOffsets(index, endIndex, innerConstraints);
|
||||||
|
_Key widgetKey = new _Key.fromWidget(widget);
|
||||||
|
bool isVisible = offsets[index] < endOffset && offsets[index + 1] >= startOffset;
|
||||||
|
if (isVisible) {
|
||||||
|
newChildren[widgetKey] = widget;
|
||||||
|
builtChildren[index] = widget;
|
||||||
|
} else {
|
||||||
|
childrenByKey.remove(widgetKey);
|
||||||
|
syncChild(null, widget, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layoutState._invalidIndices.clear();
|
||||||
|
|
||||||
int startIndex;
|
int startIndex;
|
||||||
bool haveChildren;
|
bool haveChildren;
|
||||||
if (startOffset <= 0.0) {
|
if (startOffset <= 0.0) {
|
||||||
startIndex = 0;
|
startIndex = 0;
|
||||||
if (_offsets.length > 1) {
|
if (offsets.length > 1) {
|
||||||
haveChildren = true;
|
haveChildren = true;
|
||||||
} else {
|
} else {
|
||||||
Widget widget = _getWidget(startIndex, innerConstraints);
|
Widget widget = _getWidget(startIndex, innerConstraints);
|
||||||
@ -208,41 +309,41 @@ class BlockViewport extends RenderObjectWrapper {
|
|||||||
haveChildren = true;
|
haveChildren = true;
|
||||||
} else {
|
} else {
|
||||||
haveChildren = false;
|
haveChildren = false;
|
||||||
_didReachLastChild = true;
|
layoutState._didReachLastChild = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
startIndex = _findIndexForOffsetBeforeOrAt(startOffset);
|
startIndex = _findIndexForOffsetBeforeOrAt(startOffset);
|
||||||
if (startIndex == _offsets.length - 1) {
|
if (startIndex == offsets.length - 1) {
|
||||||
// We don't have an offset on the list that is beyond the start offset.
|
// We don't have an offset on the list that is beyond the start offset.
|
||||||
assert(_offsets.last <= startOffset);
|
assert(offsets.last <= startOffset);
|
||||||
// Fill the list until this isn't true or until we know that the
|
// Fill the list until this isn't true or until we know that the
|
||||||
// list is complete (and thus we are overscrolled).
|
// list is complete (and thus we are overscrolled).
|
||||||
while (true) {
|
while (true) {
|
||||||
Widget widget = _getWidget(startIndex, innerConstraints);
|
Widget widget = _getWidget(startIndex, innerConstraints);
|
||||||
if (widget == null) {
|
if (widget == null) {
|
||||||
_didReachLastChild = true;
|
layoutState._didReachLastChild = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_Key widgetKey = new _Key.fromWidget(widget);
|
_Key widgetKey = new _Key.fromWidget(widget);
|
||||||
if (_offsets.last > startOffset) {
|
if (offsets.last > startOffset) {
|
||||||
newChildren[widgetKey] = widget;
|
newChildren[widgetKey] = widget;
|
||||||
builtChildren[startIndex] = widget;
|
builtChildren[startIndex] = widget;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!_childrenByKey.containsKey(widgetKey)) {
|
if (!childrenByKey.containsKey(widgetKey)) {
|
||||||
// we don't actually need this one, release it
|
// we don't actually need this one, release it
|
||||||
syncChild(null, widget, null);
|
syncChild(null, widget, null);
|
||||||
} // else we'll get rid of it later, when we remove old children
|
} // else we'll get rid of it later, when we remove old children
|
||||||
startIndex += 1;
|
startIndex += 1;
|
||||||
assert(startIndex == _offsets.length - 1);
|
assert(startIndex == offsets.length - 1);
|
||||||
}
|
}
|
||||||
if (_offsets.last > startOffset) {
|
if (offsets.last > startOffset) {
|
||||||
// If we're here, we have at least one child, so our list has
|
// If we're here, we have at least one child, so our list has
|
||||||
// at least two offsets, the top of the child and the bottom
|
// at least two offsets, the top of the child and the bottom
|
||||||
// of the child.
|
// of the child.
|
||||||
assert(_offsets.length >= 2);
|
assert(offsets.length >= 2);
|
||||||
assert(startIndex == _offsets.length - 2);
|
assert(startIndex == offsets.length - 2);
|
||||||
haveChildren = true;
|
haveChildren = true;
|
||||||
} else {
|
} else {
|
||||||
// If we're here, there are no children to show.
|
// If we're here, there are no children to show.
|
||||||
@ -253,20 +354,20 @@ class BlockViewport extends RenderObjectWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert(haveChildren != null);
|
assert(haveChildren != null);
|
||||||
assert(haveChildren || _didReachLastChild);
|
assert(haveChildren || layoutState._didReachLastChild);
|
||||||
|
|
||||||
assert(startIndex >= 0);
|
assert(startIndex >= 0);
|
||||||
assert(startIndex < _offsets.length);
|
assert(startIndex < offsets.length);
|
||||||
|
|
||||||
int index = startIndex;
|
int index = startIndex;
|
||||||
if (haveChildren) {
|
if (haveChildren) {
|
||||||
// Build all the widgets we need.
|
// Build all the widgets we need.
|
||||||
root.startOffset = _offsets[index] - startOffset;
|
root.startOffset = offsets[index] - startOffset;
|
||||||
while (_offsets[index] < endOffset) {
|
while (offsets[index] < endOffset) {
|
||||||
if (!builtChildren.containsKey(index)) {
|
if (!builtChildren.containsKey(index)) {
|
||||||
Widget widget = _getWidget(index, innerConstraints);
|
Widget widget = _getWidget(index, innerConstraints);
|
||||||
if (widget == null) {
|
if (widget == null) {
|
||||||
_didReachLastChild = true;
|
layoutState._didReachLastChild = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
newChildren[new _Key.fromWidget(widget)] = widget;
|
newChildren[new _Key.fromWidget(widget)] = widget;
|
||||||
@ -278,9 +379,9 @@ class BlockViewport extends RenderObjectWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove any old children.
|
// Remove any old children.
|
||||||
for (_Key oldChildKey in _childrenByKey.keys) {
|
for (_Key oldChildKey in childrenByKey.keys) {
|
||||||
if (!newChildren.containsKey(oldChildKey))
|
if (!newChildren.containsKey(oldChildKey))
|
||||||
syncChild(null, _childrenByKey[oldChildKey], null); // calls detachChildRoot()
|
syncChild(null, childrenByKey[oldChildKey], null); // calls detachChildRoot()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (haveChildren) {
|
if (haveChildren) {
|
||||||
@ -302,18 +403,9 @@ class BlockViewport extends RenderObjectWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_childrenByKey = newChildren;
|
layoutState._childrenByKey = newChildren;
|
||||||
_currentStartIndex = startIndex;
|
layoutState._firstVisibleChildIndex = startIndex;
|
||||||
_currentChildCount = _childrenByKey.length;
|
layoutState._visibleChildCount = newChildren.length;
|
||||||
|
|
||||||
if (onLayoutChanged != null) {
|
|
||||||
onLayoutChanged(
|
|
||||||
_currentStartIndex,
|
|
||||||
_currentChildCount,
|
|
||||||
new UnmodifiableListView<double>(_offsets),
|
|
||||||
_didReachLastChild
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
import 'package:sky/widgets/basic.dart';
|
import 'package:sky/widgets/basic.dart';
|
||||||
import 'package:sky/widgets/material.dart';
|
import 'package:sky/widgets/material.dart';
|
||||||
|
|
||||||
|
const EdgeDims kCardMargins = const EdgeDims.all(4.0);
|
||||||
|
|
||||||
/// A material design card
|
/// A material design card
|
||||||
///
|
///
|
||||||
/// <https://www.google.com/design/spec/components/cards.html>
|
/// <https://www.google.com/design/spec/components/cards.html>
|
||||||
@ -16,7 +18,7 @@ class Card extends Component {
|
|||||||
|
|
||||||
Widget build() {
|
Widget build() {
|
||||||
return new Container(
|
return new Container(
|
||||||
margin: const EdgeDims.all(4.0),
|
margin: kCardMargins,
|
||||||
child: new Material(
|
child: new Material(
|
||||||
color: color,
|
color: color,
|
||||||
type: MaterialType.card,
|
type: MaterialType.card,
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:collection';
|
|
||||||
|
|
||||||
import 'package:sky/animation/scroll_behavior.dart';
|
import 'package:sky/animation/scroll_behavior.dart';
|
||||||
import 'package:sky/widgets/basic.dart';
|
import 'package:sky/widgets/basic.dart';
|
||||||
import 'package:sky/widgets/block_viewport.dart';
|
import 'package:sky/widgets/block_viewport.dart';
|
||||||
@ -14,18 +12,39 @@ class VariableHeightScrollable extends Scrollable {
|
|||||||
VariableHeightScrollable({
|
VariableHeightScrollable({
|
||||||
String key,
|
String key,
|
||||||
this.builder,
|
this.builder,
|
||||||
this.token
|
this.token,
|
||||||
|
this.layoutState
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
IndexedBuilder builder;
|
IndexedBuilder builder;
|
||||||
Object token;
|
Object token;
|
||||||
|
BlockViewportLayoutState layoutState;
|
||||||
|
|
||||||
|
// When the token changes the scrollable's contents may have
|
||||||
|
// changed. Remember as much so that after the new contents
|
||||||
|
// have been laid out we can adjust the scrollOffset so that
|
||||||
|
// the last page of content is still visible.
|
||||||
bool _contentsChanged = true;
|
bool _contentsChanged = true;
|
||||||
|
|
||||||
|
void initState() {
|
||||||
|
assert(layoutState != null);
|
||||||
|
layoutState.removeListener(_handleLayoutChanged);
|
||||||
|
layoutState.addListener(_handleLayoutChanged);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
void syncFields(VariableHeightScrollable source) {
|
void syncFields(VariableHeightScrollable source) {
|
||||||
builder = source.builder;
|
builder = source.builder;
|
||||||
if (token != source.token)
|
if (token != source.token)
|
||||||
_contentsChanged = true;
|
_contentsChanged = true;
|
||||||
token = source.token;
|
token = source.token;
|
||||||
|
if (layoutState != source.layoutState) {
|
||||||
|
// Warning: this is unlikely to be what you intended.
|
||||||
|
assert(source.layoutState != null);
|
||||||
|
layoutState == source.layoutState;
|
||||||
|
layoutState.removeListener(_handleLayoutChanged);
|
||||||
|
layoutState.addListener(_handleLayoutChanged);
|
||||||
|
}
|
||||||
super.syncFields(source);
|
super.syncFields(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,15 +55,9 @@ class VariableHeightScrollable extends Scrollable {
|
|||||||
scrollBehavior.containerSize = newSize.height;
|
scrollBehavior.containerSize = newSize.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleLayoutChanged(
|
void _handleLayoutChanged() {
|
||||||
int firstVisibleChildIndex,
|
if (layoutState.didReachLastChild) {
|
||||||
int visibleChildCount,
|
scrollBehavior.contentsSize = layoutState.contentsSize;
|
||||||
UnmodifiableListView<double> childOffsets,
|
|
||||||
bool didReachLastChild
|
|
||||||
) {
|
|
||||||
assert(childOffsets.length > 0);
|
|
||||||
if (didReachLastChild) {
|
|
||||||
scrollBehavior.contentsSize = childOffsets.last;
|
|
||||||
if (_contentsChanged && scrollOffset > scrollBehavior.maxScrollOffset) {
|
if (_contentsChanged && scrollOffset > scrollBehavior.maxScrollOffset) {
|
||||||
_contentsChanged = false;
|
_contentsChanged = false;
|
||||||
settleScrollOffset();
|
settleScrollOffset();
|
||||||
@ -59,7 +72,7 @@ class VariableHeightScrollable extends Scrollable {
|
|||||||
callback: _handleSizeChanged,
|
callback: _handleSizeChanged,
|
||||||
child: new BlockViewport(
|
child: new BlockViewport(
|
||||||
builder: builder,
|
builder: builder,
|
||||||
onLayoutChanged: _handleLayoutChanged,
|
layoutState: layoutState,
|
||||||
startOffset: scrollOffset,
|
startOffset: scrollOffset,
|
||||||
token: token
|
token: token
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user