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
        break;
137 138 139 140
    }
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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