blob: 51f4ce67d29498d8c1fa6f0c6ac5c2f2204f54ba [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/animation.dart';
import "../todo_model.dart";
const Duration _todoSnapDuration = const Duration(milliseconds: 200);
typedef void TodoHandleDelete(TodoModel);
enum TodoStatus { idle, completing, deleting, }
class TodoItem extends StatefulComponent {
TodoItem({Key key, this.todo, this.onDelete}) : super(key: key) {
assert(todo != null);
assert(onDelete != null);
}
final TodoModel todo;
final TodoHandleDelete onDelete;
TodoItemState createState() => new TodoItemState();
}
// TODO(jasoncampbell): Create a throttle animation so that dragging
// appears to slow down as the item gets closer to the threshold.
class TodoItemState extends State<TodoItem> {
HorizontalDragGestureRecognizer drag;
ValuePerformance<double> snapPerformance;
TodoItemState() {
drag = new HorizontalDragGestureRecognizer(
router: Gesturer.instance.pointerRouter,
gestureArena: Gesturer.instance.gestureArena)
..onStart = handleDragStart
..onUpdate = handleDragUpdate
..onEnd = handleDragEnd;
snapPerformance = new ValuePerformance<double>(
variable: snapValue, duration: _todoSnapDuration)
..addStatusListener(handleSnapAnimationStatus);
}
TodoModel todo;
TodoStatus status;
Size size = new Size(ui.window.size.width, 0.0);
double position = 0.0;
double lastDelta;
final double dragThreshold = ui.window.size.width / 2 * 0.75;
final AnimatedValue<double> snapValue =
new AnimatedValue<double>(1.0, end: 0.0, curve: Curves.easeOut);
/// Override `initState()`.
@override
void initState() {
super.initState();
todo = config.todo;
}
void dispose() {
snapPerformance?.stop();
super.dispose();
}
void handleSnapAnimationStatus(PerformanceStatus update) {
if (update == PerformanceStatus.completed) {
setState(() {
snapPerformance.progress = 0.0;
position = 0.0;
// Make sure the right thing happens after the snap animation.
switch (status) {
case TodoStatus.completing:
todo.completed = true;
status = TodoStatus.idle;
break;
case TodoStatus.deleting:
config.onDelete(todo);
break;
default:
status = TodoStatus.idle;
}
});
}
}
void handlePointerDown(PointerDownEvent event) {
drag.addPointer(event);
}
void handleDragStart(Point globalPosition) {
position = 0.0;
}
void handleDragUpdate(double delta) {
double update = position + delta;
// NOTE: My eyes might be playing tricks on me but I think this prevents
// some jitter while dragging a Todo horizonatlly.
if (position != update) {
bool pastThreshold = update.abs() > dragThreshold;
// Prevent the drag from moving past the treshold.
if (!pastThreshold) {
setState(() {
lastDelta = delta;
position = update;
double statusThreshold = dragThreshold / 2;
// Update status based on the current position.
if (position.abs() > statusThreshold) {
if (position.isNegative) {
// Dragging to the left (revealing the delete icon).
status = TodoStatus.deleting;
} else {
// Dragging to the right (revealing the complete icon).
status = TodoStatus.completing;
}
} else {
// When dragging back and forth between the original position and
// the status threshold return the item to the idle state.
status = TodoStatus.idle;
}
});
}
}
}
void handleDragEnd(Offset velocity) {
snapPerformance.play();
}
void handleResize(Size update) {
setState(() {
size = update;
});
}
Widget build(BuildContext context) {
Widget overlay = new BuilderTransition(
variables: <AnimatedValue<double>>[snapPerformance.variable],
performance: snapPerformance.view, builder: (BuildContext context) {
double left = snapValue.value.clamp(0.0, 1.0) * position;
return new Positioned(
width: size.width,
left: left,
child: new TodoItemBody(todo: todo, status: status));
});
List<Widget> children = [
// NOTE: This instance of TodoItemBody will never be seen, it is used
// entirely for sizing purposes and appears behind the action icons
// below. The visible instance is inside of a positioned element on top
// of the stack and as a result will lack a hight constraint.
new TodoItemBody(todo: todo, onResize: handleResize),
];
Widget background = new Container(
decoration: new BoxDecoration(
backgroundColor: Theme.of(context).canvasColor,
border: new Border(
bottom: new BorderSide(
color: Theme.of(context).dividerColor, width: 1.0))),
height: size.height,
child: new Row(<Widget>[
new Flexible(
child: new Container(
child: new Align(
child: new Icon(
icon: "action/check_circle",
color: IconThemeColor.white),
alignment: const FractionalOffset(0.0, 0.5)),
decoration: new BoxDecoration(
backgroundColor: Colors.greenAccent[100]),
padding: const EdgeDims.all(24.0))),
new Flexible(
child: new Container(
child: new Align(
child: new Icon(
icon: "action/delete", color: IconThemeColor.white),
alignment: const FractionalOffset(1.0, 0.5)),
decoration: new BoxDecoration(
backgroundColor: Colors.redAccent[100]),
padding: const EdgeDims.all(24.0)))
]));
children.add(background);
children.add(overlay);
return new Listener(
onPointerDown: handlePointerDown,
behavior: HitTestBehavior.translucent,
child: new Stack(children));
}
}
typedef void TodoHandleResize(Size);
class TodoItemBody extends StatelessComponent {
TodoItemBody({Key key, this.todo, this.status, this.onResize});
final TodoModel todo;
final TodoHandleResize onResize;
final TodoStatus status;
void maybeCallResize(Size size) {
if (onResize != null) {
onResize(size);
}
}
Widget build(BuildContext context) {
Color backgroundColor;
switch (status) {
case TodoStatus.completing:
backgroundColor = Colors.greenAccent[100];
break;
case TodoStatus.deleting:
backgroundColor = Colors.redAccent[100];
break;
default:
backgroundColor = Theme.of(context).canvasColor;
}
String message = todo.title;
if (todo.completed) {
message = "COMPLETED: ${todo.title}";
}
return new SizeObserver(
onSizeChanged: maybeCallResize,
child: new Container(
decoration: new BoxDecoration(backgroundColor: backgroundColor),
child: new Padding(
padding: const EdgeDims.all(24.0),
child: new Text(message))));
}
}