Skip to content

Commit 1301475

Browse files
gmackallGray Mackall
andauthored
Implement clipPath Mutator for hcpp (flutter#164525)
Implements `clipPath` mutator for hcpp. Fixes flutter#164219 https://github.com/user-attachments/assets/2c98e621-c73e-40ca-bc76-77de1a3826a0 ## 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: Gray Mackall <mackall@google.com>
1 parent ca9d3b0 commit 1301475

File tree

4 files changed

+389
-1
lines changed

4 files changed

+389
-1
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
7+
import 'package:android_driver_extensions/extension.dart';
8+
import 'package:flutter/foundation.dart';
9+
import 'package:flutter/gestures.dart';
10+
import 'package:flutter/material.dart';
11+
import 'package:flutter/rendering.dart';
12+
import 'package:flutter/services.dart';
13+
import 'package:flutter_driver/driver_extension.dart';
14+
15+
import '../src/allow_list_devices.dart';
16+
17+
void main() async {
18+
ensureAndroidDevice();
19+
enableFlutterDriverExtension(
20+
handler: (String? command) async {
21+
return json.encode(<String, Object?>{
22+
'supported': await HybridAndroidViewController.checkIfSupported(),
23+
});
24+
},
25+
commands: <CommandExtension>[nativeDriverCommands],
26+
);
27+
28+
// Run on full screen.
29+
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
30+
runApp(const _ComplicatedClipPathWrappedMainApp());
31+
}
32+
33+
final class _ComplicatedClipPathWrappedMainApp extends StatefulWidget {
34+
const _ComplicatedClipPathWrappedMainApp();
35+
36+
@override
37+
State<_ComplicatedClipPathWrappedMainApp> createState() {
38+
return _ComplicatedClipPathWrappedMainAppState();
39+
}
40+
}
41+
42+
class _ComplicatedClipPathWrappedMainAppState extends State<_ComplicatedClipPathWrappedMainApp> {
43+
final CustomClipper<Path> _triangleClipper = TriangleClipper();
44+
CustomClipper<Path>? _triangleOrEmpty = TriangleClipper();
45+
46+
void _toggleTriangleClipper() {
47+
setState(() {
48+
if (_triangleOrEmpty == null) {
49+
_triangleOrEmpty = _triangleClipper;
50+
} else {
51+
_triangleOrEmpty = null;
52+
}
53+
});
54+
}
55+
56+
@override
57+
Widget build(BuildContext context) {
58+
return MaterialApp(
59+
home: ClipPath(
60+
clipper: _triangleOrEmpty,
61+
child: ClipPath(
62+
clipper: CubicWaveClipper(),
63+
child: ClipOval(
64+
child: Stack(
65+
alignment: Alignment.center,
66+
children: <Widget>[
67+
TextButton(
68+
key: const ValueKey<String>('ToggleTriangleClipping'),
69+
onPressed: _toggleTriangleClipper,
70+
child: const SizedBox(
71+
width: 500,
72+
height: 500,
73+
child: ColoredBox(color: Colors.green),
74+
),
75+
),
76+
const SizedBox(
77+
width: 400,
78+
height: 400,
79+
child: _HybridCompositionAndroidPlatformView(
80+
viewType: 'changing_color_button_platform_view',
81+
),
82+
),
83+
],
84+
),
85+
),
86+
),
87+
),
88+
);
89+
}
90+
}
91+
92+
// Clips to show the top half of the screen, with a cubic wave as the dividing
93+
// line.
94+
class CubicWaveClipper extends CustomClipper<Path> {
95+
@override
96+
Path getClip(Size size) {
97+
final Path path = Path();
98+
// Closer to 1 moves the wave lower, closer to 0 moves it higher.
99+
final double waveHeight = size.height * 0.65;
100+
101+
path.lineTo(0, waveHeight);
102+
103+
path.cubicTo(
104+
size.width * 0.25,
105+
waveHeight * 0.8,
106+
size.width * 0.75,
107+
waveHeight * 1.2,
108+
size.width,
109+
waveHeight,
110+
);
111+
112+
path.lineTo(size.width, 0);
113+
path.lineTo(0, 0);
114+
115+
path.close();
116+
return path;
117+
}
118+
119+
@override
120+
bool shouldReclip(CustomClipper<Path> oldClipper) {
121+
return false;
122+
}
123+
}
124+
125+
// Clips a triangle off the top left of the screen.
126+
class TriangleClipper extends CustomClipper<Path> {
127+
@override
128+
Path getClip(Size size) {
129+
final Path path = Path();
130+
path.lineTo(0, size.height);
131+
path.lineTo(size.width, size.height);
132+
path.lineTo(size.width, 0);
133+
path.lineTo(size.width / 2, 0);
134+
path.lineTo(0, size.height / 2);
135+
path.lineTo(0, size.height);
136+
path.close();
137+
return path;
138+
}
139+
140+
@override
141+
bool shouldReclip(CustomClipper<Path> oldClipper) {
142+
return false;
143+
}
144+
}
145+
146+
final class _HybridCompositionAndroidPlatformView extends StatelessWidget {
147+
const _HybridCompositionAndroidPlatformView({required this.viewType});
148+
149+
final String viewType;
150+
151+
@override
152+
Widget build(BuildContext context) {
153+
return PlatformViewLink(
154+
viewType: viewType,
155+
surfaceFactory: (BuildContext context, PlatformViewController controller) {
156+
return AndroidViewSurface(
157+
controller: controller as AndroidViewController,
158+
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
159+
hitTestBehavior: PlatformViewHitTestBehavior.transparent,
160+
);
161+
},
162+
onCreatePlatformView: (PlatformViewCreationParams params) {
163+
return PlatformViewsService.initHybridAndroidView(
164+
id: params.id,
165+
viewType: viewType,
166+
layoutDirection: TextDirection.ltr,
167+
creationParamsCodec: const StandardMessageCodec(),
168+
)
169+
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
170+
..create();
171+
},
172+
);
173+
}
174+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
7+
import 'package:android_driver_extensions/native_driver.dart';
8+
import 'package:android_driver_extensions/skia_gold.dart';
9+
import 'package:flutter_driver/flutter_driver.dart';
10+
import 'package:test/test.dart';
11+
12+
import '../_luci_skia_gold_prelude.dart';
13+
14+
/// For local debugging, a (local) golden-file is required as a baseline:
15+
///
16+
/// ```sh
17+
/// # Checkout HEAD, i.e. *before* changes you want to test.
18+
/// UPDATE_GOLDENS=1 flutter drive lib/hcpp/platform_view_clippath_main.dart
19+
///
20+
/// # Make your changes.
21+
///
22+
/// # Run the test against baseline.
23+
/// flutter drive lib/hcpp/platform_view_clippath_main.dart
24+
/// ```
25+
///
26+
/// For a convenient way to deflake a test, see `tool/deflake.dart`.
27+
void main() async {
28+
const String goldenPrefix = 'hybrid_composition_pp_platform_view';
29+
30+
late final FlutterDriver flutterDriver;
31+
late final NativeDriver nativeDriver;
32+
33+
setUpAll(() async {
34+
if (isLuci) {
35+
await enableSkiaGoldComparator(namePrefix: 'android_engine_test$goldenVariant');
36+
}
37+
flutterDriver = await FlutterDriver.connect();
38+
nativeDriver = await AndroidNativeDriver.connect(flutterDriver);
39+
await nativeDriver.configureForScreenshotTesting();
40+
await flutterDriver.waitUntilFirstFrameRasterized();
41+
});
42+
43+
tearDownAll(() async {
44+
await nativeDriver.close();
45+
await flutterDriver.close();
46+
});
47+
48+
test('verify that HCPP is supported and enabled', () async {
49+
final Map<String, Object?> response =
50+
json.decode(await flutterDriver.requestData('')) as Map<String, Object?>;
51+
52+
expect(response['supported'], true);
53+
}, timeout: Timeout.none);
54+
55+
test('should screenshot a platform view with specified clippath', () async {
56+
await expectLater(
57+
nativeDriver.screenshot(),
58+
matchesGoldenFile('$goldenPrefix.complex_clippath.png'),
59+
);
60+
}, timeout: Timeout.none);
61+
62+
test(
63+
'should start with triangle cutoff on left, and toggle to no triangle cutoff on left',
64+
() async {
65+
await expectLater(
66+
nativeDriver.screenshot(),
67+
matchesGoldenFile('$goldenPrefix.complex_clippath.png'),
68+
);
69+
await flutterDriver.tap(find.byValueKey('ToggleTriangleClipping'));
70+
await expectLater(
71+
nativeDriver.screenshot(),
72+
matchesGoldenFile('$goldenPrefix.complex_clippath_no_triangle.png'),
73+
);
74+
},
75+
timeout: Timeout.none,
76+
);
77+
}

engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStack.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,18 @@ public void pushOpacity(float opacity) {
220220
finalOpacity *= opacity;
221221
}
222222

223+
/**
224+
* Push a clipPath {@link FlutterMutatorsStack.FlutterMutator} to the stack.
225+
*
226+
* @param path the path to be clipped.
227+
*/
228+
public void pushClipPath(Path path) {
229+
FlutterMutator mutator = new FlutterMutator(path);
230+
mutators.add(mutator);
231+
path.transform(finalMatrix);
232+
finalClippingPaths.add(path);
233+
}
234+
223235
/**
224236
* Get a list of all the raw mutators. The 0 index of the returned list is the top of the stack.
225237
*/

0 commit comments

Comments
 (0)