stadium_border.dart 12.4 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

6 7
import 'dart:ui' as ui show lerpDouble;

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

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
import 'basic_types.dart';
import 'border_radius.dart';
import 'borders.dart';
import 'circle_border.dart';
import 'edge_insets.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.
28
class StadiumBorder extends OutlinedBorder {
29 30 31
  /// Create a stadium border.
  ///
  /// The [side] argument must not be null.
32
  const StadiumBorder({ BorderSide side = BorderSide.none }) : assert(side != null), super(side: side);
33 34 35

  @override
  EdgeInsetsGeometry get dimensions {
36
    return EdgeInsets.all(side.width);
37 38 39
  }

  @override
40
  ShapeBorder scale(double t) => StadiumBorder(side: side.scale(t));
41 42

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

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

84
  @override
85
  StadiumBorder copyWith({ BorderSide? side }) {
86 87 88
    return StadiumBorder(side: side ?? this.side);
  }

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

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

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

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

  @override
  int get hashCode => side.hashCode;

  @override
  String toString() {
130
    return '${objectRuntimeType(this, 'StadiumBorder')}($side)';
131 132 133 134
  }
}

// Class to help with transitioning to/from a CircleBorder.
135
class _StadiumToCircleBorder extends OutlinedBorder {
136
  const _StadiumToCircleBorder({
137
    BorderSide side = BorderSide.none,
138
    this.circleness = 0.0,
139
  }) : assert(side != null),
140 141
       assert(circleness != null),
       super(side: side);
142 143 144 145 146

  final double circleness;

  @override
  EdgeInsetsGeometry get dimensions {
147
    return EdgeInsets.all(side.width);
148 149 150 151
  }

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

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

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

  Rect _adjustRect(Rect rect) {
    if (circleness == 0.0 || rect.width == rect.height)
      return rect;
    if (rect.width < rect.height) {
      final double delta = circleness * (rect.height - rect.width) / 2.0;
211
      return Rect.fromLTRB(
212 213 214 215 216 217 218
        rect.left,
        rect.top + delta,
        rect.right,
        rect.bottom - delta,
      );
    } else {
      final double delta = circleness * (rect.width - rect.height) / 2.0;
219
      return Rect.fromLTRB(
220 221 222 223 224 225 226 227 228
        rect.left + delta,
        rect.top,
        rect.right - delta,
        rect.bottom,
      );
    }
  }

  BorderRadius _adjustBorderRadius(Rect rect) {
229
    return BorderRadius.circular(rect.shortestSide / 2.0);
230 231 232
  }

  @override
233
  Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
234
    return Path()
235 236 237 238
      ..addRRect(_adjustBorderRadius(rect).toRRect(_adjustRect(rect)).deflate(side.width));
  }

  @override
239
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
240
    return Path()
241 242 243
      ..addRRect(_adjustBorderRadius(rect).toRRect(_adjustRect(rect)));
  }

244
  @override
245
  _StadiumToCircleBorder copyWith({ BorderSide? side, double? circleness }) {
246 247 248 249 250 251
    return _StadiumToCircleBorder(
      side: side ?? this.side,
      circleness: circleness ?? this.circleness,
    );
  }

252
  @override
253
  void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
254 255 256 257 258 259 260 261 262 263
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
        final double width = side.width;
        if (width == 0.0) {
          canvas.drawRRect(_adjustBorderRadius(rect).toRRect(_adjustRect(rect)), side.toPaint());
        } else {
          final RRect outer = _adjustBorderRadius(rect).toRRect(_adjustRect(rect));
          final RRect inner = outer.deflate(width);
264
          final Paint paint = Paint()
265 266 267 268 269 270 271
            ..color = side.color;
          canvas.drawDRRect(outer, inner, paint);
        }
    }
  }

  @override
272 273
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
274
      return false;
275 276 277
    return other is _StadiumToCircleBorder
        && other.side == side
        && other.circleness == circleness;
278 279 280 281 282 283 284 285 286 287 288 289 290
  }

  @override
  int get hashCode => hashValues(side, circleness);

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

// Class to help with transitioning to/from a RoundedRectBorder.
291
class _StadiumToRoundedRectangleBorder extends OutlinedBorder {
292
  const _StadiumToRoundedRectangleBorder({
293
    BorderSide side = BorderSide.none,
294 295
    this.borderRadius = BorderRadius.zero,
    this.rectness = 0.0,
296 297
  }) : assert(side != null),
       assert(borderRadius != null),
298 299
       assert(rectness != null),
       super(side: side);
300 301 302 303 304 305 306

  final BorderRadius borderRadius;

  final double rectness;

  @override
  EdgeInsetsGeometry get dimensions {
307
    return EdgeInsets.all(side.width);
308 309 310 311
  }

  @override
  ShapeBorder scale(double t) {
312
    return _StadiumToRoundedRectangleBorder(
313 314 315 316 317 318 319
      side: side.scale(t),
      borderRadius: borderRadius * t,
      rectness: t,
    );
  }

  @override
320
  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
321
    assert(t != null);
322
    if (a is StadiumBorder) {
323
      return _StadiumToRoundedRectangleBorder(
324 325 326 327 328 329
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: borderRadius,
        rectness: rectness * t,
      );
    }
    if (a is RoundedRectangleBorder) {
330
      return _StadiumToRoundedRectangleBorder(
331 332 333 334 335 336
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: borderRadius,
        rectness: rectness + (1.0 - rectness) * (1.0 - t),
      );
    }
    if (a is _StadiumToRoundedRectangleBorder) {
337
      return _StadiumToRoundedRectangleBorder(
338
        side: BorderSide.lerp(a.side, side, t),
339 340
        borderRadius: BorderRadius.lerp(a.borderRadius, borderRadius, t)!,
        rectness: ui.lerpDouble(a.rectness, rectness, t)!,
341 342 343 344 345 346
      );
    }
    return super.lerpFrom(a, t);
  }

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

  BorderRadius _adjustBorderRadius(Rect rect) {
    return BorderRadius.lerp(
      borderRadius,
376
      BorderRadius.all(Radius.circular(rect.shortestSide / 2.0)),
377
      1.0 - rectness,
378
    )!;
379 380 381
  }

  @override
382
  Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
383
    return Path()
384 385 386 387
      ..addRRect(_adjustBorderRadius(rect).toRRect(rect).deflate(side.width));
  }

  @override
388
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
389
    return Path()
390 391 392
      ..addRRect(_adjustBorderRadius(rect).toRRect(rect));
  }

393
  @override
394
  _StadiumToRoundedRectangleBorder copyWith({ BorderSide? side, BorderRadius? borderRadius, double? rectness }) {
395 396 397 398 399 400 401
    return _StadiumToRoundedRectangleBorder(
      side: side ?? this.side,
      borderRadius: borderRadius ?? this.borderRadius,
      rectness: rectness ?? this.rectness
    );
  }

402
  @override
403
  void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
404 405 406 407 408 409 410 411 412 413
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
        final double width = side.width;
        if (width == 0.0) {
          canvas.drawRRect(_adjustBorderRadius(rect).toRRect(rect), side.toPaint());
        } else {
          final RRect outer = _adjustBorderRadius(rect).toRRect(rect);
          final RRect inner = outer.deflate(width);
414
          final Paint paint = Paint()
415 416 417 418 419 420 421
            ..color = side.color;
          canvas.drawDRRect(outer, inner, paint);
        }
    }
  }

  @override
422 423
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
424
      return false;
425 426 427 428
    return other is _StadiumToRoundedRectangleBorder
        && other.side == side
        && other.borderRadius == borderRadius
        && other.rectness == rectness;
429 430 431 432 433 434 435 436 437 438 439 440
  }

  @override
  int get hashCode => hashValues(side, borderRadius, rectness);

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