mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Implements the Android native stretch effect as a fragment shader (Impeller-only). (#169293)
> Sorry, Closing PR #169196 and reopening this in a new PR for clarity and a cleaner commit history. Fixes #82906 In the existing Flutter implementation, the Android stretch overscroll effect is achieved using Transform. However, this approach does not evenly stretch the screen as it does in the native Android environment. Therefore, in the Impeller environment, add or modify files to implement the effect using a fragment shader identical to the one used in native Android. > [!NOTE] > - The addition of a separate test file for StretchOverscrollEffect was not included because it would likely duplicate coverage already provided by the changes made in overscroll_stretch_indicator_test.dart within this PR. >> However, since determining whether stretching occurred by referencing global coordinates is currently considered impossible with the new Fragment Shader approach, the code was modified to partially exclude the relevant test logic in the Impeller. >> >> For reference, in the page_view_test.dart test, there was an issue with referencing the child Transform widget, but because the StretchOverscrollEffect widget is defined instead of the Transform widget in the Impeller environment, the code logic was adjusted accordingly. > > - Golden image tests were updated as the visual effect changes. ## Reference Source - https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/effects/StretchEffect.cpp ## `Old` Skia (Using Transform) https://github.com/user-attachments/assets/22d8ff96-d875-4722-bf6f-f0ad15b9077d ## `New` Impeller (Using fragment shader) https://github.com/user-attachments/assets/73b6ef18-06b2-42ea-9793-c391ec2ce282 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com> Co-authored-by: Kate Lovett <katelovett@google.com>
This commit is contained in:
parent
20563f943f
commit
03736a282e
159
packages/flutter/lib/src/material/shaders/stretch_effect.frag
Normal file
159
packages/flutter/lib/src/material/shaders/stretch_effect.frag
Normal file
@ -0,0 +1,159 @@
|
||||
#version 320 es
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// This shader was created based on or with reference to the implementation found at:
|
||||
// https://cs.android.com/android/platform/superproject/main/+/512046e84bcc51cc241bc6599f83ab345e93ab12:frameworks/base/libs/hwui/effects/StretchEffect.cpp
|
||||
|
||||
#include <flutter/runtime_effect.glsl>
|
||||
|
||||
uniform vec2 u_size;
|
||||
uniform sampler2D u_texture;
|
||||
|
||||
// Multiplier to apply to scale effect.
|
||||
uniform float u_max_stretch_intensity;
|
||||
|
||||
// Normalized overscroll amount in the horizontal direction.
|
||||
uniform float u_overscroll_x;
|
||||
|
||||
// Normalized overscroll amount in the vertical direction.
|
||||
uniform float u_overscroll_y;
|
||||
|
||||
// u_interpolation_strength is the intensity of the interpolation.
|
||||
uniform float u_interpolation_strength;
|
||||
|
||||
float ease_in(float t, float d) {
|
||||
return t * d;
|
||||
}
|
||||
|
||||
float compute_overscroll_start(
|
||||
float in_pos,
|
||||
float overscroll,
|
||||
float u_stretch_affected_dist,
|
||||
float u_inverse_stretch_affected_dist,
|
||||
float distance_stretched,
|
||||
float interpolation_strength
|
||||
) {
|
||||
float offset_pos = u_stretch_affected_dist - in_pos;
|
||||
float pos_based_variation = mix(
|
||||
1.0,
|
||||
ease_in(offset_pos, u_inverse_stretch_affected_dist),
|
||||
interpolation_strength
|
||||
);
|
||||
float stretch_intensity = overscroll * pos_based_variation;
|
||||
return distance_stretched - (offset_pos / (1.0 + stretch_intensity));
|
||||
}
|
||||
|
||||
float compute_overscroll_end(
|
||||
float in_pos,
|
||||
float overscroll,
|
||||
float reverse_stretch_dist,
|
||||
float u_stretch_affected_dist,
|
||||
float u_inverse_stretch_affected_dist,
|
||||
float distance_stretched,
|
||||
float interpolation_strength,
|
||||
float viewport_dimension
|
||||
) {
|
||||
float offset_pos = in_pos - reverse_stretch_dist;
|
||||
float pos_based_variation = mix(
|
||||
1.0,
|
||||
ease_in(offset_pos, u_inverse_stretch_affected_dist),
|
||||
interpolation_strength
|
||||
);
|
||||
float stretch_intensity = (-overscroll) * pos_based_variation;
|
||||
return viewport_dimension - (distance_stretched - (offset_pos / (1.0 + stretch_intensity)));
|
||||
}
|
||||
|
||||
float compute_streched_effect(
|
||||
float in_pos,
|
||||
float overscroll,
|
||||
float u_stretch_affected_dist,
|
||||
float u_inverse_stretch_affected_dist,
|
||||
float distance_stretched,
|
||||
float distance_diff,
|
||||
float interpolation_strength,
|
||||
float viewport_dimension
|
||||
) {
|
||||
if (overscroll > 0.0) {
|
||||
if (in_pos <= u_stretch_affected_dist) {
|
||||
return compute_overscroll_start(
|
||||
in_pos, overscroll, u_stretch_affected_dist,
|
||||
u_inverse_stretch_affected_dist, distance_stretched,
|
||||
interpolation_strength
|
||||
);
|
||||
} else {
|
||||
return distance_diff + in_pos;
|
||||
}
|
||||
} else if (overscroll < 0.0) {
|
||||
float stretch_affected_dist_calc = viewport_dimension - u_stretch_affected_dist;
|
||||
if (in_pos >= stretch_affected_dist_calc) {
|
||||
return compute_overscroll_end(
|
||||
in_pos,
|
||||
overscroll,
|
||||
stretch_affected_dist_calc,
|
||||
u_stretch_affected_dist,
|
||||
u_inverse_stretch_affected_dist,
|
||||
distance_stretched,
|
||||
interpolation_strength,
|
||||
viewport_dimension
|
||||
);
|
||||
} else {
|
||||
return -distance_diff + in_pos;
|
||||
}
|
||||
} else {
|
||||
return in_pos;
|
||||
}
|
||||
}
|
||||
|
||||
out vec4 frag_color;
|
||||
|
||||
void main() {
|
||||
vec2 uv = FlutterFragCoord().xy / u_size;
|
||||
float in_u_norm = uv.x;
|
||||
float in_v_norm = uv.y;
|
||||
|
||||
float out_u_norm;
|
||||
float out_v_norm;
|
||||
|
||||
bool isVertical = u_overscroll_y != 0;
|
||||
float overscroll = isVertical ? u_overscroll_y : u_overscroll_x;
|
||||
|
||||
float norm_distance_stretched = 1.0 / (1.0 + abs(overscroll));
|
||||
float norm_dist_diff = norm_distance_stretched - 1.0;
|
||||
|
||||
const float norm_viewport = 1.0;
|
||||
const float norm_stretch_affected_dist = 1.0;
|
||||
const float norm_inverse_stretch_affected_dist = 1.0;
|
||||
|
||||
out_u_norm = isVertical ? in_u_norm : compute_streched_effect(
|
||||
in_u_norm,
|
||||
overscroll,
|
||||
norm_stretch_affected_dist,
|
||||
norm_inverse_stretch_affected_dist,
|
||||
norm_distance_stretched,
|
||||
norm_dist_diff,
|
||||
u_interpolation_strength,
|
||||
norm_viewport
|
||||
);
|
||||
|
||||
out_v_norm = isVertical ? compute_streched_effect(
|
||||
in_v_norm,
|
||||
overscroll,
|
||||
norm_stretch_affected_dist,
|
||||
norm_inverse_stretch_affected_dist,
|
||||
norm_distance_stretched,
|
||||
norm_dist_diff,
|
||||
u_interpolation_strength,
|
||||
norm_viewport
|
||||
) : in_v_norm;
|
||||
|
||||
uv.x = out_u_norm;
|
||||
#ifdef IMPELLER_TARGET_OPENGLES
|
||||
uv.y = 1.0 - out_v_norm;
|
||||
#else
|
||||
uv.y = out_v_norm;
|
||||
#endif
|
||||
|
||||
frag_color = texture(u_texture, uv);
|
||||
}
|
||||
@ -23,6 +23,7 @@ import 'framework.dart';
|
||||
import 'media_query.dart';
|
||||
import 'notification_listener.dart';
|
||||
import 'scroll_notification.dart';
|
||||
import 'stretch_effect.dart';
|
||||
import 'ticker_provider.dart';
|
||||
import 'transitions.dart';
|
||||
|
||||
@ -774,20 +775,6 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
|
||||
return false;
|
||||
}
|
||||
|
||||
AlignmentGeometry _getAlignmentForAxisDirection(_StretchDirection stretchDirection) {
|
||||
// Accounts for reversed scrollables by checking the AxisDirection
|
||||
final AxisDirection direction = switch (stretchDirection) {
|
||||
_StretchDirection.trailing => widget.axisDirection,
|
||||
_StretchDirection.leading => flipAxisDirection(widget.axisDirection),
|
||||
};
|
||||
return switch (direction) {
|
||||
AxisDirection.up => AlignmentDirectional.topCenter,
|
||||
AxisDirection.down => AlignmentDirectional.bottomCenter,
|
||||
AxisDirection.left => Alignment.centerLeft,
|
||||
AxisDirection.right => Alignment.centerRight,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stretchController.dispose();
|
||||
@ -802,30 +789,34 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
|
||||
animation: _stretchController,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
final double stretch = _stretchController.value;
|
||||
double x = 1.0;
|
||||
double y = 1.0;
|
||||
final double mainAxisSize;
|
||||
|
||||
switch (widget.axis) {
|
||||
case Axis.horizontal:
|
||||
x += stretch;
|
||||
mainAxisSize = MediaQuery.widthOf(context);
|
||||
case Axis.vertical:
|
||||
y += stretch;
|
||||
mainAxisSize = MediaQuery.heightOf(context);
|
||||
}
|
||||
|
||||
final AlignmentGeometry alignment = _getAlignmentForAxisDirection(
|
||||
_stretchController.stretchDirection,
|
||||
);
|
||||
|
||||
final double viewportDimension =
|
||||
_lastOverscrollNotification?.metrics.viewportDimension ?? mainAxisSize;
|
||||
final Widget transform = Transform(
|
||||
alignment: alignment,
|
||||
transform: Matrix4.diagonal3Values(x, y, 1.0),
|
||||
filterQuality: stretch == 0 ? null : FilterQuality.medium,
|
||||
child: widget.child,
|
||||
|
||||
double overscroll = stretch;
|
||||
|
||||
if (_stretchController.stretchDirection == _StretchDirection.trailing) {
|
||||
overscroll = -overscroll;
|
||||
}
|
||||
|
||||
// Adjust overscroll for reverse scroll directions.
|
||||
if (widget.axisDirection == AxisDirection.up ||
|
||||
widget.axisDirection == AxisDirection.left) {
|
||||
overscroll = -overscroll;
|
||||
}
|
||||
|
||||
final Widget transform = StretchEffect(
|
||||
stretchStrength: overscroll,
|
||||
axis: widget.axis,
|
||||
child: widget.child!,
|
||||
);
|
||||
|
||||
// Only clip if the viewport dimension is smaller than that of the
|
||||
|
||||
254
packages/flutter/lib/src/widgets/stretch_effect.dart
Normal file
254
packages/flutter/lib/src/widgets/stretch_effect.dart
Normal file
@ -0,0 +1,254 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// 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;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'framework.dart';
|
||||
import 'image_filter.dart';
|
||||
|
||||
/// A widget that applies a stretching visual effect to its child.
|
||||
///
|
||||
/// When shader-based effects are supported, this effect replicates the native Android stretch overscroll effect.
|
||||
/// Otherwise, a matrix transform provides an approximation.
|
||||
///
|
||||
/// Used by [StretchingOverscrollIndicator] widget.
|
||||
class StretchEffect extends StatelessWidget {
|
||||
/// Creates a [StretchEffect] widget that applies a stretch effect
|
||||
/// when the user overscrolls horizontally or vertically.
|
||||
///
|
||||
/// The [stretchStrength] controls the intensity of the stretch effect
|
||||
/// and must be between -1.0 and 1.0.
|
||||
const StretchEffect({
|
||||
super.key,
|
||||
this.stretchStrength = 0.0,
|
||||
required this.axis,
|
||||
required this.child,
|
||||
}) : assert(
|
||||
stretchStrength >= -1.0 && stretchStrength <= 1.0,
|
||||
'stretchStrength must be between -1.0 and 1.0',
|
||||
);
|
||||
|
||||
/// The overscroll strength applied for the stretching effect.
|
||||
///
|
||||
/// The value should be between -1.0 and 1.0 inclusive.
|
||||
///
|
||||
/// For the horizontal axis:
|
||||
/// - Positive values apply a pull/stretch from left to right,
|
||||
/// where 1.0 represents the maximum stretch to the right.
|
||||
/// - Negative values apply a pull/stretch from right to left,
|
||||
/// where -1.0 represents the maximum stretch to the left.
|
||||
///
|
||||
/// For the vertical axis:
|
||||
/// - Positive values apply a pull/stretch from top to bottom,
|
||||
/// where 1.0 represents the maximum stretch downward.
|
||||
/// - Negative values apply a pull/stretch from bottom to top,
|
||||
/// where -1.0 represents the maximum stretch upward.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// This example shows how to set the horizontal stretch strength to pull right.
|
||||
///
|
||||
/// ```dart
|
||||
/// const StretchEffect(
|
||||
/// stretchStrength: 0.5,
|
||||
/// axis: Axis.horizontal,
|
||||
/// child: Text('Hello, World!'),
|
||||
/// );
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
final double stretchStrength;
|
||||
|
||||
/// The axis along which the stretching overscroll effect is applied.
|
||||
///
|
||||
/// Determines the direction of the stretch, either horizontal or vertical.
|
||||
final Axis axis;
|
||||
|
||||
/// The child widget that the stretching overscroll effect applies to.
|
||||
final Widget child;
|
||||
|
||||
AlignmentGeometry _getAlignment(TextDirection direction) {
|
||||
final bool isForward = stretchStrength > 0;
|
||||
|
||||
if (axis == Axis.vertical) {
|
||||
return isForward ? AlignmentDirectional.topCenter : AlignmentDirectional.bottomCenter;
|
||||
}
|
||||
|
||||
// RTL horizontal.
|
||||
if (direction == TextDirection.rtl) {
|
||||
return isForward ? AlignmentDirectional.centerEnd : AlignmentDirectional.centerStart;
|
||||
} else {
|
||||
return isForward ? AlignmentDirectional.centerStart : AlignmentDirectional.centerEnd;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (ui.ImageFilter.isShaderFilterSupported) {
|
||||
return _StretchOverscrollEffect(stretchStrength: stretchStrength, axis: axis, child: child);
|
||||
}
|
||||
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
double x = 1.0;
|
||||
double y = 1.0;
|
||||
|
||||
switch (axis) {
|
||||
case Axis.horizontal:
|
||||
x += stretchStrength.abs();
|
||||
case Axis.vertical:
|
||||
y += stretchStrength.abs();
|
||||
}
|
||||
|
||||
return Transform(
|
||||
alignment: _getAlignment(textDirection),
|
||||
transform: Matrix4.diagonal3Values(x, y, 1.0),
|
||||
filterQuality: stretchStrength == 0 ? null : FilterQuality.medium,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that replicates the native Android stretch overscroll effect.
|
||||
///
|
||||
/// This widget is used in the [StretchEffect] widget and creates
|
||||
/// a stretch visual feedback when the user overscrolls at the edges.
|
||||
///
|
||||
/// Only supported when using the Impeller rendering engine.
|
||||
class _StretchOverscrollEffect extends StatefulWidget {
|
||||
/// Creates a [_StretchOverscrollEffect] widget that applies a stretch
|
||||
/// effect when the user overscrolls horizontally or vertically.
|
||||
const _StretchOverscrollEffect({
|
||||
this.stretchStrength = 0.0,
|
||||
required this.axis,
|
||||
required this.child,
|
||||
}) : assert(
|
||||
stretchStrength >= -1.0 && stretchStrength <= 1.0,
|
||||
'stretchStrength must be between -1.0 and 1.0',
|
||||
);
|
||||
|
||||
/// The overscroll strength applied for the stretching effect.
|
||||
///
|
||||
/// The value should be between -1.0 and 1.0 inclusive.
|
||||
/// For horizontal axis, Positive values apply a pull from
|
||||
/// left to right, while negative values pull from right to left.
|
||||
final double stretchStrength;
|
||||
|
||||
/// The axis along which the stretching overscroll effect is applied.
|
||||
///
|
||||
/// Determines the direction of the stretch, either horizontal or vertical.
|
||||
final Axis axis;
|
||||
|
||||
/// The child widget that the stretching overscroll effect applies to.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<_StretchOverscrollEffect> createState() => _StretchOverscrollEffectState();
|
||||
}
|
||||
|
||||
class _StretchOverscrollEffectState extends State<_StretchOverscrollEffect> {
|
||||
ui.FragmentShader? _fragmentShader;
|
||||
|
||||
/// The maximum scale multiplier applied during a stretch effect.
|
||||
static const double maxStretchIntensity = 1.0;
|
||||
|
||||
/// The strength of the interpolation used for smoothing the effect.
|
||||
static const double interpolationStrength = 0.7;
|
||||
|
||||
/// A no-op [ui.ImageFilter] that uses the identity matrix.
|
||||
static final ui.ImageFilter _emptyFilter = ui.ImageFilter.matrix(Matrix4.identity().storage);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fragmentShader?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_StretchEffectShader.initializeShader();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isShaderNeeded = widget.stretchStrength.abs() > precisionErrorTolerance;
|
||||
|
||||
final ui.ImageFilter imageFilter;
|
||||
|
||||
if (_StretchEffectShader._initialized) {
|
||||
_fragmentShader?.dispose();
|
||||
_fragmentShader = _StretchEffectShader._program!.fragmentShader();
|
||||
_fragmentShader!.setFloat(2, maxStretchIntensity);
|
||||
if (widget.axis == Axis.vertical) {
|
||||
_fragmentShader!.setFloat(3, 0.0);
|
||||
_fragmentShader!.setFloat(4, widget.stretchStrength);
|
||||
} else {
|
||||
_fragmentShader!.setFloat(3, widget.stretchStrength);
|
||||
_fragmentShader!.setFloat(4, 0.0);
|
||||
}
|
||||
_fragmentShader!.setFloat(5, interpolationStrength);
|
||||
|
||||
imageFilter = ui.ImageFilter.shader(_fragmentShader!);
|
||||
} else {
|
||||
_fragmentShader?.dispose();
|
||||
_fragmentShader = null;
|
||||
|
||||
imageFilter = _emptyFilter;
|
||||
}
|
||||
|
||||
return ImageFiltered(
|
||||
imageFilter: imageFilter,
|
||||
enabled: isShaderNeeded,
|
||||
// A nearly-transparent pixels is used to ensure the shader gets applied,
|
||||
// even when the child is visually transparent or has no paint operations.
|
||||
child: CustomPaint(
|
||||
painter: isShaderNeeded ? _StretchEffectPainter() : null,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A [CustomPainter] that draws nearly transparent pixels at the four corners.
|
||||
///
|
||||
/// This ensures the fragment shader covers the entire canvas by forcing
|
||||
/// painting operations on all edges, preventing shader optimization skips.
|
||||
class _StretchEffectPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Paint paint = Paint()
|
||||
..color = const Color.fromARGB(1, 0, 0, 0)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawPoints(ui.PointMode.points, <Offset>[
|
||||
Offset.zero,
|
||||
Offset(size.width - 1, 0),
|
||||
Offset(0, size.height - 1),
|
||||
Offset(size.width - 1, size.height - 1),
|
||||
], paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _StretchEffectShader {
|
||||
static bool _initCalled = false;
|
||||
static bool _initialized = false;
|
||||
static ui.FragmentProgram? _program;
|
||||
|
||||
static void initializeShader() {
|
||||
if (!_initCalled) {
|
||||
ui.FragmentProgram.fromAsset('shaders/stretch_effect.frag').then((
|
||||
ui.FragmentProgram program,
|
||||
) {
|
||||
_program = program;
|
||||
_initialized = true;
|
||||
});
|
||||
_initCalled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,6 +154,7 @@ export 'src/widgets/spacer.dart';
|
||||
export 'src/widgets/spell_check.dart';
|
||||
export 'src/widgets/standard_component_type.dart';
|
||||
export 'src/widgets/status_transitions.dart';
|
||||
export 'src/widgets/stretch_effect.dart';
|
||||
export 'src/widgets/system_context_menu.dart';
|
||||
export 'src/widgets/table.dart';
|
||||
export 'src/widgets/tap_region.dart';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// This file is run as part of a reduced test set in CI on Mac and Windows
|
||||
// machines.
|
||||
@Tags(<String>['reduced-test-set'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart';
|
||||
@ -1264,19 +1269,10 @@ void main() {
|
||||
controller.animateToPage(2, duration: const Duration(milliseconds: 300), curve: Curves.ease);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final Finder transformFinder = find.descendant(
|
||||
of: find.byType(PageView),
|
||||
matching: find.byType(Transform),
|
||||
await expectLater(
|
||||
find.byType(PageView),
|
||||
matchesGoldenFile('page_view_no_stretch_precision_error.png'),
|
||||
);
|
||||
expect(transformFinder, findsOneWidget);
|
||||
|
||||
// Get the Transform widget that stretches the PageView.
|
||||
final Transform transform = tester.firstWidget<Transform>(
|
||||
find.descendant(of: find.byType(PageView), matching: find.byType(Transform)),
|
||||
);
|
||||
|
||||
// Check the stretch factor in the first element of the transform matrix.
|
||||
expect(transform.transform.storage.first, 1.0);
|
||||
});
|
||||
|
||||
testWidgets('PageController onAttach, onDetach', (WidgetTester tester) async {
|
||||
|
||||
56
packages/flutter/test/widgets/stretch_effect_test.dart
Normal file
56
packages/flutter/test/widgets/stretch_effect_test.dart
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// This file is run as part of a reduced test set in CI on Mac and Windows
|
||||
// machines.
|
||||
@Tags(<String>['reduced-test-set'])
|
||||
library;
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
// `StretchingOverscrollIndicator` uses a different algorithm when
|
||||
// shader is available, therefore the tests must be different depending
|
||||
// on shader support.
|
||||
final bool shaderSupported = ui.ImageFilter.isShaderFilterSupported;
|
||||
|
||||
testWidgets(
|
||||
'Stretch effect covers full viewport',
|
||||
(WidgetTester tester) async {
|
||||
// This test verifies that when the stretch effect is applied to a scrollable widget,
|
||||
// it should cover the entire scrollable area (e.g., full height of the scroll view),
|
||||
// even if the actual content inside has a smaller height.
|
||||
//
|
||||
// Without this behavior, the shader is clipped only to the content area,
|
||||
// causing the stretch effect to render incorrectly or be invisible
|
||||
// when the content doesn't fill the scroll view.
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: StretchEffect(
|
||||
stretchStrength: 1,
|
||||
axis: Axis.vertical,
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: <Widget>[
|
||||
Container(height: 100),
|
||||
Container(height: 50, color: const Color.fromRGBO(255, 0, 0, 1)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
find.byType(StretchEffect),
|
||||
matchesGoldenFile('stretch_effect_covers_full_viewport.png'),
|
||||
);
|
||||
},
|
||||
// Skips this test when fragment shaders are not used.
|
||||
skip: !shaderSupported,
|
||||
);
|
||||
}
|
||||
@ -50,7 +50,7 @@ const kMaterialFonts = <Map<String, Object>>[
|
||||
},
|
||||
];
|
||||
|
||||
const kMaterialShaders = <String>['shaders/ink_sparkle.frag'];
|
||||
const kMaterialShaders = <String>['shaders/ink_sparkle.frag', 'shaders/stretch_effect.frag'];
|
||||
|
||||
/// Injected factory class for spawning [AssetBundle] instances.
|
||||
abstract class AssetBundleFactory {
|
||||
|
||||
@ -1017,26 +1017,30 @@ flutter:
|
||||
materialDir.childFile(shader).createSync(recursive: true);
|
||||
}
|
||||
|
||||
(globals.processManager as FakeProcessManager).addCommand(
|
||||
FakeCommand(
|
||||
command: <String>[
|
||||
impellerc,
|
||||
'--sksl',
|
||||
'--iplr',
|
||||
'--json',
|
||||
'--sl=${fileSystem.path.join(output.path, 'shaders', 'ink_sparkle.frag')}',
|
||||
'--spirv=${fileSystem.path.join(output.path, 'shaders', 'ink_sparkle.frag.spirv')}',
|
||||
'--input=${fileSystem.path.join(materialDir.path, 'shaders', 'ink_sparkle.frag')}',
|
||||
'--input-type=frag',
|
||||
'--include=${fileSystem.path.join(materialDir.path, 'shaders')}',
|
||||
'--include=$shaderLibDir',
|
||||
],
|
||||
onRun: (_) {
|
||||
fileSystem.file(outputPath).createSync(recursive: true);
|
||||
fileSystem.file('$outputPath.spirv').createSync(recursive: true);
|
||||
},
|
||||
),
|
||||
);
|
||||
final testShaders = <String>['ink_sparkle.frag', 'stretch_effect.frag'];
|
||||
|
||||
for (final shader in testShaders) {
|
||||
(globals.processManager as FakeProcessManager).addCommand(
|
||||
FakeCommand(
|
||||
command: <String>[
|
||||
impellerc,
|
||||
'--sksl',
|
||||
'--iplr',
|
||||
'--json',
|
||||
'--sl=${fileSystem.path.join(output.path, 'shaders', shader)}',
|
||||
'--spirv=${fileSystem.path.join(output.path, 'shaders', '$shader.spirv')}',
|
||||
'--input=${fileSystem.path.join(materialDir.path, 'shaders', shader)}',
|
||||
'--input-type=frag',
|
||||
'--include=${fileSystem.path.join(materialDir.path, 'shaders')}',
|
||||
'--include=$shaderLibDir',
|
||||
],
|
||||
onRun: (_) {
|
||||
fileSystem.file(outputPath).createSync(recursive: true);
|
||||
fileSystem.file('$outputPath.spirv').createSync(recursive: true);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fileSystem.file('pubspec.yaml')
|
||||
..createSync()
|
||||
|
||||
@ -304,18 +304,23 @@ flutter:
|
||||
expect(assetBundle.inputFiles.map((File f) => f.path), <String>[]);
|
||||
});
|
||||
|
||||
final testShaders = <String>['ink_sparkle.frag', 'stretch_effect.frag'];
|
||||
|
||||
testWithoutContext('bundles material shaders on non-web platforms', () async {
|
||||
final String shaderPath = fileSystem.path.join(
|
||||
flutterRoot,
|
||||
'packages',
|
||||
'flutter',
|
||||
'lib',
|
||||
'src',
|
||||
'material',
|
||||
'shaders',
|
||||
'ink_sparkle.frag',
|
||||
);
|
||||
fileSystem.file(shaderPath).createSync(recursive: true);
|
||||
for (final shader in testShaders) {
|
||||
final String shaderPath = fileSystem.path.join(
|
||||
flutterRoot,
|
||||
'packages',
|
||||
'flutter',
|
||||
'lib',
|
||||
'src',
|
||||
'material',
|
||||
'shaders',
|
||||
shader,
|
||||
);
|
||||
fileSystem.file(shaderPath).createSync(recursive: true);
|
||||
}
|
||||
|
||||
writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_package');
|
||||
fileSystem.file('pubspec.yaml').writeAsStringSync('name: my_package');
|
||||
final assetBundle = ManifestAssetBundle(
|
||||
@ -331,21 +336,26 @@ flutter:
|
||||
flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
|
||||
);
|
||||
|
||||
expect(assetBundle.entries.keys, contains('shaders/ink_sparkle.frag'));
|
||||
for (final shader in testShaders) {
|
||||
expect(assetBundle.entries.keys, contains('shaders/$shader'));
|
||||
}
|
||||
});
|
||||
|
||||
testWithoutContext('bundles material shaders on web platforms', () async {
|
||||
final String shaderPath = fileSystem.path.join(
|
||||
flutterRoot,
|
||||
'packages',
|
||||
'flutter',
|
||||
'lib',
|
||||
'src',
|
||||
'material',
|
||||
'shaders',
|
||||
'ink_sparkle.frag',
|
||||
);
|
||||
fileSystem.file(shaderPath).createSync(recursive: true);
|
||||
for (final shader in testShaders) {
|
||||
final String shaderPath = fileSystem.path.join(
|
||||
flutterRoot,
|
||||
'packages',
|
||||
'flutter',
|
||||
'lib',
|
||||
'src',
|
||||
'material',
|
||||
'shaders',
|
||||
shader,
|
||||
);
|
||||
fileSystem.file(shaderPath).createSync(recursive: true);
|
||||
}
|
||||
|
||||
writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_package');
|
||||
fileSystem.file('pubspec.yaml').writeAsStringSync('name: my_package');
|
||||
final assetBundle = ManifestAssetBundle(
|
||||
@ -361,7 +371,9 @@ flutter:
|
||||
flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory),
|
||||
);
|
||||
|
||||
expect(assetBundle.entries.keys, contains('shaders/ink_sparkle.frag'));
|
||||
for (final shader in testShaders) {
|
||||
expect(assetBundle.entries.keys, contains('shaders/$shader'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user