1// Copyright 2015 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'package:flutter/gestures.dart'; 6import 'package:flutter/material.dart'; 7import 'package:flutter/rendering.dart'; 8 9class CardModel { 10 CardModel(this.value, this.height, this.color); 11 12 int value; 13 double height; 14 Color color; 15 16 String get label => 'Card $value'; 17 Key get key => ObjectKey(this); 18 GlobalKey get targetKey => GlobalObjectKey(this); 19} 20 21enum MarkerType { topLeft, bottomRight, touch } 22 23class _MarkerPainter extends CustomPainter { 24 const _MarkerPainter({ 25 this.size, 26 this.type, 27 }); 28 29 final double size; 30 final MarkerType type; 31 32 @override 33 void paint(Canvas canvas, _) { 34 final Paint paint = Paint()..color = const Color(0x8000FF00); 35 final double r = size / 2.0; 36 canvas.drawCircle(Offset(r, r), r, paint); 37 38 paint 39 ..color = const Color(0xFFFFFFFF) 40 ..style = PaintingStyle.stroke 41 ..strokeWidth = 1.0; 42 if (type == MarkerType.topLeft) { 43 canvas.drawLine(Offset(r, r), Offset(r + r - 1.0, r), paint); 44 canvas.drawLine(Offset(r, r), Offset(r, r + r - 1.0), paint); 45 } 46 if (type == MarkerType.bottomRight) { 47 canvas.drawLine(Offset(r, r), Offset(1.0, r), paint); 48 canvas.drawLine(Offset(r, r), Offset(r, 1.0), paint); 49 } 50 } 51 52 @override 53 bool shouldRepaint(_MarkerPainter oldPainter) { 54 return oldPainter.size != size 55 || oldPainter.type != type; 56 } 57} 58 59class Marker extends StatelessWidget { 60 const Marker({ 61 Key key, 62 this.type = MarkerType.touch, 63 this.position, 64 this.size = 40.0, 65 }) : super(key: key); 66 67 final Offset position; 68 final double size; 69 final MarkerType type; 70 71 @override 72 Widget build(BuildContext context) { 73 return Positioned( 74 left: position.dx - size / 2.0, 75 top: position.dy - size / 2.0, 76 width: size, 77 height: size, 78 child: IgnorePointer( 79 child: CustomPaint( 80 painter: _MarkerPainter( 81 size: size, 82 type: type, 83 ), 84 ), 85 ), 86 ); 87 } 88} 89 90class OverlayGeometryApp extends StatefulWidget { 91 @override 92 OverlayGeometryAppState createState() => OverlayGeometryAppState(); 93} 94 95typedef CardTapCallback = void Function(GlobalKey targetKey, Offset globalPosition); 96 97class CardBuilder extends SliverChildDelegate { 98 CardBuilder({ this.cardModels, this.onTapUp }); 99 100 final List<CardModel> cardModels; 101 final CardTapCallback onTapUp; 102 103 static const TextStyle cardLabelStyle = 104 TextStyle(color: Colors.white, fontSize: 18.0, fontWeight: FontWeight.bold); 105 106 @override 107 Widget build(BuildContext context, int index) { 108 if (index >= cardModels.length) 109 return null; 110 final CardModel cardModel = cardModels[index]; 111 return GestureDetector( 112 key: cardModel.key, 113 onTapUp: (TapUpDetails details) { onTapUp(cardModel.targetKey, details.globalPosition); }, 114 child: Card( 115 key: cardModel.targetKey, 116 color: cardModel.color, 117 child: Container( 118 height: cardModel.height, 119 padding: const EdgeInsets.all(8.0), 120 child: Center(child: Text(cardModel.label, style: cardLabelStyle)), 121 ), 122 ), 123 ); 124 } 125 126 @override 127 int get estimatedChildCount => cardModels.length; 128 129 @override 130 bool shouldRebuild(CardBuilder oldDelegate) { 131 return oldDelegate.cardModels != cardModels; 132 } 133} 134 135class OverlayGeometryAppState extends State<OverlayGeometryApp> { 136 List<CardModel> cardModels; 137 Map<MarkerType, Offset> markers = <MarkerType, Offset>{}; 138 double markersScrollOffset = 0.0; 139 140 @override 141 void initState() { 142 super.initState(); 143 final List<double> cardHeights = <double>[ 144 48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0, 145 48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0, 146 48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0, 147 ]; 148 cardModels = List<CardModel>.generate(cardHeights.length, (int i) { 149 final Color color = Color.lerp(Colors.red.shade300, Colors.blue.shade900, i / cardHeights.length); 150 return CardModel(i, cardHeights[i], color); 151 }); 152 } 153 154 bool handleScrollNotification(ScrollNotification notification) { 155 if (notification is ScrollUpdateNotification && notification.depth == 0) { 156 setState(() { 157 final double dy = markersScrollOffset - notification.metrics.extentBefore; 158 markersScrollOffset = notification.metrics.extentBefore; 159 for (MarkerType type in markers.keys) { 160 final Offset oldPosition = markers[type]; 161 markers[type] = oldPosition.translate(0.0, dy); 162 } 163 }); 164 } 165 return false; 166 } 167 168 void handleTapUp(GlobalKey target, Offset globalPosition) { 169 setState(() { 170 markers[MarkerType.touch] = globalPosition; 171 final RenderBox box = target.currentContext.findRenderObject(); 172 markers[MarkerType.topLeft] = box.localToGlobal(const Offset(0.0, 0.0)); 173 final Size size = box.size; 174 markers[MarkerType.bottomRight] = box.localToGlobal(Offset(size.width, size.height)); 175 final ScrollableState scrollable = Scrollable.of(target.currentContext); 176 markersScrollOffset = scrollable.position.pixels; 177 }); 178 } 179 180 @override 181 Widget build(BuildContext context) { 182 final List<Widget> layers = <Widget>[ 183 Scaffold( 184 appBar: AppBar(title: const Text('Tap a Card')), 185 body: Container( 186 padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0), 187 child: NotificationListener<ScrollNotification>( 188 onNotification: handleScrollNotification, 189 child: ListView.custom( 190 childrenDelegate: CardBuilder( 191 cardModels: cardModels, 192 onTapUp: handleTapUp, 193 ), 194 ), 195 ), 196 ), 197 ), 198 ]; 199 for (MarkerType type in markers.keys) 200 layers.add(Marker(type: type, position: markers[type])); 201 return Stack(children: layers); 202 } 203} 204 205void main() { 206 runApp(MaterialApp( 207 theme: ThemeData( 208 brightness: Brightness.light, 209 primarySwatch: Colors.blue, 210 accentColor: Colors.redAccent, 211 ), 212 title: 'Cards', 213 home: OverlayGeometryApp(), 214 )); 215} 216