rounded_rectangle_border.dart 11.4 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
// 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';

/// 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.
26
class RoundedRectangleBorder extends OutlinedBorder {
27 28 29
  /// Creates a rounded rectangle border.
  ///
  /// The arguments must not be null.
30
  const RoundedRectangleBorder({
31
    super.side,
32
    this.borderRadius = BorderRadius.zero,
33
  }) : assert(side != null),
34
       assert(borderRadius != null);
35 36

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

  @override
  ShapeBorder scale(double t) {
41
    return RoundedRectangleBorder(
42 43 44 45 46 47
      side: side.scale(t),
      borderRadius: borderRadius * t,
    );
  }

  @override
48
  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
49
    assert(t != null);
50
    if (a is RoundedRectangleBorder) {
51
      return RoundedRectangleBorder(
52
        side: BorderSide.lerp(a.side, side, t),
53
        borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!,
54 55 56
      );
    }
    if (a is CircleBorder) {
57
      return _RoundedRectangleToCircleBorder(
58 59
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: borderRadius,
60
        circularity: 1.0 - t,
61
        eccentricity: a.eccentricity,
62 63 64 65 66 67
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
68
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
69
    assert(t != null);
70
    if (b is RoundedRectangleBorder) {
71
      return RoundedRectangleBorder(
72
        side: BorderSide.lerp(side, b.side, t),
73
        borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
74 75 76
      );
    }
    if (b is CircleBorder) {
77
      return _RoundedRectangleToCircleBorder(
78 79
        side: BorderSide.lerp(side, b.side, t),
        borderRadius: borderRadius,
80
        circularity: t,
81
        eccentricity: b.eccentricity,
82 83 84 85 86
      );
    }
    return super.lerpTo(b, t);
  }

87 88 89
  /// Returns a copy of this RoundedRectangleBorder with the given fields
  /// replaced with the new values.
  @override
90
  RoundedRectangleBorder copyWith({ BorderSide? side, BorderRadiusGeometry? borderRadius }) {
91 92 93 94 95 96
    return RoundedRectangleBorder(
      side: side ?? this.side,
      borderRadius: borderRadius ?? this.borderRadius,
    );
  }

97
  @override
98
  Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
99
    final RRect borderRect = borderRadius.resolve(textDirection).toRRect(rect);
100
    final RRect adjustedRect = borderRect.deflate(side.strokeInset);
101
    return Path()
102
      ..addRRect(adjustedRect);
103 104 105
  }

  @override
106
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
107
    return Path()
108
      ..addRRect(borderRadius.resolve(textDirection).toRRect(rect));
109 110
  }

111 112 113 114 115 116 117 118 119 120 121 122
  @override
  void paintInterior(Canvas canvas, Rect rect, Paint paint, { TextDirection? textDirection }) {
    if (borderRadius == BorderRadius.zero) {
      canvas.drawRect(rect, paint);
    } else {
      canvas.drawRRect(borderRadius.resolve(textDirection).toRRect(rect), paint);
    }
  }

  @override
  bool get preferPaintInterior => true;

123
  @override
124
  void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
125 126 127 128
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
129
        if (side.width == 0.0) {
130 131 132 133
          canvas.drawRRect(borderRadius.resolve(textDirection).toRRect(rect), side.toPaint());
        } else {
          final Paint paint = Paint()
            ..color = side.color;
134
          final RRect borderRect = borderRadius.resolve(textDirection).toRRect(rect);
135
          final RRect inner = borderRect.deflate(side.strokeInset);
136 137
          final RRect outer = borderRect.inflate(side.strokeOutset);
          canvas.drawDRRect(outer, inner, paint);
138
        }
139
        break;
140 141 142 143
    }
  }

  @override
144
  bool operator ==(Object other) {
145
    if (other.runtimeType != runtimeType) {
146
      return false;
147
    }
148 149 150
    return other is RoundedRectangleBorder
        && other.side == side
        && other.borderRadius == borderRadius;
151 152 153
  }

  @override
154
  int get hashCode => Object.hash(side, borderRadius);
155 156 157

  @override
  String toString() {
158
    return '${objectRuntimeType(this, 'RoundedRectangleBorder')}($side, $borderRadius)';
159 160 161
  }
}

162
class _RoundedRectangleToCircleBorder extends OutlinedBorder {
163
  const _RoundedRectangleToCircleBorder({
164
    super.side,
165
    this.borderRadius = BorderRadius.zero,
166
    required this.circularity,
167
    required this.eccentricity,
168 169
  }) : assert(side != null),
       assert(borderRadius != null),
170
       assert(circularity != null);
171

172
  final BorderRadiusGeometry borderRadius;
173
  final double circularity;
174
  final double eccentricity;
175 176 177

  @override
  ShapeBorder scale(double t) {
178
    return _RoundedRectangleToCircleBorder(
179 180
      side: side.scale(t),
      borderRadius: borderRadius * t,
181
      circularity: t,
182
      eccentricity: eccentricity,
183 184 185 186
    );
  }

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

