rounded_rectangle_border.dart 11.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 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
  /// Creates a rounded rectangle border.
28
  const RoundedRectangleBorder({
29
    super.side,
30
    this.borderRadius = BorderRadius.zero,
31
  });
32 33

  /// The radii for each corner.
34
  final BorderRadiusGeometry borderRadius;
35 36 37

  @override
  ShapeBorder scale(double t) {
38
    return RoundedRectangleBorder(
39 40 41 42 43 44
      side: side.scale(t),
      borderRadius: borderRadius * t,
    );
  }

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

  @override
64
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
65
    if (b is RoundedRectangleBorder) {
66
      return RoundedRectangleBorder(
67
        side: BorderSide.lerp(side, b.side, t),
68
        borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
69 70 71
      );
    }
    if (b is CircleBorder) {
72
      return _RoundedRectangleToCircleBorder(
73 74
        side: BorderSide.lerp(side, b.side, t),
        borderRadius: borderRadius,
75
        circularity: t,
76
        eccentricity: b.eccentricity,
77 78 79 80 81
      );
    }
    return super.lerpTo(b, t);
  }

82 83 84
  /// Returns a copy of this RoundedRectangleBorder with the given fields
  /// replaced with the new values.
  @override
85
  RoundedRectangleBorder copyWith({ BorderSide? side, BorderRadiusGeometry? borderRadius }) {
86 87 88 89 90 91
    return RoundedRectangleBorder(
      side: side ?? this.side,
      borderRadius: borderRadius ?? this.borderRadius,
    );
  }

92
  @override
93
  Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
94
    final RRect borderRect = borderRadius.resolve(textDirection).toRRect(rect);
95
    final RRect adjustedRect = borderRect.deflate(side.strokeInset);
96
    return Path()
97
      ..addRRect(adjustedRect);
98 99 100
  }

  @override
101
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
102
    return Path()
103
      ..addRRect(borderRadius.resolve(textDirection).toRRect(rect));
104 105
  }

106 107 108 109 110 111 112 113 114 115 116 117
  @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;

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

  @override
138
  bool operator ==(Object other) {
139
    if (other.runtimeType != runtimeType) {
140
      return false;
141
    }
142 143 144
    return other is RoundedRectangleBorder
        && other.side == side
        && other.borderRadius == borderRadius;
145 146 147
  }

  @override
148
  int get hashCode => Object.hash(side, borderRadius);
149 150 151

  @override
  String toString() {
152
    return '${objectRuntimeType(this, 'RoundedRectangleBorder')}($side, $borderRadius)';
153 154 155
  }
}

156
class _RoundedRectangleToCircleBorder extends OutlinedBorder {
157
  const _RoundedRectangleToCircleBorder({
158
    super.side,
159
    this.borderRadius = BorderRadius.zero,
160
    required this.circularity,
161
    required this.eccentricity,
162
  });
163

164
  final BorderRadiusGeometry borderRadius;
165
  final double circularity;
166
  final double eccentricity;
167 168 169

  @override
  ShapeBorder scale(double t) {
170
    return _RoundedRectangleToCircleBorder(
171 172
      side: side.scale(t),
      borderRadius: borderRadius * t,
173
      circularity: t,
174
      eccentricity: eccentricity,
175 176 177 178
    );
  }

