overlay_geometry.dart 6.03 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
    super.key,
60
    this.type = MarkerType.touch,
61
    this.position,
62
    this.size = 40.0,
63
  });
64

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({super.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
    if (index >= cardModels.length) {
109
      return null;
110
    }
111
    final CardModel cardModel = cardModels[index];
112
    return GestureDetector(
113
      key: cardModel.key,
114
      onTapUp: (TapUpDetails details) { onTapUp!(cardModel.targetKey, details.globalPosition); },
115
      child: Card(
116 117
        key: cardModel.targetKey,
        color: cardModel.color,
118
        child: Container(
119 120
          height: cardModel.height,
          padding: const EdgeInsets.all(8.0),
121
          child: Center(child: Text(cardModel.label, style: cardLabelStyle)),
122 123
        ),
      ),
124 125 126 127
    );
  }

  @override
128
  int get estimatedChildCount => cardModels.length;
129 130

  @override
131 132
  bool shouldRebuild(CardBuilder oldDelegate) {
    return oldDelegate.cardModels != cardModels;
133
  }
134 135 136
}

class OverlayGeometryAppState extends State<OverlayGeometryApp> {
137
  List<CardModel> cardModels = <CardModel>[];
138
  Map<MarkerType, Offset> markers = <MarkerType, Offset>{};
139
  double markersScrollOffset = 0.0;
140

141
  @override
142
  void initState() {
143
    super.initState();
144
    final List<double> cardHeights = <double>[
145 146
      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,
147
      48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0,
148
    ];
149
    cardModels = List<CardModel>.generate(cardHeights.length, (int i) {
150 151
      final Color? color = Color.lerp(Colors.red.shade300, Colors.blue.shade900, i / cardHeights.length);
      return CardModel(i, cardHeights[i], color!);
152 153 154
    });
  }

Adam Barth's avatar
Adam Barth committed
155
  bool handleScrollNotification(ScrollNotification notification) {
156
    if (notification is ScrollUpdateNotification && notification.depth == 0) {
157
      setState(() {
158
        final double dy = markersScrollOffset - notification.metrics.extentBefore;
159
        markersScrollOffset = notification.metrics.extentBefore;
160
        markers.forEach((MarkerType type, Offset oldPosition) {
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 172
      final RenderBox? box = target.currentContext?.findRenderObject() as RenderBox?;
      markers[MarkerType.topLeft] = box!.localToGlobal(Offset.zero);
173
      final Size size = box.size;
174
      markers[MarkerType.bottomRight] = box.localToGlobal(Offset(size.width, size.height));
175 176
      final ScrollableState scrollable = Scrollable.of(target.currentContext!);
      markersScrollOffset = scrollable.position.pixels;
177 178 179
    });
  }

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

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