rounded_rectangle_border.dart 11.2 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
  });
34 35

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

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

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

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

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

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

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

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

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

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

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

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

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

166
  final BorderRadiusGeometry borderRadius;
167
  final double circularity;
168
  final double eccentricity;
169 170 171

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

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

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

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

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

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

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

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

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

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

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

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

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