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 }) : assert(side != null);
31 32

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

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

  @override
59
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
60
    assert(t != null);
61
    if (b is StadiumBorder) {
62
      return StadiumBorder(side: BorderSide.lerp(side, b.side, t));
63
    }
64
    if (b is CircleBorder) {
65
      return _StadiumToCircleBorder(
66 67
        side: BorderSide.lerp(side, b.side, t),
        circleness: t,
68
        eccentricity: b.eccentricity,
69 70 71
      );
    }
    if (b is RoundedRectangleBorder) {
72
      return _StadiumToRoundedRectangleBorder(
73
        side: BorderSide.lerp(side, b.side, t),
74
        borderRadius: b.borderRadius as BorderRadius,
75 76 77 78 79 80
        rectness: t,
      );
    }
    return super.lerpTo(b, t);
  }

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

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

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

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

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

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

  @override
  int get hashCode => side.hashCode;

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

// Class to help with transitioning to/from a CircleBorder.
142
class _StadiumToCircleBorder extends OutlinedBorder {
143
  const _StadiumToCircleBorder({
144
    super.side,
145
    this.circleness = 0.0,
146
    required this.eccentricity,
147
  }) : assert(side != null),
148
       assert(circleness != null);
149 150

  final double circleness;
151
  final double eccentricity;
152 153 154

  @override
  ShapeBorder scale(double t) {
155
    return _StadiumToCircleBorder(
156 157
      side: side.scale(t),
      circleness: t,
158
      eccentricity: eccentricity,
159 160 161 162
    );
  }

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

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

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

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

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

  @override
268
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
269
    return Path()
270 271 272
      ..addRRect(_adjustBorderRadius(rect).toRRect(_adjustRect(rect)));
  }

273 274 275 276 277 278 279 280
  @override
  void paintInterior(Canvas canvas, Rect rect, Paint paint, { TextDirection? textDirection }) {
    canvas.drawRRect(_adjustBorderRadius(rect).toRRect(_adjustRect(rect)), paint);
  }

  @override
  bool get preferPaintInterior => true;

281
  @override
282
  _StadiumToCircleBorder copyWith({ BorderSide? side, double? circleness, double? eccentricity }) {
283 284 285
    return _StadiumToCircleBorder(
      side: side ?? this.side,
      circleness: circleness ?? this.circleness,
286
      eccentricity: eccentricity ?? this.eccentricity,
287 288 289
    );
  }

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

  @override
302
  bool operator ==(Object other) {
303
    if (other.runtimeType != runtimeType) {
304
      return false;
305
    }
306 307 308
    return other is _StadiumToCircleBorder
        && other.side == side
        && other.circleness == circleness;
309 310 311
  }

  @override
312
  int get hashCode => Object.hash(side, circleness);
313 314 315

  @override
  String toString() {
316 317 318 319
    if (eccentricity != 0.0) {
      return 'StadiumBorder($side, ${(circleness * 100).toStringAsFixed(1)}% of the way to being a CircleBorder that is ${(eccentricity * 100).toStringAsFixed(1)}% oval)';
    }
    return 'StadiumBorder($side, ${(circleness * 100).toStringAsFixed(1)}% of the way to being a CircleBorder)';
320 321 322 323
  }
}

// Class to help with transitioning to/from a RoundedRectBorder.
324
class _StadiumToRoundedRectangleBorder extends OutlinedBorder {
325
  const _StadiumToRoundedRectangleBorder({
326
    super.side,
327 328
    this.borderRadius = BorderRadius.zero,
    this.rectness = 0.0,
329 330
  }) : assert(side != null),
       assert(borderRadius != null),
331
       assert(rectness != null);
332 333 334 335 336 337 338

  final BorderRadius borderRadius;

  final double rectness;

  @override
  ShapeBorder scale(double t) {
339
    return _StadiumToRoundedRectangleBorder(
340 341 342 343 344 345 346
      side: side.scale(t),
      borderRadius: borderRadius * t,
      rectness: t,
    );
  }

