rounded_rectangle_border.dart 12.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
// 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;

import 'package:flutter/foundation.dart';

import 'basic_types.dart';
import 'border_radius.dart';
import 'borders.dart';
import 'circle_border.dart';
import 'edge_insets.dart';

/// A rectangular border with rounded corners.
///
/// Typically used with [ShapeDecoration] to draw a box with a rounded
/// rectangle.
///
/// This shape can interpolate to and from [CircleBorder].
///
/// See also:
///
///  * [BorderSide], which is used to describe each side of the box.
///  * [Border], which, when used with [BoxDecoration], can also
///    describe a rounded rectangle.
27
class RoundedRectangleBorder extends OutlinedBorder {
28 29 30
  /// Creates a rounded rectangle border.
  ///
  /// The arguments must not be null.
31
  const RoundedRectangleBorder({
32
    super.side,
33
    this.borderRadius = BorderRadius.zero,
34
  }) : assert(side != null),
35
       assert(borderRadius != null);
36 37

  /// The radii for each corner.
38
  final BorderRadiusGeometry borderRadius;
39 40 41

  @override
  EdgeInsetsGeometry get dimensions {
42 43 44 45 46 47 48 49
    switch (side.strokeAlign) {
      case StrokeAlign.inside:
        return EdgeInsets.all(side.width);
      case StrokeAlign.center:
        return EdgeInsets.all(side.width / 2);
      case StrokeAlign.outside:
        return EdgeInsets.zero;
    }
50 51 52 53
  }

  @override
  ShapeBorder scale(double t) {
54
    return RoundedRectangleBorder(
55 56 57 58 59 60
      side: side.scale(t),
      borderRadius: borderRadius * t,
    );
  }

  @override
61
  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
62
    assert(t != null);
63
    if (a is RoundedRectangleBorder) {
64
      return RoundedRectangleBorder(
65
        side: BorderSide.lerp(a.side, side, t),
66
        borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!,
67 68 69
      );
    }
    if (a is CircleBorder) {
70
      return _RoundedRectangleToCircleBorder(
71 72 73
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: borderRadius,
        circleness: 1.0 - t,
74
        eccentricity: a.eccentricity,
75 76 77 78 79 80
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
81
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
82
    assert(t != null);
83
    if (b is RoundedRectangleBorder) {
84
      return RoundedRectangleBorder(
85
        side: BorderSide.lerp(side, b.side, t),
86
        borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
87 88 89
      );
    }
    if (b is CircleBorder) {
90
      return _RoundedRectangleToCircleBorder(
91 92 93
        side: BorderSide.lerp(side, b.side, t),
        borderRadius: borderRadius,
        circleness: t,
94
        eccentricity: b.eccentricity,
95 96 97 98 99
      );
    }
    return super.lerpTo(b, t);
  }

100 101 102
  /// Returns a copy of this RoundedRectangleBorder with the given fields
  /// replaced with the new values.
  @override
103
  RoundedRectangleBorder copyWith({ BorderSide? side, BorderRadiusGeometry? borderRadius }) {
104 105 106 107 108 109
    return RoundedRectangleBorder(
      side: side ?? this.side,
      borderRadius: borderRadius ?? this.borderRadius,
    );
  }

110
  @override
111
  Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
112 113 114 115 116 117 118 119 120 121 122 123 124
    final RRect borderRect = borderRadius.resolve(textDirection).toRRect(rect);
    final RRect adjustedRect;
    switch (side.strokeAlign) {
      case StrokeAlign.inside:
        adjustedRect = borderRect.deflate(side.width);
        break;
      case StrokeAlign.center:
        adjustedRect = borderRect.deflate(side.width / 2);
        break;
      case StrokeAlign.outside:
        adjustedRect = borderRect;
        break;
    }
125
    return Path()
126
      ..addRRect(adjustedRect);
127 128 129
  }

  @override
130
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
131
    return Path()
132
      ..addRRect(borderRadius.resolve(textDirection).toRRect(rect));
133 134 135
  }

  @override
136
  void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