  @override
179
  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
180
    if (a is RoundedRectangleBorder) {
181
      return _RoundedRectangleToCircleBorder(
182
        side: BorderSide.lerp(a.side, side, t),
183
        borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!,
184
        circularity: circularity * t,
185
        eccentricity: eccentricity,
186 187 188
      );
    }
    if (a is CircleBorder) {
189
      return _RoundedRectangleToCircleBorder(
190 191
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: borderRadius,
192
        circularity: circularity + (1.0 - circularity) * (1.0 - t),
193
        eccentricity: a.eccentricity,
194 195 196
      );
    }
    if (a is _RoundedRectangleToCircleBorder) {
197
      return _RoundedRectangleToCircleBorder(
198
        side: BorderSide.lerp(a.side, side, t),
199
        borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!,
200
        circularity: ui.lerpDouble(a.circularity, circularity, t)!,
201
        eccentricity: eccentricity,
202 203 204 205 206 207
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
208
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
209
    if (b is RoundedRectangleBorder) {
210
      return _RoundedRectangleToCircleBorder(
211
        side: BorderSide.lerp(side, b.side, t),
212
        borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
213
        circularity: circularity * (1.0 - t),
214
        eccentricity: eccentricity,
215 216 217
      );
    }
    if (b is CircleBorder) {
218
      return _RoundedRectangleToCircleBorder(
219 220
        side: BorderSide.lerp(side, b.side, t),
        borderRadius: borderRadius,
221
        circularity: circularity + (1.0 - circularity) * t,
222
        eccentricity: b.eccentricity,
223 224 225
      );
    }
    if (b is _RoundedRectangleToCircleBorder) {
226
      return _RoundedRectangleToCircleBorder(
227
        side: BorderSide.lerp(side, b.side, t),
228
        borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
229
        circularity: ui.lerpDouble(circularity, b.circularity, t)!,
230
        eccentricity: eccentricity,
231 232 233 234 235 236
      );
    }
    return super.lerpTo(b, t);
  }

  Rect _adjustRect(Rect rect) {
237
    if (circularity == 0.0 || rect.width == rect.height) {
238
      return rect;
239
    }
240
    if (rect.width < rect.height) {
241
      final double partialDelta = (rect.height - rect.width) / 2;
242
      final double delta = circularity * partialDelta * (1.0 - eccentricity);
243
      return Rect.fromLTRB(
244 245 246 247 248 249
        rect.left,
        rect.top + delta,
        rect.right,
        rect.bottom - delta,
      );
    } else {
250
      final double partialDelta = (rect.width - rect.height) / 2;
251
      final double delta = circularity * partialDelta * (1.0 - eccentricity);
252
      return Rect.fromLTRB(
253 254 255 256 257 258 259 260
        rect.left + delta,
        rect.top,
        rect.right - delta,
        rect.bottom,
      );
    }
  }

261
  BorderRadius? _adjustBorderRadius(Rect rect, TextDirection? textDirection) {
262
    final BorderRadius resolvedRadius = borderRadius.resolve(textDirection);
263
    if (circularity == 0.0) {
264
      return resolvedRadius;
265
    }
266 267 268 269 270
    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)),
271
          circularity,
272 273 274 275 276
        )!;
      } else {
        return BorderRadius.lerp(
          resolvedRadius,
          BorderRadius.all(Radius.elliptical((0.5 + eccentricity / 2) * rect.width / 2, rect.height / 2)),
277
          circularity,
278 279 280
        )!;
      }
    }
281
    return BorderRadius.lerp(resolvedRadius, BorderRadius.circular(rect.shortestSide / 2), circularity);
282 283 284
  }

  @override
285
  Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
286
    final RRect borderRect = _adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect));
287
    final RRect adjustedRect = borderRect.deflate(ui.lerpDouble(side.width, 0, side.strokeAlign)!);
288
    return Path()
289
      ..addRRect(adjustedRect);
290 291 292
  }

  @override
293
  Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
294
    return Path()
295
      ..addRRect(_adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect)));
296 297
  }

298 299 300 301 302 303 304 305 306 307 308 309 310
  @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;

311
  @override
312
  _RoundedRectangleToCircleBorder copyWith({ BorderSide? side, BorderRadiusGeometry? borderRadius, double? circularity, double? eccentricity }) {
313 314 315
    return _RoundedRectangleToCircleBorder(
      side: side ?? this.side,
      borderRadius: borderRadius ?? this.borderRadius,
316
      circularity: circularity ?? this.circularity,
317
      eccentricity: eccentricity ?? this.eccentricity,
318 319 320
    );
  }

321
  @override
322
  void paint(Canvas canvas, Rect rect, { TextDirection? textDirection }) {
323 324 325 326
    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
327 328 329
        final BorderRadius adjustedBorderRadius = _adjustBorderRadius(rect, textDirection)!;
        final RRect borderRect = adjustedBorderRadius.toRRect(_adjustRect(rect));
        canvas.drawRRect(borderRect.inflate(side.strokeOffset / 2), side.toPaint());
330 331 332 333
    }
  }

  @override
334
  bool operator ==(Object other) {
335
    if (other.runtimeType != runtimeType) {
336
      return false;
337
    }
338 339 340
    return other is _RoundedRectangleToCircleBorder
        && other.side == side
        && other.borderRadius == borderRadius
341
        && other.circularity == circularity;
342 343 344
  }

  @override
345
  int get hashCode => Object.hash(side, borderRadius, circularity);
346 347 348

  @override
  String toString() {
349
    if (eccentricity != 0.0) {
350
      return 'RoundedRectangleBorder($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder that is ${(eccentricity * 100).toStringAsFixed(1)}% oval)';
351
    }
352
    return 'RoundedRectangleBorder($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder)';
353 354
  }
}