flutter_path_layout 0.1.2
flutter_path_layout: ^0.1.2 copied to clipboard
Reusable Flutter layouts for learning, activity, roadmap, and progression paths.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_path_layout/flutter_path_layout.dart';
void main() {
runApp(const PathLayoutDemoApp());
}
class PathLayoutDemoApp extends StatelessWidget {
const PathLayoutDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Path Layout Demo',
theme: ThemeData(useMaterial3: true),
home: const PathLayoutDemoScreen(),
);
}
}
class PathLayoutDemoScreen extends StatelessWidget {
const PathLayoutDemoScreen({super.key});
static const demoItems = <DemoItem>[
DemoItem(1, 'Lesson', Size.square(64)),
DemoItem(2, 'Practice', Size.square(64)),
DemoItem(3, 'Reward', Size(108, 72)),
DemoItem(4, 'Character', Size(132, 112)),
DemoItem(5, 'Review', Size.square(76)),
DemoItem(6, 'Lesson', Size.square(64)),
DemoItem(7, 'Boss', Size(96, 96)),
];
static const keyedProgressItems = <DemoItem>[
DemoItem(5, 'Five', Size.square(64)),
DemoItem(4, 'Four', Size.square(76)),
DemoItem(3, 'Three', Size.square(64)),
DemoItem(2, 'Two', Size.square(76)),
DemoItem(1, 'One', Size.square(64)),
];
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 8,
child: Scaffold(
appBar: AppBar(
title: const Text('Path Layout Demo'),
bottom: const TabBar(
isScrollable: true,
tabs: [
Tab(text: 'Vertical wave'),
Tab(text: 'Horizontal wave'),
Tab(text: 'Zigzag'),
Tab(text: 'Straight'),
Tab(text: 'Sizes'),
Tab(text: 'Custom'),
Tab(text: 'Connectors'),
Tab(text: 'Progress'),
],
),
),
body: TabBarView(
children: [
_PathDemo(
items: demoItems,
shape: PathShape.wave,
connectorStyle: const PathConnectorStyle.solid(curved: true),
),
_PathDemo(
items: demoItems,
direction: Axis.horizontal,
shape: PathShape.wave,
connectorStyle: const PathConnectorStyle.solid(curved: true),
),
_PathDemo(
items: demoItems,
shape: PathShape.zigzag,
amplitude: 0.85,
connectorStyle: const PathConnectorStyle.dashed(),
),
_PathDemo(items: demoItems, shape: PathShape.straight),
_PathDemo(items: demoItems, shape: PathShape.alternating),
_PathDemo(
items: demoItems,
strategy: CustomPathStrategy(
positionBuilder: (context) {
final middle = (context.itemCount - 1) / 2;
if (middle == 0) {
return 0;
}
final distance = (context.index - middle).abs() / middle;
return math.cos(distance * math.pi) * 0.65;
},
),
connectorStyle: const PathConnectorStyle.solid(curved: true),
),
ListView(
padding: const EdgeInsets.all(16),
children: const [
SizedBox(
height: 360,
child: _PathDemo(
items: demoItems,
connectorStyle: PathConnectorStyle.none,
),
),
SizedBox(
height: 360,
child: _PathDemo(
items: demoItems,
connectorStyle: PathConnectorStyle.solid(),
),
),
SizedBox(
height: 360,
child: _PathDemo(
items: demoItems,
connectorStyle: PathConnectorStyle.dashed(curved: true),
),
),
],
),
_PathDemo(
items: keyedProgressItems,
shape: PathShape.wave,
isItemCompleted: (item, index) => item.id <= 3,
isItemCurrent: (item, index) => item.id == 4,
connectorStyleBuilder: (context, segment) {
if (segment.isCompleted) {
return const PathConnectorStyle.solid(
color: Colors.green,
width: 8,
curved: true,
);
}
return const PathConnectorStyle.solid(
color: Colors.black26,
width: 5,
curved: true,
);
},
),
],
),
),
);
}
}
class _PathDemo extends StatelessWidget {
const _PathDemo({
required this.items,
this.direction = Axis.vertical,
this.shape = PathShape.wave,
this.strategy,
this.amplitude = 0.45,
this.connectorStyle = PathConnectorStyle.none,
this.isItemCompleted,
this.isItemCurrent,
this.connectorStyleBuilder,
});
final List<DemoItem> items;
final Axis direction;
final PathShape shape;
final PathLayoutStrategy? strategy;
final double amplitude;
final PathConnectorStyle connectorStyle;
final PathItemStateResolver<DemoItem>? isItemCompleted;
final PathItemStateResolver<DemoItem>? isItemCurrent;
final PathConnectorStyleBuilder<DemoItem>? connectorStyleBuilder;
@override
Widget build(BuildContext context) {
return FlutterPathLayout<DemoItem>(
items: items,
direction: direction,
shape: shape,
strategy: strategy,
amplitude: amplitude,
itemSpacing: 124,
padding: const EdgeInsets.all(32),
physics: const BouncingScrollPhysics(),
connectorStyle: connectorStyle,
isItemCompleted: isItemCompleted,
isItemCurrent: isItemCurrent,
connectorStyleBuilder: connectorStyleBuilder,
contextItemBuilder: (context, item, pathContext) {
return DemoNode(
item: item,
index: pathContext.index,
isCompleted: pathContext.isCompleted,
isCurrent: pathContext.isCurrent,
);
},
);
}
}
class DemoNode extends StatelessWidget {
const DemoNode({
super.key,
required this.item,
required this.index,
this.isCompleted = false,
this.isCurrent = false,
});
final DemoItem item;
final int index;
final bool isCompleted;
final bool isCurrent;
@override
Widget build(BuildContext context) {
final colors = [
Colors.blueGrey,
Colors.teal,
Colors.deepPurple,
Colors.orange,
Colors.indigo,
];
final color = isCompleted ? Colors.green : colors[index % colors.length];
final borderWidth = isCurrent ? 5.0 : 2.0;
return Semantics(
label: '${item.label} ${index + 1}',
button: true,
child: Stack(
clipBehavior: Clip.none,
children: [
DecoratedBox(
decoration: BoxDecoration(
color: isCompleted ? Colors.green.shade50 : color.shade100,
border: Border.all(color: color.shade600, width: borderWidth),
borderRadius: BorderRadius.circular(18),
),
child: SizedBox.fromSize(
size: item.size,
child: Center(
child: Text(
item.label,
textAlign: TextAlign.center,
style: TextStyle(
color: color.shade900,
fontWeight: FontWeight.w700,
),
),
),
),
),
if (isCurrent)
Positioned(
right: -8,
top: -8,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.amber,
border: Border.all(color: Colors.white, width: 2),
shape: BoxShape.circle,
),
child: const SizedBox.square(
dimension: 24,
child: Icon(Icons.location_on, size: 14),
),
),
),
],
),
);
}
}
class DemoItem {
const DemoItem(this.id, this.label, this.size);
final int id;
final String label;
final Size size;
}