  @override
217
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
218
    if (b is RoundedRectangleBorder) {
219
      return _RoundedRectangleToCircleBorder(
220
        side: BorderSide.lerp(side, b.side, t),
221
        borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
222
        circularity: circularity * (1.0 - t),
223
        eccentricity: eccentricity,
224 225 226
      );
    }
    if (b is CircleBorder) {
227
      return _RoundedRectangleToCircleBorder(
228 229
        side: BorderSide.lerp(side, b.side, t),
        borderRadius: borderRadius,
230
        circularity: circularity + (1.0 - circularity) * t,
231
        eccentricity: b.eccentricity,
232 233 234
      );
    }
    if (b is _RoundedRectangleToCircleBorder) {
235
      return _RoundedRectangleToCircleBorder(
236
        side: BorderSide.lerp(side, b.side, t),
237
        borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
238
        circularity: ui.lerpDouble(circularity, b.circularity, t)!,
239
        eccentricity: eccentricity,
240 241 242 243 244 245
      );
    }
    return super.lerpTo(b, t);
  }

  Rect _adjustRect(Rect rect) {
246
    if (circularity == 0.0 || rect.width == rect.height) {
247
      return rect;
248
    }
249
    if (rect.width < rect.height) {
250
      final double partialDelta = (rect.height - rect.width) / 2;
251
      final double delta = circularity * partialDelta * (1.0 - eccentricity);
252
      return Rect.fromLTRB(
253 254 255 256 257 258
        rect.left,
        rect.top + delta,
        rect.right,
        rect.bottom - delta,
      );
    } else {
259
      final double partialDelta = (rect.width - rect.height) / 2;
260
      final double delta = circularity * partialDelta * (1.0 - eccentricity);
261
      return Rect.fromLTRB(
262 263 264 265 266 267 268 269
        rect.left + delta,
        rect.top,
        rect.right - delta,
        rect.bottom,
      );
    }
  }

270
  BorderRadius? _adjustBorderRadius(Rect rect, TextDirection? textDirection) {
271
    final BorderRadius resolvedRadius = borderRadius.resolve(textDirection);
272
    if (circularity == 0.0) {
273
      return resolvedRadius;
274
    }
275 276 277 278 279
    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)),
280
          circularity,
281 282 283 284 285
        )!;
      } else {
        return BorderRadius.lerp(
          resolvedRadius,
          BorderRadius.all(Radius.elliptical((0.5 + eccentricity / 2) * rect.width / 2, rect.height / 2)),
286
          circularity,
287 288 289
        )!;
      }
    }
290
    return BorderRadius.lerp(resolvedRadius, BorderRadius.circular(rect.shortestSide / 2), circularity);
291 292 293
  }

  @override
294
  Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
295
    final RRect borderRect = _adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect));
296
    final RRect adjustedRect = borderRect.deflate(ui.lerpDouble(side.width, 0, side.strokeAlign)!);
297
    return Path()
298
      ..addRRect(adjustedRect);
299 300 301
  }

  @override
302
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
303
    return Path()
304
      ..addRRect(_adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect)));
305 306
  }

307 308 309 310 311 312 313 314 315 316 317 318 319
  @override
  void paintInterior(Canvas canvas, Rect rect, Paint paint, { TextDirection? textDirection }) {
    final BorderRadius adjustedBorderRadius = _adjustBorderRadius(rect, textDirection)!;
    if (adjustedBorderRadius == BorderRadius.zero) {
      canvas.drawRect(_adjustRect(rect), paint);
    } else {
      canvas.drawRRect(adjustedBorderRadius.toRRect(_adjustRect(rect)), paint);
    }
  }

  @override
  bool get preferPaintInterior => true;

320
  @override
321
  _RoundedRectangleToCircleBorder copyWith({ BorderSide? side, BorderRadiusGeometry? borderRadius, double? circularity, double? eccentricity }) {
322 323 324
    return _RoundedRectangleToCircleBorder(
      side: side ?? this.side,
      borderRadius: borderRadius ?? this.borderRadius,
325
      circularity: circularity ?? this.circularity,
326
      eccentricity: eccentricity ?? this.eccentricity,
327 328 329
    );
  }

330
  @override
331
  void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
332 333 334 335
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
336 337 338
        final BorderRadius adjustedBorderRadius = _adjustBorderRadius(rect, textDirection)!;
        final RRect borderRect = adjustedBorderRadius.toRRect(_adjustRect(rect));
        canvas.drawRRect(borderRect.inflate(side.strokeOffset / 2), side.toPaint());
339 340 341 342
    }
  }

  @override
343
  bool operator ==(Object other) {
344
    if (other.runtimeType != runtimeType) {
345
      return false;
346
    }
347 348 349
    return other is _RoundedRectangleToCircleBorder
        && other.side == side
        && other.borderRadius == borderRadius
350
        && other.circularity == circularity;
351 352 353
  }

  @override
354
  int get hashCode => Object.hash(side, borderRadius, circularity);
355 356 357

  @override
  String toString() {
358
    if (eccentricity != 0.0) {
359
      return 'RoundedRectangleBorder($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder that is ${(eccentricity * 100).toStringAsFixed(1)}% oval)';
360
    }
361
    return 'RoundedRectangleBorder($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder)';
362 363
  }
}