137 138 139 140 141 142
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
        final double width = side.width;
        if (width == 0.0) {
143
          canvas.drawRRect(borderRadius.resolve(textDirection).toRRect(rect), side.toPaint());
144
        } else {
145
          final Paint paint = Paint()
146
            ..color = side.color;
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
          if (side.strokeAlign == StrokeAlign.inside) {
            final RRect outer = borderRadius.resolve(textDirection).toRRect(rect);
            final RRect inner = outer.deflate(width);
            canvas.drawDRRect(outer, inner, paint);
          } else {
            final Rect inner;
            final Rect outer;
            if (side.strokeAlign == StrokeAlign.center) {
              inner = rect.deflate(width / 2);
              outer = rect.inflate(width / 2);
            } else {
              inner = rect;
              outer = rect.inflate(width);
            }
            final BorderRadius borderRadiusResolved = borderRadius.resolve(textDirection);
            canvas.drawDRRect(borderRadiusResolved.toRRect(outer), borderRadiusResolved.toRRect(inner), paint);
163
        }
164
      }
165 166 167 168
    }
  }

  @override
169
  bool operator ==(Object other) {
170
    if (other.runtimeType != runtimeType) {
171
      return false;
172
    }
173 174 175
    return other is RoundedRectangleBorder
        && other.side == side
        && other.borderRadius == borderRadius;
176 177 178
  }

  @override
179
  int get hashCode => Object.hash(side, borderRadius);
180 181 182

  @override
  String toString() {
183
    return '${objectRuntimeType(this, 'RoundedRectangleBorder')}($side, $borderRadius)';
184 185 186
  }
}

187
class _RoundedRectangleToCircleBorder extends OutlinedBorder {
188
  const _RoundedRectangleToCircleBorder({
189
    super.side,
190
    this.borderRadius = BorderRadius.zero,
191
    required this.circleness,
192
    required this.eccentricity,
193 194
  }) : assert(side != null),
       assert(borderRadius != null),
195
       assert(circleness != null);
196

197
  final BorderRadiusGeometry borderRadius;
198
  final double circleness;
199
  final double eccentricity;
200 201 202

  @override
  EdgeInsetsGeometry get dimensions {
203 204 205 206 207 208 209 210
    switch (side.strokeAlign) {
      case StrokeAlign.inside:
        return EdgeInsets.all(side.width);
      case StrokeAlign.center:
        return EdgeInsets.all(side.width / 2);
      case StrokeAlign.outside:
        return EdgeInsets.zero;
    }
211 212 213 214
  }

  @override
  ShapeBorder scale(double t) {
215
    return _RoundedRectangleToCircleBorder(
216 217 218
      side: side.scale(t),
      borderRadius: borderRadius * t,
      circleness: t,
219
      eccentricity: eccentricity,
220 221 222 223
    );
  }

  @override
224
  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
225
    assert(t != null);
226
    if (a is RoundedRectangleBorder) {
227
      return _RoundedRectangleToCircleBorder(
228
        side: BorderSide.lerp(a.side, side, t),
229
        borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!,
230
        circleness: circleness * t,
231
        eccentricity: eccentricity,
232 233 234
      );
    }
    if (a is CircleBorder) {
235
      return _RoundedRectangleToCircleBorder(
236 237 238
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: borderRadius,
        circleness: circleness + (1.0 - circleness) * (1.0 - t),
239
        eccentricity: a.eccentricity,
240 241 242
      );
    }
    if (a is _RoundedRectangleToCircleBorder) {
243
      return _RoundedRectangleToCircleBorder(
244
        side: BorderSide.lerp(a.side, side, t),
245 246
        borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!,
        circleness: ui.lerpDouble(a.circleness, circleness, t)!,
247
        eccentricity: eccentricity,
248 249 250 251 252 253
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
254
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
255
    if (b is RoundedRectangleBorder) {
256
      return _RoundedRectangleToCircleBorder(
257
        side: BorderSide.lerp(side, b.side, t),
258
        borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
259
        circleness: circleness * (1.0 - t),
260
        eccentricity: eccentricity,
261 262 263
      );
    }
    if (b is CircleBorder) {
264
      return _RoundedRectangleToCircleBorder(
265 266 267
        side: BorderSide.lerp(side, b.side, t),
        borderRadius: borderRadius,
        circleness: circleness + (1.0 - circleness) * t,
268
        eccentricity: b.eccentricity,
269 270 271
      );
    }
    if (b is _RoundedRectangleToCircleBorder) {
272
      return _RoundedRectangleToCircleBorder(
273
        side: BorderSide.lerp(side, b.side, t),
274 275
        borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
        circleness: ui.lerpDouble(circleness, b.circleness, t)!,
276
        eccentricity: eccentricity,
277 278 279 280 281 282
      );
    }
    return super.lerpTo(b, t);
  }

  Rect _adjustRect(Rect rect) {
283
    if (circleness == 0.0 || rect.width == rect.height) {
284
      return rect;
285
    }
286
    if (rect.width < rect.height) {
287 288
      final double partialDelta = (rect.height - rect.width) / 2;
      final double delta = circleness * partialDelta * (1.0 - eccentricity);
289
      return Rect.fromLTRB(
290 291 292 293 294 295
        rect.left,
        rect.top + delta,
        rect.right,
        rect.bottom - delta,
      );
    } else {
296 297
      final double partialDelta = (rect.width - rect.height) / 2;
      final double delta = circleness * partialDelta * (1.0 - eccentricity);
298
      return Rect.fromLTRB(
299 300 301 302 303 304 305 306
        rect.left + delta,
        rect.top,
        rect.right - delta,
        rect.bottom,
      );
    }
  }

307
  BorderRadius? _adjustBorderRadius(Rect rect, TextDirection? textDirection) {
308
    final BorderRadius resolvedRadius = borderRadius.resolve(textDirection);
309
    if (circleness == 0.0) {
310
      return resolvedRadius;
311
    }
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
    if (eccentricity != 0.0) {
      if (rect.width < rect.height) {
        return BorderRadius.lerp(
          resolvedRadius,
          BorderRadius.all(Radius.elliptical(rect.width / 2, (0.5 + eccentricity / 2) * rect.height / 2)),
          circleness,
        )!;
      } else {
        return BorderRadius.lerp(
          resolvedRadius,
          BorderRadius.all(Radius.elliptical((0.5 + eccentricity / 2) * rect.width / 2, rect.height / 2)),
          circleness,
        )!;
      }
    }
    return BorderRadius.lerp(resolvedRadius, BorderRadius.circular(rect.shortestSide / 2), circleness);
328 329 330
  }

  @override
331
  Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
332 333 334 335 336 337 338 339 340 341 342 343 344
    final RRect borderRect = _adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect));
    final RRect adjustedRect;
    switch (side.strokeAlign) {
      case StrokeAlign.inside:
        adjustedRect = borderRect.deflate(side.width);
        break;
      case StrokeAlign.center:
        adjustedRect = borderRect.deflate(side.width / 2);
        break;
      case StrokeAlign.outside:
        adjustedRect = borderRect;
        break;
    }
