Skip to content

Instantly share code, notes, and snippets.

@abhaysood
Created April 27, 2021 07:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save abhaysood/748a0aea90ccb9ff423df19466ffebff to your computer and use it in GitHub Desktop.
Save abhaysood/748a0aea90ccb9ff423df19466ffebff to your computer and use it in GitHub Desktop.
Drag and drop to delete from list
import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:ui' show lerpDouble;
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: SafeArea(
child: DragDemo(),
),
),
);
}
}
class DragDemo extends StatefulWidget {
@override
_DragDemoState createState() => _DragDemoState();
}
class _DragDemoState extends State<DragDemo> {
final _listGlobalKey = GlobalKey<AnimatedListState>();
bool _isDragging = false;
final _tasks = TasksStore.tasks;
Widget _buildList() {
return Expanded(
child: AnimatedList(
key: _listGlobalKey,
initialItemCount: _tasks.length,
itemBuilder: (_, index, animation) => _buildTaskItem(_tasks[index]),
padding: EdgeInsets.symmetric(horizontal: 16),
),
);
}
Widget _buildHeader() {
return DragTarget<Task>(
onAccept: (data) {
_listGlobalKey.currentState!.removeItem(
_tasks.indexOf(data),
(_, Animation<double> animation) {
return SizeTransition(
sizeFactor: CurvedAnimation(
parent: animation,
curve: Interval(0, 1),
),
child: SizedBox(height: 80),
);
},
);
setState(() {
_tasks.remove(data);
});
},
builder: (_, candidateData, rejectedData) {
return AnimatedSwitcher(
duration: Duration(milliseconds: 100),
child: _isDragging
? _buildDropTargetHeader(candidateData)
: _buildListHeading(),
);
},
);
}
Widget _buildDropTargetHeader(List<Task?> candidateData) {
return TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 300),
tween: Tween<double>(begin: 0.1, end: 1),
builder: (_, value, child) {
return ClipPath(
clipper: CircularRevealClipper(
fraction: value,
),
child: child,
);
},
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
alignment: Alignment.center,
height: 80,
color: candidateData.isNotEmpty
? Color(0xFFFFC4BB)
: Color(0xFFFFC4BB).withOpacity(0.5),
child: Text(
"Drop a task here to delete",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
color: Color(0xFF2F4858),
),
),
),
);
}
Container _buildListHeading() {
return Container(
alignment: Alignment.center,
height: 80,
child: Text(
"In Progress",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 32,
),
),
);
}
Widget _buildTaskItem(Task task) {
return DraggableTaskListItem(
onDragStarted: () {
setState(() {
_isDragging = true;
});
},
onDragEnd: (details) {
setState(() {
_isDragging = false;
});
},
task: task,
);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildHeader(),
_buildList(),
],
);
}
}
class DraggableTaskListItem extends StatelessWidget {
final VoidCallback onDragStarted;
final DragEndCallback onDragEnd;
final Task task;
DraggableTaskListItem({
Key? key,
required this.onDragStarted,
required this.onDragEnd,
required this.task,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LongPressDraggable<Task>(
data: task,
dragAnchor: DragAnchor.child,
childWhenDragging: Container(
height: 80,
color: Colors.grey.shade200,
),
onDragStarted: onDragStarted,
onDragEnd: onDragEnd,
feedback: LayoutBuilder(
builder: (context, constraints) {
return Transform.rotate(
alignment: Alignment.center,
angle: -pi / 128,
child: Container(
width: MediaQuery.of(context).size.width - 32,
height: 80,
child: Card(
child: ListTile(
tileColor: Color(0xFFFFC4BB).withOpacity(0.5),
title: Text(task.title),
subtitle: Text(
task.description,
maxLines: 1,
),
),
),
),
);
},
),
child: Container(
height: 80,
child: Card(
child: ListTile(
title: Text(task.title),
subtitle: Text(task.description),
),
),
),
);
}
}
class Task {
final String id;
final String title;
final String description;
Task({
required this.id,
required this.title,
required this.description,
});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Task && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
}
class TasksStore {
TasksStore._();
static List<Task> tasks = List.generate(100, (index) {
return Task(
id: index.toString(),
title: "Title $index",
description: "Content",
);
});
}
@immutable
class CircularRevealClipper extends CustomClipper<Path> {
final double fraction;
final Alignment? centerAlignment;
final Offset? centerOffset;
final double? minRadius;
final double? maxRadius;
CircularRevealClipper({
required this.fraction,
this.centerAlignment,
this.centerOffset,
this.minRadius,
this.maxRadius,
});
@override
Path getClip(Size size) {
final Offset center = this.centerAlignment?.alongSize(size) ??
this.centerOffset ??
Offset(size.width / 2, size.height / 2);
final minRadius = this.minRadius ?? 0;
final maxRadius = this.maxRadius ?? calcMaxRadius(size, center);
return Path()
..addOval(
Rect.fromCircle(
center: center,
radius: lerpDouble(minRadius, maxRadius, fraction)!,
),
);
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => true;
static double calcMaxRadius(Size size, Offset center) {
final w = max(center.dx, size.width - center.dx);
final h = max(center.dy, size.height - center.dy);
return sqrt(w * w + h * h);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment