stadium_border.dart 11.7 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 26 27 28 29 30 31 32 33 34 35 36 37
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.
class StadiumBorder extends ShapeBorder {
  /// Create a stadium border.
  ///
  /// The [side] argument must not be null.
  const StadiumBorder({this.side = BorderSide.none}) : assert(side != null);

  /// The style of this border.
  final BorderSide side;

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

  @override
42
  ShapeBorder scale(double t) => StadiumBorder(side: side.scale(t));
43 44 45

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

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

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

  @override
  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
  }

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

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

  @override
  int get hashCode => side.hashCode;

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

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

  final BorderSide side;

  final double circleness;

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

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

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

  @override
  ShapeBorder lerpTo(ShapeBorder b, double t) {
182
    assert(t != null);
183
    if (b is StadiumBorder) {
184
      return _StadiumToCircleBorder(
185 186 187 188 189
        side: BorderSide.lerp(side, b.side, t),
        circleness: circleness * (1.0 - t),
      );
    }
    if (b is CircleBorder) {
190
      return _StadiumToCircleBorder(
191 192 193 194 195
        side: BorderSide.lerp(side, b.side, t),
        circleness: circleness + (1.0 - circleness) * t,
      );
    }
    if (b is _StadiumToCircleBorder) {
196
      return _StadiumToCircleBorder(
197 198 199 200 201 202 203 204 205 206 207 208
        side: BorderSide.lerp(side, b.side, t),
        circleness: ui.lerpDouble(circleness, b.circleness, t),
      );
    }
    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;
209
      return Rect.fromLTRB(
210 211 212 213 214 215 216
        rect.left,
        rect.top + delta,
        rect.right,
        rect.bottom - delta,
      );
    } else {
      final double delta = circleness * (rect.width - rect.height) / 2.0;
217
      return Rect.fromLTRB(
218 219 220 221 222 223 224 225 226
        rect.left + delta,
        rect.top,
        rect.right - delta,
        rect.bottom,
      );
    }
  }

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

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

  @override
  Path getOuterPath(Rect rect, { TextDirection textDirection }) {
238
    return Path()
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
      ..addRRect(_adjustBorderRadius(rect).toRRect(_adjustRect(rect)));
  }

  @override
  void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) {
    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);
254
          final Paint paint = Paint()
255 256 257 258 259 260 261
            ..color = side.color;
          canvas.drawDRRect(outer, inner, paint);
        }
    }
  }

  @override
262 263
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
264
      return false;
265 266 267
    return other is _StadiumToCircleBorder
        && other.side == side
        && other.circleness == circleness;
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
  }

  @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.
class _StadiumToRoundedRectangleBorder extends ShapeBorder {
  const _StadiumToRoundedRectangleBorder({
283 284 285
    this.side = BorderSide.none,
    this.borderRadius = BorderRadius.zero,
    this.rectness = 0.0,
286 287 288 289 290 291 292 293 294 295 296 297
  }) : assert(side != null),
       assert(borderRadius != null),
       assert(rectness != null);

  final BorderSide side;

  final BorderRadius borderRadius;

  final double rectness;

  @override
  EdgeInsetsGeometry get dimensions {
298
    return EdgeInsets.all(side.width);
299 300 301 302
  }

  @override
  ShapeBorder scale(double t) {
303
    return _StadiumToRoundedRectangleBorder(
304 305 306 307 308 309 310 311
      side: side.scale(t),
      borderRadius: borderRadius * t,
      rectness: t,
    );
  }

  @override
  ShapeBorder lerpFrom(ShapeBorder a, double t) {
312
    assert(t != null);
313
    if (a is StadiumBorder) {
314
      return _StadiumToRoundedRectangleBorder(
315 316 317 318 319 320
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: borderRadius,
        rectness: rectness * t,
      );
    }
    if (a is RoundedRectangleBorder) {
321
      return _StadiumToRoundedRectangleBorder(
322 323 324 325 326 327
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: borderRadius,
        rectness: rectness + (1.0 - rectness) * (1.0 - t),
      );
    }
    if (a is _StadiumToRoundedRectangleBorder) {
328
      return _StadiumToRoundedRectangleBorder(
329 330 331 332 333 334 335 336 337 338
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: BorderRadius.lerp(a.borderRadius, borderRadius, t),
        rectness: ui.lerpDouble(a.rectness, rectness, t),
      );
    }
    return super.lerpFrom(a, t);
  }

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

  BorderRadius _adjustBorderRadius(Rect rect) {
    return BorderRadius.lerp(
      borderRadius,
367
      BorderRadius.all(Radius.circular(rect.shortestSide / 2.0)),
368
      1.0 - rectness,
369 370 371 372 373
    );
  }

  @override
  Path getInnerPath(Rect rect, { TextDirection textDirection }) {
374
    return Path()
375 376 377 378 379
      ..addRRect(_adjustBorderRadius(rect).toRRect(rect).deflate(side.width));
  }

  @override
  Path getOuterPath(Rect rect, { TextDirection textDirection }) {
380
    return Path()
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
      ..addRRect(_adjustBorderRadius(rect).toRRect(rect));
  }

  @override
  void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) {
    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);
396
          final Paint paint = Paint()
397 398 399 400 401 402 403
            ..color = side.color;
          canvas.drawDRRect(outer, inner, paint);
        }
    }
  }

  @override
404 405
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType)
406
      return false;
407 408 409 410
    return other is _StadiumToRoundedRectangleBorder
        && other.side == side
        && other.borderRadius == borderRadius
        && other.rectness == rectness;
411 412 413 414 415 416 417 418 419 420 421 422
  }

  @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)';
  }
}