| // Copyright 2014 The Flutter 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 'package:flutter/material.dart'; |
| |
| class CardModel { |
| CardModel(this.value, this.height, this.color); |
| |
| int value; |
| double height; |
| Color color; |
| |
| String get label => 'Card $value'; |
| Key get key => ObjectKey(this); |
| GlobalKey get targetKey => GlobalObjectKey(this); |
| } |
| |
| enum MarkerType { topLeft, bottomRight, touch } |
| |
| class _MarkerPainter extends CustomPainter { |
| const _MarkerPainter({ |
| required this.size, |
| required this.type, |
| }); |
| |
| final double size; |
| final MarkerType type; |
| |
| @override |
| void paint(Canvas canvas, _) { |
| final Paint paint = Paint()..color = const Color(0x8000FF00); |
| final double r = size / 2.0; |
| canvas.drawCircle(Offset(r, r), r, paint); |
| |
| paint |
| ..color = const Color(0xFFFFFFFF) |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = 1.0; |
| if (type == MarkerType.topLeft) { |
| canvas.drawLine(Offset(r, r), Offset(r + r - 1.0, r), paint); |
| canvas.drawLine(Offset(r, r), Offset(r, r + r - 1.0), paint); |
| } |
| if (type == MarkerType.bottomRight) { |
| canvas.drawLine(Offset(r, r), Offset(1.0, r), paint); |
| canvas.drawLine(Offset(r, r), Offset(r, 1.0), paint); |
| } |
| } |
| |
| @override |
| bool shouldRepaint(_MarkerPainter oldPainter) { |
| return oldPainter.size != size |
| || oldPainter.type != type; |
| } |
| } |
| |
| class Marker extends StatelessWidget { |
| const Marker({ |
| super.key, |
| this.type = MarkerType.touch, |
| this.position, |
| this.size = 40.0, |
| }); |
| |
| final Offset? position; |
| final double size; |
| final MarkerType type; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Positioned( |
| left: position!.dx - size / 2.0, |
| top: position!.dy - size / 2.0, |
| width: size, |
| height: size, |
| child: IgnorePointer( |
| child: CustomPaint( |
| painter: _MarkerPainter( |
| size: size, |
| type: type, |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class OverlayGeometryApp extends StatefulWidget { |
| const OverlayGeometryApp({super.key}); |
| |
| @override |
| OverlayGeometryAppState createState() => OverlayGeometryAppState(); |
| } |
| |
| typedef CardTapCallback = void Function(GlobalKey targetKey, Offset globalPosition); |
| |
| class CardBuilder extends SliverChildDelegate { |
| CardBuilder({List<CardModel>? cardModels, this.onTapUp }) : cardModels = cardModels ?? <CardModel>[]; |
| |
| final List<CardModel> cardModels; |
| final CardTapCallback? onTapUp; |
| |
| static const TextStyle cardLabelStyle = |
| TextStyle(color: Colors.white, fontSize: 18.0, fontWeight: FontWeight.bold); |
| |
| @override |
| Widget? build(BuildContext context, int index) { |
| if (index >= cardModels.length) { |
| return null; |
| } |
| final CardModel cardModel = cardModels[index]; |
| return GestureDetector( |
| key: cardModel.key, |
| onTapUp: (TapUpDetails details) { onTapUp!(cardModel.targetKey, details.globalPosition); }, |
| child: Card( |
| key: cardModel.targetKey, |
| color: cardModel.color, |
| child: Container( |
| height: cardModel.height, |
| padding: const EdgeInsets.all(8.0), |
| child: Center(child: Text(cardModel.label, style: cardLabelStyle)), |
| ), |
| ), |
| ); |
| } |
| |
| @override |
| int get estimatedChildCount => cardModels.length; |
| |
| @override |
| bool shouldRebuild(CardBuilder oldDelegate) { |
| return oldDelegate.cardModels != cardModels; |
| } |
| } |
| |
| class OverlayGeometryAppState extends State<OverlayGeometryApp> { |
| List<CardModel> cardModels = <CardModel>[]; |
| Map<MarkerType, Offset> markers = <MarkerType, Offset>{}; |
| double markersScrollOffset = 0.0; |
| |
| @override |
| void initState() { |
| super.initState(); |
| final List<double> cardHeights = <double>[ |
| 48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0, |
| 48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0, |
| 48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0, |
| ]; |
| cardModels = List<CardModel>.generate(cardHeights.length, (int i) { |
| final Color? color = Color.lerp(Colors.red.shade300, Colors.blue.shade900, i / cardHeights.length); |
| return CardModel(i, cardHeights[i], color!); |
| }); |
| } |
| |
| bool handleScrollNotification(ScrollNotification notification) { |
| if (notification is ScrollUpdateNotification && notification.depth == 0) { |
| setState(() { |
| final double dy = markersScrollOffset - notification.metrics.extentBefore; |
| markersScrollOffset = notification.metrics.extentBefore; |
| markers.forEach((MarkerType type, Offset oldPosition) { |
| markers[type] = oldPosition.translate(0.0, dy); |
| }); |
| }); |
| } |
| return false; |
| } |
| |
| void handleTapUp(GlobalKey target, Offset globalPosition) { |
| setState(() { |
| markers[MarkerType.touch] = globalPosition; |
| final RenderBox? box = target.currentContext?.findRenderObject() as RenderBox?; |
| markers[MarkerType.topLeft] = box!.localToGlobal(Offset.zero); |
| final Size size = box.size; |
| markers[MarkerType.bottomRight] = box.localToGlobal(Offset(size.width, size.height)); |
| final ScrollableState? scrollable = Scrollable.of(target.currentContext!); |
| markersScrollOffset = scrollable!.position.pixels; |
| }); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return Stack( |
| children: <Widget>[ |
| Scaffold( |
| appBar: AppBar(title: const Text('Tap a Card')), |
| body: Container( |
| padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0), |
| child: NotificationListener<ScrollNotification>( |
| onNotification: handleScrollNotification, |
| child: ListView.custom( |
| childrenDelegate: CardBuilder( |
| cardModels: cardModels, |
| onTapUp: handleTapUp, |
| ), |
| ), |
| ), |
| ), |
| ), |
| for (final MarkerType type in markers.keys) |
| Marker(type: type, position: markers[type]), |
| ], |
| ); |
| } |
| } |
| |
| void main() { |
| runApp( |
| const MaterialApp( |
| title: 'Cards', |
| home: OverlayGeometryApp(), |
| ), |
| ); |
| } |