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

import 'dart:ui' as ui show lerpDouble;

7 8
import 'package:flutter/foundation.dart';

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
import 'basic_types.dart';
import 'border_radius.dart';
import 'borders.dart';
import 'circle_border.dart';
import 'rounded_rectangle_border.dart';

/// A border that fits a stadium-shaped border (a box with semicircles on the ends)
/// within the rectangle of the widget it is applied to.
///
/// Typically used with [ShapeDecoration] to draw a stadium border.
///
/// If the rectangle is taller than it is wide, then the semicircles will be on the
/// top and bottom, and on the left and right otherwise.
///
/// See also:
///
///  * [BorderSide], which is used to describe the border of the stadium.
26
class StadiumBorder extends OutlinedBorder {
27
  /// Create a stadium border.
28
  const StadiumBorder({ super.side });
29 30

  @override
31
  ShapeBorder scale(double t) => StadiumBorder(side: side.scale(t));
32 33

  @override
34
  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
35
    if (a is StadiumBorder) {
36
      return StadiumBorder(side: BorderSide.lerp(a.side, side, t));
37
    }
38
    if (a is CircleBorder) {
39
      return _StadiumToCircleBorder(
40
        side: BorderSide.lerp(a.side, side, t),
41
        circularity: 1.0 - t,
42
        eccentricity: a.eccentricity,
43 44 45
      );
    }
    if (a is RoundedRectangleBorder) {
46
      return _StadiumToRoundedRectangleBorder(
47
        side: BorderSide.lerp(a.side, side, t),
48 49
        borderRadius: a.borderRadius,
        rectilinearity: 1.0 - t,
50 51 52 53 54 55
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
56
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
57
    if (b is StadiumBorder) {
58
      return StadiumBorder(side: BorderSide.lerp(side, b.side, t));
59
    }
60
    if (b is CircleBorder) {
61
      return _StadiumToCircleBorder(
62
        side: BorderSide.lerp(side, b.side, t),
63
        circularity: t,
64
        eccentricity: b.eccentricity,
65 66 67
      );
    }
    if (b is RoundedRectangleBorder) {
68
      return _StadiumToRoundedRectangleBorder(
69
        side: BorderSide.lerp(side, b.side, t),
70 71
        borderRadius: b.borderRadius,
        rectilinearity: t,
72 73 74 75 76
      );
    }
    return super.lerpTo(b, t);
  }

77
  @override
78
  StadiumBorder copyWith({ BorderSide? side }) {
79 80 81
    return StadiumBorder(side: side ?? this.side);
  }

82
  @override
83
  Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
84
    final Radius radius = Radius.circular(rect.shortestSide / 2.0);
85
    final RRect borderRect = RRect.fromRectAndRadius(rect, radius);
86
    final RRect adjustedRect = borderRect.deflate(side.strokeInset);
87
    return Path()
88
      ..addRRect(adjustedRect);
89 90 91
  }

  @override
92
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
93 94 95
    final Radius radius = Radius.circular(rect.shortestSide / 2.0);
    return Path()
      ..addRRect(RRect.fromRectAndRadius(rect, radius));
96 97
  }

98 99 100 101 102 103 104 105 106
  @override
  void paintInterior(Canvas canvas, Rect rect, Paint paint, { TextDirection? textDirection }) {
    final Radius radius = Radius.circular(rect.shortestSide / 2.0);
    canvas.drawRRect(RRect.fromRectAndRadius(rect, radius), paint);
  }

  @override
  bool get preferPaintInterior => true;

107
  @override
108
  void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
109 110 111 112
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
113
        final Radius radius = Radius.circular(rect.shortestSide / 2);
114
        final RRect borderRect = RRect.fromRectAndRadius(rect, radius);
115
        canvas.drawRRect(borderRect.inflate(side.strokeOffset / 2), side.toPaint());
116 117 118 119
    }
  }

  @override
