diff --git a/dev/integration_tests/android_engine_test/lib/hcpp/platform_view_opacity_main.dart b/dev/integration_tests/android_engine_test/lib/hcpp/platform_view_opacity_main.dart new file mode 100644 index 0000000000..186667d770 --- /dev/null +++ b/dev/integration_tests/android_engine_test/lib/hcpp/platform_view_opacity_main.dart @@ -0,0 +1,117 @@ +// 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:convert'; + +import 'package:android_driver_extensions/extension.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_driver/driver_extension.dart'; + +import '../src/allow_list_devices.dart'; + +void main() async { + ensureAndroidDevice(); + enableFlutterDriverExtension( + handler: (String? command) async { + return json.encode({ + 'supported': await HybridAndroidViewController.checkIfSupported(), + }); + }, + commands: [nativeDriverCommands], + ); + + // Run on full screen. + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + runApp(const _OpacityWrappedMainApp()); +} + +final class _OpacityWrappedMainApp extends StatefulWidget { + const _OpacityWrappedMainApp(); + + @override + State<_OpacityWrappedMainApp> createState() { + return _OpacityWrappedMainAppState(); + } +} + +class _OpacityWrappedMainAppState extends State<_OpacityWrappedMainApp> { + double opacity = 0.3; + + void _toggleOpacity() { + setState(() { + if (opacity == 1) { + opacity = 0.3; + } else { + opacity = 1; + } + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Opacity( + opacity: opacity, + child: ColoredBox( + color: Colors.white, + child: Stack( + alignment: Alignment.center, + children: [ + TextButton( + key: const ValueKey('ToggleOpacity'), + onPressed: _toggleOpacity, + child: const SizedBox( + width: 300, + height: 300, + child: ColoredBox(color: Colors.green), + ), + ), + const SizedBox( + width: 200, + height: 200, + child: _HybridCompositionAndroidPlatformView( + viewType: 'changing_color_button_platform_view', + ), + ), + ], + ), + ), + ), + ); + } +} + +final class _HybridCompositionAndroidPlatformView extends StatelessWidget { + const _HybridCompositionAndroidPlatformView({required this.viewType}); + + final String viewType; + + @override + Widget build(BuildContext context) { + return PlatformViewLink( + viewType: viewType, + surfaceFactory: (BuildContext context, PlatformViewController controller) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.transparent, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initHybridAndroidView( + id: params.id, + viewType: viewType, + layoutDirection: TextDirection.ltr, + creationParamsCodec: const StandardMessageCodec(), + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..create(); + }, + ); + } +} diff --git a/dev/integration_tests/android_engine_test/test_driver/hcpp/platform_view_opacity_main_test.dart b/dev/integration_tests/android_engine_test/test_driver/hcpp/platform_view_opacity_main_test.dart new file mode 100644 index 0000000000..6dc7c52b86 --- /dev/null +++ b/dev/integration_tests/android_engine_test/test_driver/hcpp/platform_view_opacity_main_test.dart @@ -0,0 +1,64 @@ +// 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:convert'; + +import 'package:android_driver_extensions/native_driver.dart'; +import 'package:android_driver_extensions/skia_gold.dart'; +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart'; + +import '../_luci_skia_gold_prelude.dart'; + +/// For local debugging, a (local) golden-file is required as a baseline: +/// +/// ```sh +/// # Checkout HEAD, i.e. *before* changes you want to test. +/// UPDATE_GOLDENS=1 flutter drive lib/hcpp/platform_view_opacity_main.dart +/// +/// # Make your changes. +/// +/// # Run the test against baseline. +/// flutter drive lib/hcpp/platform_view_opacity_main.dart +/// ``` +/// +/// For a convenient way to deflake a test, see `tool/deflake.dart`. +void main() async { + const String goldenPrefix = 'hybrid_composition_pp_platform_view'; + + late final FlutterDriver flutterDriver; + late final NativeDriver nativeDriver; + + setUpAll(() async { + if (isLuci) { + await enableSkiaGoldComparator(namePrefix: 'android_engine_test$goldenVariant'); + } + flutterDriver = await FlutterDriver.connect(); + nativeDriver = await AndroidNativeDriver.connect(flutterDriver); + await nativeDriver.configureForScreenshotTesting(); + await flutterDriver.waitUntilFirstFrameRasterized(); + }); + + tearDownAll(() async { + await nativeDriver.close(); + await flutterDriver.close(); + }); + + test('verify that HCPP is supported and enabled', () async { + final Map response = + json.decode(await flutterDriver.requestData('')) as Map; + + expect(response['supported'], true); + }, timeout: Timeout.none); + + test('should screenshot a rectangle with specified opacity', () async { + await expectLater(nativeDriver.screenshot(), matchesGoldenFile('$goldenPrefix.opacity.png')); + }, timeout: Timeout.none); + + test('should start with opacity, and toggle to no opacity', () async { + await expectLater(nativeDriver.screenshot(), matchesGoldenFile('$goldenPrefix.opacity.png')); + await flutterDriver.tap(find.byValueKey('ToggleOpacity')); + await expectLater(nativeDriver.screenshot(), matchesGoldenFile('$goldenPrefix.no_opacity.png')); + }, timeout: Timeout.none); +} diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java index b6eaa3d434..c4d4d3a464 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java @@ -6,6 +6,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.Matrix; +import android.graphics.Paint; import android.graphics.Path; import android.view.MotionEvent; import android.view.View; @@ -31,6 +32,7 @@ public class FlutterMutatorView extends FrameLayout { private int prevTop; private final AndroidTouchProcessor androidTouchProcessor; + private Paint paint; /** * Initialize the FlutterMutatorView. Use this to set the screenDensity, which will be used to @@ -43,6 +45,7 @@ public class FlutterMutatorView extends FrameLayout { super(context, null); this.screenDensity = screenDensity; this.androidTouchProcessor = androidTouchProcessor; + this.paint = new Paint(); } /** Initialize the FlutterMutatorView. */ @@ -118,6 +121,14 @@ public class FlutterMutatorView extends FrameLayout { pathCopy.offset(-left, -top); canvas.clipPath(pathCopy); } + + int newAlpha = (int) (255 * mutatorsStack.getFinalOpacity()); + boolean shouldApplyOpacity = paint.getAlpha() != newAlpha; + if (shouldApplyOpacity) { + paint.setAlpha((int) (255 * mutatorsStack.getFinalOpacity())); + this.setLayerType(View.LAYER_TYPE_HARDWARE, paint); + } + super.draw(canvas); canvas.restore(); } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStack.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStack.java index f7c8b3ee9d..8013b78634 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStack.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStack.java @@ -47,6 +47,7 @@ public class FlutterMutatorsStack { @Nullable private Rect rect; @Nullable private Path path; @Nullable private float[] radiis; + private float opacity = 1.f; private FlutterMutatorType type; @@ -93,6 +94,16 @@ public class FlutterMutatorsStack { this.matrix = matrix; } + /** + * Initialize an opacity mutator. + * + * @param opacity the opacity value to apply. The value must be between 0 and 1, inclusive. + */ + public FlutterMutator(float opacity) { + this.type = FlutterMutatorType.OPACITY; + this.opacity = opacity; + } + /** * Get the mutator type. * @@ -128,18 +139,29 @@ public class FlutterMutatorsStack { public Matrix getMatrix() { return matrix; } + + /** + * Get the opacity of the mutator if the {@link #getType()} returns FlutterMutatorType.OPACITY. + * + * @return the opacity of the mutator if the type is FlutterMutatorType.OPACITY; otherwise 1. + */ + public float getOpacity() { + return opacity; + } } private @NonNull List mutators; private List finalClippingPaths; private Matrix finalMatrix; + private float finalOpacity; /** Initialize the mutator stack. */ public FlutterMutatorsStack() { this.mutators = new ArrayList(); finalMatrix = new Matrix(); finalClippingPaths = new ArrayList(); + finalOpacity = 1.f; } /** @@ -187,6 +209,17 @@ public class FlutterMutatorsStack { finalClippingPaths.add(path); } + /** + * Push an opacity {@link FlutterMutatorsStack.FlutterMutator} to the stack. + * + * @param opacity the opacity value to be pushed to the stack. + */ + public void pushOpacity(float opacity) { + FlutterMutator mutator = new FlutterMutator(opacity); + mutators.add(mutator); + finalOpacity *= opacity; + } + /** * Get a list of all the raw mutators. The 0 index of the returned list is the top of the stack. */ @@ -214,4 +247,12 @@ public class FlutterMutatorsStack { public Matrix getFinalMatrix() { return finalMatrix; } + + /** + * Returns the final opacity. The value must be between 0 and 1, inclusive, or behavior will be + * undefined. + */ + public float getFinalOpacity() { + return finalOpacity; + } } diff --git a/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc b/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc index b8e806fef1..a74ceb6cfc 100644 --- a/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc +++ b/engine/src/flutter/shell/platform/android/platform_view_android_jni_impl.cc @@ -158,6 +158,7 @@ static jmethodID g_mutators_stack_init_method = nullptr; static jmethodID g_mutators_stack_push_transform_method = nullptr; static jmethodID g_mutators_stack_push_cliprect_method = nullptr; static jmethodID g_mutators_stack_push_cliprrect_method = nullptr; +static jmethodID g_mutators_stack_push_opacity_method = nullptr; // Called By Java static jlong AttachJNI(JNIEnv* env, jclass clazz, jobject flutterJNI) { @@ -1035,6 +1036,14 @@ bool PlatformViewAndroid::Register(JNIEnv* env) { return false; } + g_mutators_stack_push_opacity_method = + env->GetMethodID(g_mutators_stack_class->obj(), "pushOpacity", "(F)V"); + if (g_mutators_stack_push_opacity_method == nullptr) { + FML_LOG(ERROR) + << "Could not locate FlutterMutatorsStack.pushOpacity method"; + return false; + } + g_java_weak_reference_class = new fml::jni::ScopedJavaGlobalRef( env, env->FindClass("java/lang/ref/WeakReference")); if (g_java_weak_reference_class->is_null()) { @@ -1975,10 +1984,15 @@ void PlatformViewAndroidJNIImpl::onDisplayPlatformView2( radiisArray.obj()); break; } + case kOpacity: { + float opacity = (*iter)->GetAlphaFloat(); + env->CallVoidMethod(mutatorsStack, g_mutators_stack_push_opacity_method, + opacity); + break; + } // TODO(cyanglaz): Implement other mutators. // https://github.com/flutter/flutter/issues/58426 case kClipPath: - case kOpacity: case kBackdropFilter: break; }