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 28 29
  /// Create a stadium border.
  ///
  /// The [side] argument must not be null.
30
  const StadiumBorder({ super.side });
31 32

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

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

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

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

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

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

100 101 102 103 104 105 106 107 108
  @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;

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

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

  @override
  int get hashCode => side.hashCode;

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

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

147
  final double circularity;
148
  final double eccentricity;
149 150 151

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

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

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

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

  BorderRadius _adjustBorderRadius(Rect rect) {
237 238 239 240 241 242
    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)),
243
          circularity,
244 245 246 247 248
        )!;
      } else {
        return BorderRadius.lerp(
            circleRadius,
            BorderRadius.all(Radius.elliptical((0.5 + eccentricity / 2) * rect.width / 2, rect.height / 2)),
249
            circularity,
250 251 252 253
        )!;
      }
    }
    return circleRadius;
254 255 256
  }

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

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

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

  @override
  bool get preferPaintInterior => true;

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

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

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

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

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

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

326
  final BorderRadiusGeometry borderRadius;
327

328
  final double rectilinearity;
329 330 331

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

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

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

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

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

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

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

  @override
  bool get preferPaintInterior => true;

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

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

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

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

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