120
  bool operator ==(Object other) {
121
    if (other.runtimeType != runtimeType) {
122
      return false;
123
    }
124 125
    return other is StadiumBorder
        && other.side == side;
126 127 128 129 130 131 132
  }

  @override
  int get hashCode => side.hashCode;

  @override
  String toString() {
133
    return '${objectRuntimeType(this, 'StadiumBorder')}($side)';
134 135 136 137
  }
}

// Class to help with transitioning to/from a CircleBorder.
138
class _StadiumToCircleBorder extends OutlinedBorder {
139
  const _StadiumToCircleBorder({
140
    super.side,
141
    this.circularity = 0.0,
142
    required this.eccentricity,
143
  });
144

145
  final double circularity;
146
  final double eccentricity;
147 148 149

  @override
  ShapeBorder scale(double t) {
150
    return _StadiumToCircleBorder(
151
      side: side.scale(t),
152
      circularity: t,
153
      eccentricity: eccentricity,
154 155 156 157
    );
  }

  @override
158
  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
159
    if (a is StadiumBorder) {
160
      return _StadiumToCircleBorder(
161
        side: BorderSide.lerp(a.side, side, t),
162
        circularity: circularity * t,
163
        eccentricity: eccentricity,
164 165 166
      );
    }
    if (a is CircleBorder) {
167
      return _StadiumToCircleBorder(
168
        side: BorderSide.lerp(a.side, side, t),
169
        circularity: circularity + (1.0 - circularity) * (1.0 - t),
170
        eccentricity: a.eccentricity,
171 172 173
      );
    }
    if (a is _StadiumToCircleBorder) {
174
      return _StadiumToCircleBorder(
175
        side: BorderSide.lerp(a.side, side, t),
176
        circularity: ui.lerpDouble(a.circularity, circularity, t)!,
177
        eccentricity: ui.lerpDouble(a.eccentricity, eccentricity, t)!,
178 179 180 181 182 183
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
184
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
185
    if (b is StadiumBorder) {
186
      return _StadiumToCircleBorder(
187
        side: BorderSide.lerp(side, b.side, t),
188
        circularity: circularity * (1.0 - t),
189
        eccentricity: eccentricity,
190 191 192
      );
    }
    if (b is CircleBorder) {
193
      return _StadiumToCircleBorder(
194
        side: BorderSide.lerp(side, b.side, t),
195
        circularity: circularity + (1.0 - circularity) * t,
196
        eccentricity: b.eccentricity,
197 198 199
      );
    }
    if (b is _StadiumToCircleBorder) {
200
      return _StadiumToCircleBorder(
201
        side: BorderSide.lerp(side, b.side, t),
202
        circularity: ui.lerpDouble(circularity, b.circularity, t)!,
203
        eccentricity: ui.lerpDouble(eccentricity, b.eccentricity, t)!,
204 205 206 207 208 209
      );
    }
    return super.lerpTo(b, t);
  }

  Rect _adjustRect(Rect rect) {
210
    if (circularity == 0.0 || rect.width == rect.height) {
211
      return rect;
212
    }
213
    if (rect.width < rect.height) {
214
      final double partialDelta = (rect.height - rect.width) / 2;
215
      final double delta = circularity * partialDelta * (1.0 - eccentricity);
216
      return Rect.fromLTRB(
217 218 219 220 221 222
        rect.left,
        rect.top + delta,
        rect.right,
        rect.bottom - delta,
      );
    } else {
223
      final double partialDelta = (rect.width - rect.height) / 2;
224
      final double delta = circularity * partialDelta * (1.0 - eccentricity);
225
      return Rect.fromLTRB(
226 227 228 229 230 231 232 233 234
        rect.left + delta,
        rect.top,
        rect.right - delta,
        rect.bottom,
      );
    }
  }

  BorderRadius _adjustBorderRadius(Rect rect) {
235 236 237 238 239 240
    final BorderRadius circleRadius = BorderRadius.circular(rect.shortestSide / 2);
    if (eccentricity != 0.0) {
      if (rect.width < rect.height) {
        return BorderRadius.lerp(
          circleRadius,
          BorderRadius.all(Radius.elliptical(rect.width / 2, (0.5 + eccentricity / 2) * rect.height / 2)),
241
          circularity,
242 243 244 245 246
        )!;
      } else {
        return BorderRadius.lerp(
            circleRadius,
            BorderRadius.all(Radius.elliptical((0.5 + eccentricity / 2) * rect.width / 2, rect.height / 2)),
247
            circularity,
248 249 250 251
        )!;
      }
    }
    return circleRadius;
252 253 254
  }

  @override
255
  Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
256
    return Path()
257
      ..addRRect(_adjustBorderRadius(rect).toRRect(_adjustRect(rect)).deflate(side.strokeInset));
258 259 260
  }

