overlay_geometry.dart 6.05 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/material.dart';
6 7 8

class CardModel {
  CardModel(this.value, this.height, this.color);
9

10 11 12
  int value;
  double height;
  Color color;
13 14

  String get label => 'Card $value';
15 16
  Key get key => ObjectKey(this);
  GlobalKey get targetKey => GlobalObjectKey(this);
17 18 19 20
}

enum MarkerType { topLeft, bottomRight, touch }

21 22
class _MarkerPainter extends CustomPainter {
  const _MarkerPainter({
23 24
    required this.size,
    required this.type,
25
  });
26 27 28 29

  final double size;
  final MarkerType type;

30
  @override
Adam Barth's avatar
Adam Barth committed
31
  void paint(Canvas canvas, _) {
32
    final Paint paint = Paint()..color = const Color(0x8000FF00);
33
    final double r = size / 2.0;
34
    canvas.drawCircle(Offset(r, r), r, paint);
35

36 37
    paint
      ..color = const Color(0xFFFFFFFF)
38
      ..style = PaintingStyle.stroke
39
      ..strokeWidth = 1.0;
40
    if (type == MarkerType.topLeft) {
41 42
      canvas.drawLine(Offset(r, r), Offset(r + r - 1.0, r), paint);
      canvas.drawLine(Offset(r, r), Offset(r, r + r - 1.0), paint);
43 44
    }
    if (type == MarkerType.bottomRight) {
45 46
      canvas.drawLine(Offset(r, r), Offset(1.0, r), paint);
      canvas.drawLine(Offset(r, r), Offset(r, 1.0), paint);
47 48 49
    }
  }

50
  @override
51 52 53 54 55 56
  bool shouldRepaint(_MarkerPainter oldPainter) {
    return oldPainter.size != size
        || oldPainter.type != type;
  }
}

57
class Marker extends StatelessWidget {
58
  const Marker({
59
    Key? key,
60
    this.type = MarkerType.touch,
61
    this.position,
62
    this.size = 40.0,
63 64
  }) : super(key: key);

65
  final Offset? position;
66 67 68
  final double size;
  final MarkerType type;

69
  @override
70
  Widget build(BuildContext context) {
71
    return Positioned(
72 73
      left: position!.dx - size / 2.0,
      top: position!.dy - size / 2.0,
74 75
      width: size,
      height: size,
76 77 78
      child: IgnorePointer(
        child: CustomPaint(
          painter: _MarkerPainter(
79
            size: size,
80 81 82 83
            type: type,
          ),
        ),
      ),
84 85 86 87
    );
  }
}

88
class OverlayGeometryApp extends StatefulWidget {
89
  const OverlayGeometryApp({Key? key}) : super(key: key);
90

91
  @override
92
  OverlayGeometryAppState createState() => OverlayGeometryAppState();
93 94
}

95
typedef CardTapCallback = void Function(GlobalKey targetKey, Offset globalPosition);
96

97
class CardBuilder extends SliverChildDelegate {
98
  CardBuilder({List<CardModel>? cardModels, this.onTapUp }) : cardModels = cardModels ?? <CardModel>[];
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 109
    if (index >= cardModels.length)
      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 116
        key: cardModel.targetKey,
        color: cardModel.color,
117
        child: Container(
118 119
          height: cardModel.height,
          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 131
  bool shouldRebuild(CardBuilder oldDelegate) {
    return oldDelegate.cardModels != cardModels;
132
  }
133 134 135
}

class OverlayGeometryAppState extends State<OverlayGeometryApp> {
136
  List<CardModel> cardModels = <CardModel>[];
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 145
      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,
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 150
      final Color? color = Color.lerp(Colors.red.shade300, Colors.blue.shade900, i / cardHeights.length);
      return CardModel(i, cardHeights[i], color!);
151 152 153
    });
  }

Adam Barth's avatar
Adam Barth committed
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
        markers.forEach((MarkerType type, Offset oldPosition) {
160
          markers[type] = oldPosition.translate(0.0, dy);
161
        });
162 163 164
      });
    }
    return false;
165 166
  }

167
  void handleTapUp(GlobalKey target, Offset globalPosition) {
168
    setState(() {
169
      markers[MarkerType.touch] = globalPosition;
170 171
      final RenderBox? box = target.currentContext?.findRenderObject() as RenderBox?;
      markers[MarkerType.topLeft] = box!.localToGlobal(Offset.zero);
172
      final Size size = box.size;
173
      markers[MarkerType.bottomRight] = box.localToGlobal(Offset(size.width, size.height));
174 175
      final ScrollableState? scrollable = Scrollable.of(target.currentContext!);
      markersScrollOffset = scrollable!.position.pixels;
176 177 178
    });
  }

179
  @override
180
  Widget build(BuildContext context) {
181 182 183 184 185 186 187 188 189 190 191 192 193
    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,
                ),
194 195 196 197
              ),
            ),
          ),
        ),
198
        for (final MarkerType type in markers.keys)
199 200 201
          Marker(type: type, position: markers[type]),
      ],
    );
202 203 204
  }
}

205
void main() {
206 207 208 209
  runApp(
    const MaterialApp(
      title: 'Cards',
      home: OverlayGeometryApp(),
210
    ),
211
  );
212
}