345
    return Path()
346
      ..addRRect(adjustedRect);
347 348 349
  }

  @override
350
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
351
    return Path()
352
      ..addRRect(_adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect)));
353 354
  }

355
  @override
356
  _RoundedRectangleToCircleBorder copyWith({ BorderSide? side, BorderRadiusGeometry? borderRadius, double? circleness, double? eccentricity }) {
357 358 359 360
    return _RoundedRectangleToCircleBorder(
      side: side ?? this.side,
      borderRadius: borderRadius ?? this.borderRadius,
      circleness: circleness ?? this.circleness,
361
      eccentricity: eccentricity ?? this.eccentricity,
362 363 364
    );
  }

365
  @override
366
  void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
367 368 369 370 371 372
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
        final double width = side.width;
        if (width == 0.0) {
373
          canvas.drawRRect(_adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect)), side.toPaint());
374
        } else {
375 376 377 378 379 380 381 382 383 384 385 386 387 388
          final RRect borderRect = _adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect));
          final RRect adjustedRect;
          switch (side.strokeAlign) {
            case StrokeAlign.inside:
              adjustedRect = borderRect.deflate(width / 2);
              break;
            case StrokeAlign.center:
              adjustedRect = borderRect;
              break;
            case StrokeAlign.outside:
              adjustedRect = borderRect.inflate(width / 2);
              break;
          }
          canvas.drawRRect(adjustedRect, side.toPaint());
389 390 391 392 393
        }
    }
  }

  @override
394
  bool operator ==(Object other) {
395
    if (other.runtimeType != runtimeType) {
396
      return false;
397
    }
398 399 400 401
    return other is _RoundedRectangleToCircleBorder
        && other.side == side
        && other.borderRadius == borderRadius
        && other.circleness == circleness;
402 403 404
  }

  @override
405
  int get hashCode => Object.hash(side, borderRadius, circleness);
406 407 408

  @override
  String toString() {
409 410 411
    if (eccentricity != 0.0) {
      return 'RoundedRectangleBorder($side, $borderRadius, ${(circleness * 100).toStringAsFixed(1)}% of the way to being a CircleBorder that is ${(eccentricity * 100).toStringAsFixed(1)}% oval)';
    }
412 413 414
    return 'RoundedRectangleBorder($side, $borderRadius, ${(circleness * 100).toStringAsFixed(1)}% of the way to being a CircleBorder)';
  }
}