  @override
261
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
262
    return Path()
263 264 265
      ..addRRect(_adjustBorderRadius(rect).toRRect(_adjustRect(rect)));
  }

266 267 268 269 270 271 272 273
  @override
  void paintInterior(Canvas canvas, Rect rect, Paint paint, { TextDirection? textDirection }) {
    canvas.drawRRect(_adjustBorderRadius(rect).toRRect(_adjustRect(rect)), paint);
  }

  @override
  bool get preferPaintInterior => true;

274
  @override
275
  _StadiumToCircleBorder copyWith({ BorderSide? side, double? circularity, double? eccentricity }) {
276 277
    return _StadiumToCircleBorder(
      side: side ?? this.side,
278
      circularity: circularity ?? this.circularity,
279
      eccentricity: eccentricity ?? this.eccentricity,
280 281 282
    );
  }

283
  @override
284
  void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
285 286 287 288
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
289 290
        final RRect borderRect = _adjustBorderRadius(rect).toRRect(_adjustRect(rect));
        canvas.drawRRect(borderRect.inflate(side.strokeOffset / 2), side.toPaint());
291 292 293 294
    }
  }

  @override
295
  bool operator ==(Object other) {
296
    if (other.runtimeType != runtimeType) {
297
      return false;
298
    }
299 300
    return other is _StadiumToCircleBorder
        && other.side == side
301
        && other.circularity == circularity;
302 303 304
  }

  @override
305
  int get hashCode => Object.hash(side, circularity);
306 307 308

  @override
  String toString() {
309
    if (eccentricity != 0.0) {
310
      return 'StadiumBorder($side, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder that is ${(eccentricity * 100).toStringAsFixed(1)}% oval)';
311
    }
312
    return 'StadiumBorder($side, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder)';
313 314 315 316
  }
}

// Class to help with transitioning to/from a RoundedRectBorder.
317
class _StadiumToRoundedRectangleBorder extends OutlinedBorder {
318
  const _StadiumToRoundedRectangleBorder({
319
    super.side,
320
    this.borderRadius = BorderRadius.zero,
321
    this.rectilinearity = 0.0,
322
  });
323

324
  final BorderRadiusGeometry borderRadius;
325

326
  final double rectilinearity;
327 328 329

  @override
  ShapeBorder scale(double t) {
330
    return _StadiumToRoundedRectangleBorder(
331 332
      side: side.scale(t),
      borderRadius: borderRadius * t,
333
      rectilinearity: t,
334 335 336 337
    );
  }