  @override
347
  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
348
    assert(t != null);
349
    if (a is StadiumBorder) {
350
      return _StadiumToRoundedRectangleBorder(
351 352 353 354 355 356
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: borderRadius,
        rectness: rectness * t,
      );
    }
    if (a is RoundedRectangleBorder) {
357
      return _StadiumToRoundedRectangleBorder(
358 359 360 361 362 363
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: borderRadius,
        rectness: rectness + (1.0 - rectness) * (1.0 - t),
      );
    }
    if (a is _StadiumToRoundedRectangleBorder) {
364
      return _StadiumToRoundedRectangleBorder(
365
        side: BorderSide.lerp(a.side, side, t),
366 367
        borderRadius: BorderRadius.lerp(a.borderRadius, borderRadius, t)!,
        rectness: ui.lerpDouble(a.rectness, rectness, t)!,
368 369 370 371 372 373
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
374
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
375
    assert(t != null);
376
    if (b is StadiumBorder) {
377
      return _StadiumToRoundedRectangleBorder(
378 379 380 381 382 383
        side: BorderSide.lerp(side, b.side, t),
        borderRadius: borderRadius,
        rectness: rectness * (1.0 - t),
      );
    }
    if (b is RoundedRectangleBorder) {
384
      return _StadiumToRoundedRectangleBorder(
385 386 387 388 389 390
        side: BorderSide.lerp(side, b.side, t),
        borderRadius: borderRadius,
        rectness: rectness + (1.0 - rectness) * t,
      );
    }
    if (b is _StadiumToRoundedRectangleBorder) {
391
      return _StadiumToRoundedRectangleBorder(
392
        side: BorderSide.lerp(side, b.side, t),
393 394
        borderRadius: BorderRadius.lerp(borderRadius, b.borderRadius, t)!,
        rectness: ui.lerpDouble(rectness, b.rectness, t)!,
395 396 397 398 399 400 401 402
      );
    }
    return super.lerpTo(b, t);
  }

  BorderRadius _adjustBorderRadius(Rect rect) {
    return BorderRadius.lerp(
      borderRadius,
403
      BorderRadius.all(Radius.circular(rect.shortestSide / 2.0)),
404
      1.0 - rectness,
405
    )!;
406 407 408
  }

  @override
409
  Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
410
    final RRect borderRect = _adjustBorderRadius(rect).toRRect(rect);
411
    final RRect adjustedRect = borderRect.deflate(ui.lerpDouble(side.width, 0, side.strokeAlign)!);
412
    return Path()
413
      ..addRRect(adjustedRect);
414 415 416
  }

  @override
417
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
418
    return Path()
419 420 421
      ..addRRect(_adjustBorderRadius(rect).toRRect(rect));
  }

422 423 424 425 426 427 428 429 430 431 432 433 434
  @override
  void paintInterior(Canvas canvas, Rect rect, Paint paint, { TextDirection? textDirection }) {
    final BorderRadius adjustedBorderRadius = _adjustBorderRadius(rect);
    if (adjustedBorderRadius == BorderRadius.zero) {
      canvas.drawRect(rect, paint);
    } else {
      canvas.drawRRect(adjustedBorderRadius.toRRect(rect), paint);
    }
  }

  @override
  bool get preferPaintInterior => true;

435
  @override
436
  _StadiumToRoundedRectangleBorder copyWith({ BorderSide? side, BorderRadius? borderRadius, double? rectness }) {
437 438 439
    return _StadiumToRoundedRectangleBorder(
      side: side ?? this.side,
      borderRadius: borderRadius ?? this.borderRadius,
440
      rectness: rectness ?? this.rectness,
441 442 443
    );
  }

444
  @override
445
  void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
446 447 448 449
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
450 451 452
        final BorderRadius adjustedBorderRadius = _adjustBorderRadius(rect);
        final RRect borderRect = adjustedBorderRadius.resolve(textDirection).toRRect(rect);
        canvas.drawRRect(borderRect.inflate(side.strokeOffset / 2), side.toPaint());
453 454 455 456
    }
  }

  @override
457
  bool operator ==(Object other) {
458
    if (other.runtimeType != runtimeType) {
459
      return false;
460
    }
461 462 463 464
    return other is _StadiumToRoundedRectangleBorder
        && other.side == side
        && other.borderRadius == borderRadius
        && other.rectness == rectness;
465 466 467
  }

  @override
468
  int get hashCode => Object.hash(side, borderRadius, rectness);
469 470 471 472 473 474 475 476

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