Skip to content

Commit bae1ac2

Browse files
authored
ImageDecoration.lerp (flutter#130533)
This primarily implements DecorationImage.lerp(). It also makes some minor tweaks, the main one of which is defering to dart:ui for `clampDouble` instead of duplicating it in package:foundation. Fixes flutter#12452
1 parent 5527009 commit bae1ac2

File tree

11 files changed

+612
-59
lines changed

11 files changed

+612
-59
lines changed

analysis_options.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ linter:
5050
- avoid_field_initializers_in_const_classes
5151
# - avoid_final_parameters # incompatible with prefer_final_parameters
5252
- avoid_function_literals_in_foreach_calls
53-
- avoid_implementing_value_types
53+
# - avoid_implementing_value_types # see https://github.com/dart-lang/linter/issues/4558
5454
- avoid_init_to_null
5555
- avoid_js_rounded_ints
5656
# - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to

packages/flutter/lib/foundation.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export 'src/foundation/diagnostics.dart';
3434
export 'src/foundation/isolates.dart';
3535
export 'src/foundation/key.dart';
3636
export 'src/foundation/licenses.dart';
37-
export 'src/foundation/math.dart';
3837
export 'src/foundation/memory_allocations.dart';
3938
export 'src/foundation/node.dart';
4039
export 'src/foundation/object.dart';

packages/flutter/lib/src/foundation/README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ nothing but core Dart packages. They can't depend on `dart:ui`, they
33
can't depend on any `package:`, and they can't depend on anything
44
outside this directory.
55

6-
Currently they do depend on dart:ui, but only for `VoidCallback` (and
7-
maybe one day `lerpDouble`), which are all intended to be moved out
8-
of `dart:ui` and into `dart:core`.
6+
Currently they do depend on dart:ui, but only for `VoidCallback` and
7+
`clampDouble` (and maybe one day `lerpDouble`), which are all intended
8+
to be moved out of `dart:ui` and into `dart:core`.
99

1010
There is currently also an unfortunate dependency on the platform
1111
dispatcher logic (SingletonFlutterWindow, Brightness,
@@ -14,5 +14,4 @@ PlatformDispatcher, window), though that should probably move to the
1414

1515
See also:
1616

17-
* https://github.com/dart-lang/sdk/issues/27791 (`VoidCallback`)
18-
* https://github.com/dart-lang/sdk/issues/25217 (`hashValues`, `hashList`, and `lerpDouble`)
17+
* https://github.com/dart-lang/sdk/issues/25217

packages/flutter/lib/src/foundation/binding.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import 'print.dart';
2222
import 'service_extensions.dart';
2323
import 'timeline.dart';
2424

25-
export 'dart:ui' show PlatformDispatcher, SingletonFlutterWindow; // ignore: deprecated_member_use
25+
export 'dart:ui' show PlatformDispatcher, SingletonFlutterWindow, clampDouble; // ignore: deprecated_member_use
2626

2727
export 'basic_types.dart' show AsyncCallback, AsyncValueGetter, AsyncValueSetter;
2828

packages/flutter/lib/src/foundation/diagnostics.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
// found in the LICENSE file.
44

55
import 'dart:math' as math;
6+
import 'dart:ui' show clampDouble;
67

78
import 'package:meta/meta.dart';
89

910
import 'assertions.dart';
1011
import 'constants.dart';
1112
import 'debug.dart';
12-
import 'math.dart' show clampDouble;
1313
import 'object.dart';
1414

1515
// Examples can assume:

packages/flutter/lib/src/foundation/math.dart

Lines changed: 0 additions & 23 deletions
This file was deleted.

packages/flutter/lib/src/material/dialog.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'dart:ui';
5+
import 'dart:ui' show clampDouble, lerpDouble;
66

77
import 'package:flutter/cupertino.dart';
8-
import 'package:flutter/foundation.dart' show clampDouble;
98

109
import 'color_scheme.dart';
1110
import 'colors.dart';

packages/flutter/lib/src/painting/box_decoration.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ class BoxDecoration extends Decoration {
232232
BoxDecoration scale(double factor) {
233233
return BoxDecoration(
234234
color: Color.lerp(null, color, factor),
235-
image: image, // TODO(ianh): fade the image from transparent
235+
image: DecorationImage.lerp(null, image, factor),
236236
border: BoxBorder.lerp(null, border, factor),
237237
borderRadius: BorderRadiusGeometry.lerp(null, borderRadius, factor),
238238
boxShadow: BoxShadow.lerpList(null, boxShadow, factor),
@@ -307,7 +307,7 @@ class BoxDecoration extends Decoration {
307307
}
308308
return BoxDecoration(
309309
color: Color.lerp(a.color, b.color, t),
310-
image: t < 0.5 ? a.image : b.image, // TODO(ianh): cross-fade the image
310+
image: DecorationImage.lerp(a.image, b.image, t),
311311
border: BoxBorder.lerp(a.border, b.border, t),
312312
borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, b.borderRadius, t),
313313
boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t),

packages/flutter/lib/src/painting/decoration_image.dart

Lines changed: 161 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ class DecorationImage {
177177
/// image needs to be repainted, e.g. because it is loading incrementally or
178178
/// because it is animated.
179179
DecorationImagePainter createPainter(VoidCallback onChanged) {
180-
return DecorationImagePainter._(this, onChanged);
180+
return _DecorationImagePainter._(this, onChanged);
181181
}
182182

183183
@override
@@ -246,6 +246,28 @@ class DecorationImage {
246246
];
247247
return '${objectRuntimeType(this, 'DecorationImage')}(${properties.join(", ")})';
248248
}
249+
250+
/// Linearly interpolates between two [DecorationImage]s.
251+
///
252+
/// The `t` argument represents position on the timeline, with 0.0 meaning
253+
/// that the interpolation has not started, returning `a`, 1.0 meaning that
254+
/// the interpolation has finished, returning `b`, and values in between
255+
/// meaning that the interpolation is at the relevant point on the timeline
256+
/// between `a` and `this`. The interpolation can be extrapolated beyond 0.0
257+
/// and 1.0, so negative values and values greater than 1.0 are valid (and can
258+
/// easily be generated by curves such as [Curves.elasticInOut]).
259+
///
260+
/// Values for `t` are usually obtained from an [Animation<double>], such as
261+
/// an [AnimationController].
262+
static DecorationImage? lerp(DecorationImage? a, DecorationImage? b, double t) {
263+
if (identical(a, b) || t == 0.0) {
264+
return a;
265+
}
266+
if (t == 1.0) {
267+
return b;
268+
}
269+
return _BlendedDecorationImage(a, b, t);
270+
}
249271
}
250272

251273
/// The painter for a [DecorationImage].
@@ -259,15 +281,7 @@ class DecorationImage {
259281
///
260282
/// This object should be disposed using the [dispose] method when it is no
261283
/// longer needed.
262-
class DecorationImagePainter {
263-
DecorationImagePainter._(this._details, this._onChanged);
264-
265-
final DecorationImage _details;
266-
final VoidCallback _onChanged;
267-
268-
ImageStream? _imageStream;
269-
ImageInfo? _image;
270-
284+
abstract interface class DecorationImagePainter {
271285
/// Draw the image onto the given canvas.
272286
///
273287
/// The image is drawn at the position and size given by the `rect` argument.
@@ -282,8 +296,34 @@ class DecorationImagePainter {
282296
/// because it had not yet been loaded the first time this method was called,
283297
/// then the `onChanged` callback passed to [DecorationImage.createPainter]
284298
/// will be called.
285-
void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration) {
299+
///
300+
/// The `blend` argument specifies the opacity that should be applied to the
301+
/// image due to this image being blended with another. The `blendMode`
302+
/// argument can be specified to override the [DecorationImagePainter]'s
303+
/// default [BlendMode] behavior. It is usually set to [BlendMode.srcOver] if
304+
/// this is the first or only image being blended, and [BlendMode.plus] if it
305+
/// is being blended with an image below.
306+
void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration, { double blend = 1.0, BlendMode blendMode = BlendMode.srcOver });
286307

308+
/// Releases the resources used by this painter.
309+
///
310+
/// This should be called whenever the painter is no longer needed.
311+
///
312+
/// After this method has been called, the object is no longer usable.
313+
void dispose();
314+
}
315+
316+
class _DecorationImagePainter implements DecorationImagePainter {
317+
_DecorationImagePainter._(this._details, this._onChanged);
318+
319+
final DecorationImage _details;
320+
final VoidCallback _onChanged;
321+
322+
ImageStream? _imageStream;
323+
ImageInfo? _image;
324+
325+
@override
326+
void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration, { double blend = 1.0, BlendMode blendMode = BlendMode.srcOver }) {
287327
bool flipHorizontally = false;
288328
if (_details.matchTextDirection) {
289329
assert(() {
@@ -338,10 +378,11 @@ class DecorationImagePainter {
338378
centerSlice: _details.centerSlice,
339379
repeat: _details.repeat,
340380
flipHorizontally: flipHorizontally,
341-
opacity: _details.opacity,
381+
opacity: _details.opacity * blend,
342382
filterQuality: _details.filterQuality,
343383
invertColors: _details.invertColors,
344384
isAntiAlias: _details.isAntiAlias,
385+
blendMode: blendMode,
345386
);
346387

347388
if (clipPath != null) {
@@ -364,12 +405,7 @@ class DecorationImagePainter {
364405
}
365406
}
366407

367-
/// Releases the resources used by this painter.
368-
///
369-
/// This should be called whenever the painter is no longer needed.
370-
///
371-
/// After this method has been called, the object is no longer usable.
372-
@mustCallSuper
408+
@override
373409
void dispose() {
374410
_imageStream?.removeListener(ImageStreamListener(
375411
_handleImage,
@@ -444,7 +480,7 @@ void debugFlushLastFrameImageSizeInfo() {
444480
/// corners of the destination rectangle defined by applying `fit`. The
445481
/// remaining five regions are drawn by stretching them to fit such that they
446482
/// exactly cover the destination rectangle while maintaining their relative
447-
/// positions.
483+
/// positions. See also [Canvas.drawImageNine].
448484
///
449485
/// * `repeat`: If the image does not fill `rect`, whether and how the image
450486
/// should be repeated to fill `rect`. By default, the image is not repeated.
@@ -490,6 +526,7 @@ void paintImage({
490526
bool invertColors = false,
491527
FilterQuality filterQuality = FilterQuality.low,
492528
bool isAntiAlias = false,
529+
BlendMode blendMode = BlendMode.srcOver,
493530
}) {
494531
assert(
495532
image.debugGetOpenHandleStackTraces()?.isNotEmpty ?? true,
@@ -530,9 +567,10 @@ void paintImage({
530567
if (colorFilter != null) {
531568
paint.colorFilter = colorFilter;
532569
}
533-
paint.color = Color.fromRGBO(0, 0, 0, opacity);
570+
paint.color = Color.fromRGBO(0, 0, 0, clampDouble(opacity, 0.0, 1.0));
534571
paint.filterQuality = filterQuality;
535572
paint.invertColors = invertColors;
573+
paint.blendMode = blendMode;
536574
final double halfWidthDelta = (outputSize.width - destinationSize.width) / 2.0;
537575
final double halfHeightDelta = (outputSize.height - destinationSize.height) / 2.0;
538576
final double dx = halfWidthDelta + (flipHorizontally ? -alignment.x : alignment.x) * halfWidthDelta;
@@ -543,6 +581,12 @@ void paintImage({
543581
// Set to true if we added a saveLayer to the canvas to invert/flip the image.
544582
bool invertedCanvas = false;
545583
// Output size and destination rect are fully calculated.
584+
585+
// Implement debug-mode and profile-mode features:
586+
// - cacheWidth/cacheHeight warning
587+
// - debugInvertOversizedImages
588+
// - debugOnPaintImage
589+
// - Flutter.ImageSizesForFrame events in timeline
546590
if (!kReleaseMode) {
547591
// We can use the devicePixelRatio of the views directly here (instead of
548592
// going through a MediaQuery) because if it changes, whatever is aware of
@@ -554,7 +598,6 @@ void paintImage({
554598
0.0,
555599
(double previousValue, ui.FlutterView view) => math.max(previousValue, view.devicePixelRatio),
556600
);
557-
558601
final ImageSizeInfo sizeInfo = ImageSizeInfo(
559602
// Some ImageProvider implementations may not have given this.
560603
source: debugImageLabel ?? '<Unknown Image(${image.width}×${image.height})>',
@@ -599,7 +642,7 @@ void paintImage({
599642
return true;
600643
}());
601644
// Avoid emitting events that are the same as those emitted in the last frame.
602-
if (!kReleaseMode && !_lastFrameImageSizeInfo.contains(sizeInfo)) {
645+
if (!_lastFrameImageSizeInfo.contains(sizeInfo)) {
603646
final ImageSizeInfo? existingSizeInfo = _pendingImageSizeInfo[sizeInfo.source];
604647
if (existingSizeInfo == null || existingSizeInfo.displaySizeInBytes < sizeInfo.displaySizeInBytes) {
605648
_pendingImageSizeInfo[sizeInfo.source!] = sizeInfo;
@@ -691,3 +734,99 @@ Iterable<Rect> _generateImageTileRects(Rect outputRect, Rect fundamentalRect, Im
691734
}
692735

693736
Rect _scaleRect(Rect rect, double scale) => Rect.fromLTRB(rect.left * scale, rect.top * scale, rect.right * scale, rect.bottom * scale);
737+
738+
// Implements DecorationImage.lerp when the image is different.
739+
//
740+
// This class just paints both decorations on top of each other, blended together.
741+
//
742+
// The Decoration properties are faked by just forwarded to the target image.
743+
class _BlendedDecorationImage implements DecorationImage {
744+
const _BlendedDecorationImage(this.a, this.b, this.t) : assert(a != null || b != null);
745+
746+
final DecorationImage? a;
747+
final DecorationImage? b;
748+
final double t;
749+
750+
@override
751+
ImageProvider get image => b?.image ?? a!.image;
752+
@override
753+
ImageErrorListener? get onError => b?.onError ?? a!.onError;
754+
@override
755+
ColorFilter? get colorFilter => b?.colorFilter ?? a!.colorFilter;
756+
@override
757+
BoxFit? get fit => b?.fit ?? a!.fit;
758+
@override
759+
AlignmentGeometry get alignment => b?.alignment ?? a!.alignment;
760+
@override
761+
Rect? get centerSlice => b?.centerSlice ?? a!.centerSlice;
762+
@override
763+
ImageRepeat get repeat => b?.repeat ?? a!.repeat;
764+
@override
765+
bool get matchTextDirection => b?.matchTextDirection ?? a!.matchTextDirection;
766+
@override
767+
double get scale => b?.scale ?? a!.scale;
768+
@override
769+
double get opacity => b?.opacity ?? a!.opacity;
770+
@override
771+
FilterQuality get filterQuality => b?.filterQuality ?? a!.filterQuality;
772+
@override
773+
bool get invertColors => b?.invertColors ?? a!.invertColors;
774+
@override
775+
bool get isAntiAlias => b?.isAntiAlias ?? a!.isAntiAlias;
776+
777+
@override
778+
DecorationImagePainter createPainter(VoidCallback onChanged) {
779+
return _BlendedDecorationImagePainter._(
780+
a?.createPainter(onChanged),
781+
b?.createPainter(onChanged),
782+
t,
783+
);
784+
}
785+
786+
@override
787+
bool operator ==(Object other) {
788+
if (identical(this, other)) {
789+
return true;
790+
}
791+
if (other.runtimeType != runtimeType) {
792+
return false;
793+
}
794+
return other is _BlendedDecorationImage
795+
&& other.a == a
796+
&& other.b == b
797+
&& other.t == t;
798+
}
799+
800+
@override
801+
int get hashCode => Object.hash(a, b, t);
802+
803+
@override
804+
String toString() {
805+
return '${objectRuntimeType(this, '_BlendedDecorationImage')}($a, $b, $t)';
806+
}
807+
}
808+
809+
class _BlendedDecorationImagePainter implements DecorationImagePainter {
810+
_BlendedDecorationImagePainter._(this.a, this.b, this.t);
811+
812+
final DecorationImagePainter? a;
813+
final DecorationImagePainter? b;
814+
final double t;
815+
816+
@override
817+
void paint(Canvas canvas, Rect rect, Path? clipPath, ImageConfiguration configuration, { double blend = 1.0, BlendMode blendMode = BlendMode.srcOver }) {
818+
a?.paint(canvas, rect, clipPath, configuration, blend: blend * (1.0 - t), blendMode: blendMode);
819+
b?.paint(canvas, rect, clipPath, configuration, blend: blend * t, blendMode: a != null ? BlendMode.plus : blendMode);
820+
}
821+
822+
@override
823+
void dispose() {
824+
a?.dispose();
825+
b?.dispose();
826+
}
827+
828+
@override
829+
String toString() {
830+
return '${objectRuntimeType(this, '_BlendedDecorationImagePainter')}($a, $b, $t)';
831+
}
832+
}

0 commit comments

Comments
 (0)