  @override
338
  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
339
    if (a is StadiumBorder) {
340
      return _StadiumToRoundedRectangleBorder(
341 342
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: borderRadius,
343
        rectilinearity: rectilinearity * t,
344 345 346
      );
    }
    if (a is RoundedRectangleBorder) {
347
      return _StadiumToRoundedRectangleBorder(
348 349
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: borderRadius,
350
        rectilinearity: rectilinearity + (1.0 - rectilinearity) * (1.0 - t),
351 352 353
      );
    }
    if (a is _StadiumToRoundedRectangleBorder) {
354
      return _StadiumToRoundedRectangleBorder(
355
        side: BorderSide.lerp(a.side, side, t),
356 357
        borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!,
        rectilinearity: ui.lerpDouble(a.rectilinearity, rectilinearity, t)!,
358 359 360 361 362 363
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
364
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
365
    if (b is StadiumBorder) {
366
      return _StadiumToRoundedRectangleBorder(
367 368
        side: BorderSide.lerp(side, b.side, t),
        borderRadius: borderRadius,
369
        rectilinearity: rectilinearity * (1.0 - t),
370 371 372
      );
    }
    if (b is RoundedRectangleBorder) {
373
      return _StadiumToRoundedRectangleBorder(
374 375
        side: BorderSide.lerp(side, b.side, t),
        borderRadius: borderRadius,
376
        rectilinearity: rectilinearity + (1.0 - rectilinearity) * t,
377 378 379
      );
    }
    if (b is _StadiumToRoundedRectangleBorder) {
380
      return _StadiumToRoundedRectangleBorder(
381
        side: BorderSide.lerp(side, b.side, t),
382 383
        borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
        rectilinearity: ui.lerpDouble(rectilinearity, b.rectilinearity, t)!,
384 385 386 387 388
      );
    }
    return super.lerpTo(b, t);
  }

389 390
  BorderRadiusGeometry _adjustBorderRadius(Rect rect) {
    return BorderRadiusGeometry.lerp(
391
      borderRadius,
392
      BorderRadius.all(Radius.circular(rect.shortestSide / 2.0)),
393
      1.0 - rectilinearity,
394
    )!;
395 396 397
  }

  @override
398
  Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
399
    final RRect borderRect = _adjustBorderRadius(rect).resolve(textDirection).toRRect(rect);
400
    final RRect adjustedRect = borderRect.deflate(ui.lerpDouble(side.width, 0, side.strokeAlign)!);
401
    return Path()
402
      ..addRRect(adjustedRect);
403 404 405
  }

  @override
406
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
407
    return Path()
408
      ..addRRect(_adjustBorderRadius(rect).resolve(textDirection).toRRect(rect));
409 410
  }

411 412
  @override
  void paintInterior(Canvas canvas, Rect rect, Paint paint, { TextDirection? textDirection }) {
413
    final BorderRadiusGeometry adjustedBorderRadius = _adjustBorderRadius(rect);
414 415 416
    if (adjustedBorderRadius == BorderRadius.zero) {
      canvas.drawRect(rect, paint);
    } else {
417
      canvas.drawRRect(adjustedBorderRadius.resolve(textDirection).toRRect(rect), paint);
418 419 420 421 422 423
    }
  }

  @override
  bool get preferPaintInterior => true;

424
  @override
425
  _StadiumToRoundedRectangleBorder copyWith({ BorderSide? side, BorderRadiusGeometry? borderRadius, double? rectilinearity }) {
426 427 428
    return _StadiumToRoundedRectangleBorder(
      side: side ?? this.side,
      borderRadius: borderRadius ?? this.borderRadius,
429
      rectilinearity: rectilinearity ?? this.rectilinearity,
430 431 432
    );
  }

433
  @override
434
  void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
435 436 437 438
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
439
        final BorderRadiusGeometry adjustedBorderRadius = _adjustBorderRadius(rect);
440 441
        final RRect borderRect = adjustedBorderRadius.resolve(textDirection).toRRect(rect);
        canvas.drawRRect(borderRect.inflate(side.strokeOffset / 2), side.toPaint());
442 443 444 445
    }
  }

  @override
446
  bool operator ==(Object other) {
447
    if (other.runtimeType != runtimeType) {
448
      return false;
449
    }
450 451 452
    return other is _StadiumToRoundedRectangleBorder
        && other.side == side
        && other.borderRadius == borderRadius
453
        && other.rectilinearity == rectilinearity;
454 455 456
  }

  @override
457
  int get hashCode => Object.hash(side, borderRadius, rectilinearity);
458 459 460 461

  @override
  String toString() {
    return 'StadiumBorder($side, $borderRadius, '
462
           '${(rectilinearity * 100).toStringAsFixed(1)}% of the way to being a '
463 464 465
           'RoundedRectangleBorder)';
  }
}