scribble_with_timestamp 0.10.0+1 scribble_with_timestamp: ^0.10.0+1 copied to clipboard
Scribble is a lightweight library for freehand drawing in Flutter supporting pressure, variable line width and more along with these features I have added timestamp to each point drawn on the canvas s [...]
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:scribble_with_timestamp/scribble.dart';
import 'package:value_notifier_tools/value_notifier_tools.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Scribble',
theme: ThemeData.from(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple)),
home: const HomePage(title: 'Scribble'),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key, required this.title});
final String title;
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late ScribbleNotifier notifier;
bool _simulatePressure = true;
@override
void initState() {
notifier = ScribbleNotifier();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: AppBar(
title: Text(widget.title),
actions: _buildActions(context),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 64),
child: Column(
children: [
Expanded(
child: Card(
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
color: Colors.white,
surfaceTintColor: Colors.white,
child: Scribble(
notifier: notifier,
drawPen: true,
simulatePressure: _simulatePressure,
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
SizedBox(
width: double.infinity,
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
alignment: WrapAlignment.spaceBetween,
spacing: 16,
runSpacing: 16,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildColorToolbar(context),
const VerticalDivider(width: 32),
_buildStrokeToolbar(context),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Switch.adaptive(
value: _simulatePressure,
onChanged: (v) =>
setState(() => _simulatePressure = v),
),
const SizedBox(width: 8),
const Text("Simulate Pressure")
],
),
_buildPointerModeSwitcher(context),
],
),
),
const Divider(
height: 32,
),
Row(
children: [
ValueListenableBuilder(
valueListenable: notifier.select((value) => value.lines
.expand((element) => element.points)
.length),
builder: (context, value, child) =>
Text("Simplification:\n($value points)"),
),
Expanded(
child: ValueListenableBuilder(
valueListenable: notifier
.select((value) => value.simplificationTolerance),
builder: (context, value, child) => Slider(
value: value,
max: 10,
onChanged: notifier.setSimplificationTolerance,
label: "${value.toStringAsFixed(2)} px",
divisions: 100,
),
),
),
TextButton(
onPressed: notifier.simplify,
child: const Text("Simplify"),
),
],
),
],
),
)
],
),
),
);
}
List<Widget> _buildActions(context) {
return [
ValueListenableBuilder(
valueListenable: notifier,
builder: (context, value, child) => IconButton(
icon: child as Icon,
tooltip: "Undo",
onPressed: notifier.canUndo ? notifier.undo : null,
),
child: const Icon(Icons.undo),
),
ValueListenableBuilder(
valueListenable: notifier,
builder: (context, value, child) => IconButton(
icon: child as Icon,
tooltip: "Redo",
onPressed: notifier.canRedo ? notifier.redo : null,
),
child: const Icon(Icons.redo),
),
IconButton(
icon: const Icon(Icons.clear),
tooltip: "Clear",
onPressed: notifier.clear,
),
IconButton(
icon: const Icon(Icons.image),
tooltip: "Show PNG Image",
onPressed: () => _showImage(context),
),
IconButton(
icon: const Icon(Icons.data_object),
tooltip: "Show JSON",
onPressed: () => _showJson(context),
),
];
}
void _showImage(BuildContext context) async {
final image = notifier.renderImage();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Generated Image"),
content: SizedBox.expand(
child: FutureBuilder(
future: image,
builder: (context, snapshot) => snapshot.hasData
? Image.memory(snapshot.data!.buffer.asUint8List())
: const Center(child: CircularProgressIndicator()),
),
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text("Close"),
)
],
),
);
}
void _showJson(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Sketch as JSON"),
content: SizedBox.expand(
child: SelectableText(
jsonEncode(notifier.currentSketch.toJson()),
autofocus: true,
),
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text("Close"),
)
],
),
);
}
Widget _buildStrokeToolbar(BuildContext context) {
return ValueListenableBuilder<ScribbleState>(
valueListenable: notifier,
builder: (context, state, _) => Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
for (final w in notifier.widths)
_buildStrokeButton(
context,
strokeWidth: w,
state: state,
),
],
),
);
}
Widget _buildStrokeButton(
BuildContext context, {
required double strokeWidth,
required ScribbleState state,
}) {
final selected = state.selectedWidth == strokeWidth;
return Padding(
padding: const EdgeInsets.all(4),
child: Material(
elevation: selected ? 4 : 0,
shape: const CircleBorder(),
child: InkWell(
onTap: () => notifier.setStrokeWidth(strokeWidth),
customBorder: const CircleBorder(),
child: AnimatedContainer(
duration: kThemeAnimationDuration,
width: strokeWidth * 2,
height: strokeWidth * 2,
decoration: BoxDecoration(
color: state.map(
drawing: (s) => Color(s.selectedColor),
erasing: (_) => Colors.transparent,
),
border: state.map(
drawing: (_) => null,
erasing: (_) => Border.all(width: 1),
),
borderRadius: BorderRadius.circular(50.0)),
),
),
),
);
}
Widget _buildColorToolbar(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
_buildColorButton(context, color: Colors.black),
_buildColorButton(context, color: Colors.red),
_buildColorButton(context, color: Colors.green),
_buildColorButton(context, color: Colors.blue),
_buildColorButton(context, color: Colors.yellow),
_buildEraserButton(context),
],
);
}
Widget _buildPointerModeSwitcher(BuildContext context) {
return ValueListenableBuilder(
valueListenable: notifier.select(
(value) => value.allowedPointersMode,
),
builder: (context, value, child) {
return SegmentedButton<ScribblePointerMode>(
multiSelectionEnabled: false,
emptySelectionAllowed: false,
onSelectionChanged: (v) => notifier.setAllowedPointersMode(v.first),
segments: const [
ButtonSegment(
value: ScribblePointerMode.all,
icon: Icon(Icons.touch_app),
label: Text("All pointers"),
),
ButtonSegment(
value: ScribblePointerMode.penOnly,
icon: Icon(Icons.draw),
label: Text("Pen only"),
),
],
selected: {value},
);
});
}
Widget _buildEraserButton(BuildContext context) {
return ValueListenableBuilder(
valueListenable: notifier.select((value) => value is Erasing),
builder: (context, value, child) => ColorButton(
color: Colors.transparent,
outlineColor: Colors.black,
isActive: value,
onPressed: () => notifier.setEraser(),
child: const Icon(Icons.cleaning_services),
),
);
}
Widget _buildColorButton(
BuildContext context, {
required Color color,
}) {
return ValueListenableBuilder(
valueListenable: notifier.select(
(value) => value is Drawing && value.selectedColor == color.value),
builder: (context, value, child) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ColorButton(
color: color,
isActive: value,
onPressed: () => notifier.setColor(color),
),
),
);
}
}
class ColorButton extends StatelessWidget {
const ColorButton({
required this.color,
required this.isActive,
required this.onPressed,
this.outlineColor,
this.child,
super.key,
});
final Color color;
final Color? outlineColor;
final bool isActive;
final VoidCallback onPressed;
final Icon? child;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: kThemeAnimationDuration,
decoration: ShapeDecoration(
shape: CircleBorder(
side: BorderSide(
color: switch (isActive) {
true => outlineColor ?? color,
false => Colors.transparent,
},
width: 2,
),
),
),
child: IconButton(
style: FilledButton.styleFrom(
backgroundColor: color,
shape: const CircleBorder(),
side: isActive
? const BorderSide(color: Colors.white, width: 2)
: const BorderSide(color: Colors.transparent),
),
onPressed: onPressed,
icon: child ?? const SizedBox(),
),
);
}
}