• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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