I want to make an interactive hall plan in Flutter, but I can't figure out how to implement it correctly.
Key points:
- the plan should scale without loss of quality.
- the view of the place should change slightly when you click
The first option: is to make it as a Canvas of a large size, and put it in the InteractiveViewer, but I do not know if it is possible to change the already drawn parts on tap?
The second option: is to make each place as an SVG with onTap event, and put it all in a Stack widget. And then again in InteractiveViewer. But there will be about 500 seats on the plan, and I'm not sure if the phone can handle scaling such a scene?
Maybe there are other solutions? I will be very grateful for the advice, I still have very little experience in Flutter.
CodePudding user response:
This can be achieved with InteractiveViewer & GridView:
for any shape of place, you can draw it using 2d array, like
List<List<int>> triangleHall = [
[0, 0, 1, 0, 0],
[0, 1, 1, 1, 0],
[1, 1, 1, 1, 1],
]
List<List<int>> arcHall = [
[0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
]
List<Widget> _generateHallItem(List<List<int>> hall){
List<Widget> list = [];
hall.map((row) {
row.map((item) {
item == 0 ? list.add(Container(width:5, height:5)) : list.add(Container(width:5, height:5, color: Colors.red));
});
});
return list;
}
Wrap(
children: _generateHallItem,
OR
InteractiveViewer(
minScale: 0.8,
maxScale: 2.0,
child: GridView(),
),
CodePudding user response:
try InteractiveViewer
and CustomMultiChildLayout
as a child (notice how easy you can animate the seats inside CinemaDelegate
with passed AnimationController
):
class CinemaHall extends StatefulWidget {
const CinemaHall({Key? key}) : super(key: key);
@override
_CinemaHallState createState() => _CinemaHallState();
}
class _CinemaHallState extends State<CinemaHall> with TickerProviderStateMixin {
late List<Offset> positions;
late List<ValueNotifier<bool>> states;
late AnimationController ctrl;
@override
void initState() {
super.initState();
positions = [
...List.generate(8, (index) => (Offset(index 1, 0))),
for (int r = 1; r < 7; r )
...List.generate(10, (index) => (Offset(index.toDouble(), r.toDouble()))),
];
states = List.generate(positions.length, (index) => ValueNotifier(false));
ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000));
Future.delayed(const Duration(milliseconds: 2000), () => ctrl.forward());
}
@override
Widget build(BuildContext context) {
return ColoredBox(
color: const Color(0xff000044),
child: InteractiveViewer(
minScale: 1.0,
maxScale: 3.0,
child: CustomMultiChildLayout(
delegate: CinemaHallDelegate(ctrl, positions, const Size(64, 64)),
children: List.generate(positions.length, (index) => LayoutId(
id: index,
child: FittedBox(
child: ValueListenableBuilder<bool>(
valueListenable: states[index],
builder: (context, state, child) {
return TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 500),
tween: Tween(end: state? 0.8 : 0.2),
builder: (context, t, child) {
return IconButton(
icon: Icon(index == 43? Icons.people : Icons.person, color: Colors.white.withOpacity(t)),
padding: EdgeInsets.zero,
iconSize: 64,
tooltip: 'seat ${positions[index].dy.toInt() 1}, ${positions[index].dx.toInt() 1}',
onPressed: () {
states[index].value = !state;
print(index);
},
);
}
);
}
),
)
)),
),
),
);
}
@override
void dispose() {
super.dispose();
ctrl.dispose();
}
}
class CinemaHallDelegate extends MultiChildLayoutDelegate {
final AnimationController ctrl;
final List<Offset> positions;
final Size seatSize;
late Size cinemaHallSize;
CinemaHallDelegate(this.ctrl, this.positions, this.seatSize) : super(relayout: ctrl) {
double cols = positions.map((o) => o.dx).reduce(max) 1;
double rows = positions.map((o) => o.dy).reduce(max) 1;
cinemaHallSize = Size(cols * seatSize.width, rows * seatSize.height);
}
@override
void performLayout(ui.Size size) {
final matrix = sizeToRect(cinemaHallSize, Offset.zero & size);
final seatRect = MatrixUtils.transformRect(matrix, Offset.zero & seatSize);
final center = size.center(Offset(-seatRect.width / 2, -seatRect.height / 2));
int childId = 0;
final constraints = BoxConstraints.tight(Size(seatRect.width, seatRect.height));
final t = Curves.bounceOut.transform(ctrl.value);
for (final position in positions) {
layoutChild(childId, constraints);
final offset = Offset(
seatRect.left position.dx * seatRect.width,
seatRect.top position.dy * seatRect.height);
positionChild(childId, Offset.lerp(center, offset, t)!);
childId ;
}
}
@override
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) => false;
}
Matrix4 sizeToRect(Size src, Rect dst, {BoxFit fit = BoxFit.contain, Alignment alignment = Alignment.center}) {
FittedSizes fs = applyBoxFit(fit, src, dst.size);
double scaleX = fs.destination.width / fs.source.width;
double scaleY = fs.destination.height / fs.source.height;
Size fittedSrc = Size(src.width * scaleX, src.height * scaleY);
Rect out = alignment.inscribe(fittedSrc, dst);
return Matrix4.identity()
..translate(out.left, out.top)
..scale(scaleX, scaleY);
}