From e024ec4bd141926bbb8ce720f6811cb49100fda5 Mon Sep 17 00:00:00 2001 From: WasserEsser Date: Fri, 10 Dec 2021 02:32:33 +0100 Subject: [PATCH 1/5] Support opacity for platform views using hybrid composition --- .../mutatorsstack/FlutterMutatorView.java | 8 ++++ .../mutatorsstack/FlutterMutatorsStack.java | 38 +++++++++++++++++++ .../android/platform_view_android_jni_impl.cc | 16 +++++++- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java index d46f5346d7ab3..e6b3f4be75096 100644 --- a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java +++ b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java @@ -7,6 +7,8 @@ import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -142,6 +144,12 @@ public void draw(Canvas canvas) { pathCopy.offset(-left, -top); canvas.clipPath(pathCopy); } + + // Apply the final opacity value on the parent canvas. + Rect clipBounds = canvas.getClipBounds(); + RectF rect = new RectF(clipBounds); + canvas.saveLayerAlpha(rect, (int) (mutatorsStack.getFinalOpacity() * 255)); + super.draw(canvas); canvas.restore(); } diff --git a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStack.java b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStack.java index f7c8b3ee9d0e0..6fad699c7f7a1 100644 --- a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStack.java +++ b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStack.java @@ -47,6 +47,7 @@ public class FlutterMutator { @Nullable private Rect rect; @Nullable private Path path; @Nullable private float[] radiis; + @Nullable private float opacity; private FlutterMutatorType type; @@ -93,6 +94,16 @@ public FlutterMutator(Matrix matrix) { this.matrix = matrix; } + /** + * Initialize an opacity mutator. + * + * @param opacity the opacity to apply. + */ + public FlutterMutator(float opacity) { + this.type = FlutterMutatorType.OPACITY; + this.opacity = opacity; + } + /** * Get the mutator type. * @@ -128,18 +139,29 @@ public Path getPath() { public Matrix getMatrix() { return matrix; } + + /** + * Get the opacity of the mutator if {@link #getType()} returns FlutterMutatorType.OPACITY. + * + * @return the opacity if the type is FlutterMutatorType.OPACITY; otherwise null. + */ + 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 void pushClipRRect(int left, int top, int right, int bottom, float[] radi finalClippingPaths.add(path); } + /** + * Push an opacity {@link FlutterMutatorsStack.FlutterMutator} to the stack. + * + * @param opacity the opacity value. + */ + 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,9 @@ public List getFinalClippingPaths() { public Matrix getFinalMatrix() { return finalMatrix; } + + /** Returns the final opacity value. Apply this value to the canvas of the view. */ + public float getFinalOpacity() { + return finalOpacity; + } } diff --git a/shell/platform/android/platform_view_android_jni_impl.cc b/shell/platform/android/platform_view_android_jni_impl.cc index 79bea738b6a68..a7af39a41dafe 100644 --- a/shell/platform/android/platform_view_android_jni_impl.cc +++ b/shell/platform/android/platform_view_android_jni_impl.cc @@ -121,6 +121,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) { @@ -1019,6 +1020,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_on_display_platform_view_method = env->GetMethodID(g_flutter_jni_class->obj(), "onDisplayPlatformView", "(IIIIIIILio/flutter/embedding/engine/mutatorsstack/" @@ -1453,10 +1462,15 @@ void PlatformViewAndroidJNIImpl::FlutterViewOnDisplayPlatformView( (int)rect.bottom(), radiisArray.obj()); break; } + case opacity: { + 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 clip_path: - case opacity: break; } ++iter; From 99c7c3bde43155754e1037727b1b2c573c0b7be3 Mon Sep 17 00:00:00 2001 From: WasserEsser Date: Fri, 10 Dec 2021 14:33:09 +0100 Subject: [PATCH 2/5] Use a hardware layer to avoid double filling --- .../engine/mutatorsstack/FlutterMutatorView.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java index e6b3f4be75096..7c08ec7214665 100644 --- a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java +++ b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java @@ -6,9 +6,8 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Matrix; +import android.graphics.Paint; import android.graphics.Path; -import android.graphics.Rect; -import android.graphics.RectF; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -146,9 +145,9 @@ public void draw(Canvas canvas) { } // Apply the final opacity value on the parent canvas. - Rect clipBounds = canvas.getClipBounds(); - RectF rect = new RectF(clipBounds); - canvas.saveLayerAlpha(rect, (int) (mutatorsStack.getFinalOpacity() * 255)); + Paint transparency = new Paint(); + transparency.setAlpha((int) (mutatorsStack.getFinalOpacity() * 255)); + this.setLayerType(LAYER_TYPE_HARDWARE, transparency); super.draw(canvas); canvas.restore(); From e78ac768eea6a56adb22affdc0c345c29fb757bd Mon Sep 17 00:00:00 2001 From: WasserEsser Date: Sat, 11 Dec 2021 02:05:55 +0100 Subject: [PATCH 3/5] Add tests --- .../test/io/flutter/FlutterTestSuite.java | 2 + .../mutatorsstack/FlutterMutatorViewTest.java | 19 +++++++++ .../FlutterMutatorsStackTest.java | 42 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStackTest.java diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 7f42093a2f657..eae516d515d90 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -24,6 +24,7 @@ import io.flutter.embedding.engine.loader.ApplicationInfoLoaderTest; import io.flutter.embedding.engine.loader.FlutterLoaderTest; import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorViewTest; +import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorsStackTest; import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistryTest; import io.flutter.embedding.engine.renderer.FlutterRendererTest; import io.flutter.embedding.engine.systemchannels.DeferredComponentChannelTest; @@ -73,6 +74,7 @@ FlutterJNITest.class, FlutterLaunchTests.class, FlutterLoaderTest.class, + FlutterMutatorsStackTest.class, FlutterMutatorViewTest.class, FlutterShellArgsTest.class, FlutterRendererTest.class, diff --git a/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java b/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java index 9a5e46b030971..4c5bf1ea077c7 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java @@ -1,10 +1,13 @@ package io.flutter.embedding.engine.mutatorsstack; +import static android.view.View.LAYER_TYPE_HARDWARE; import static android.view.View.OnFocusChangeListener; import static junit.framework.TestCase.*; import static org.mockito.Mockito.*; +import android.graphics.Canvas; import android.graphics.Matrix; +import android.graphics.Paint; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -250,4 +253,20 @@ public ViewTreeObserver getViewTreeObserver() { view.unsetOnDescendantFocusChangeListener(); verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener); } + + @Test + public void draw_opacityApplied() { + final FlutterMutatorView view = new FlutterMutatorView(RuntimeEnvironment.systemContext); + final FlutterMutatorView spy = spy(view); + + final FlutterMutatorsStack mutatorsStack = new FlutterMutatorsStack(); + mutatorsStack.pushOpacity(.3f); + + spy.readyToDisplay(mutatorsStack, /*left=*/ 1, /*top=*/ 2, /*width=*/ 0, /*height=*/ 0); + spy.draw(new Canvas()); + verify(spy) + .setLayerType( + intThat((Integer layerType) -> layerType == LAYER_TYPE_HARDWARE), + argThat((Paint paint) -> paint.getAlpha() == (int) (.3f * 255))); + } } diff --git a/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStackTest.java b/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStackTest.java new file mode 100644 index 0000000000000..9c8b5dbd20a59 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStackTest.java @@ -0,0 +1,42 @@ +package io.flutter.embedding.engine.mutatorsstack; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class FlutterMutatorsStackTest { + + @Test + public void pushOpacity() { + final FlutterMutatorsStack mutatorsStack = new FlutterMutatorsStack(); + mutatorsStack.pushOpacity(.5f); + + assertEquals(mutatorsStack.getMutators().size(), 1); + assertEquals( + mutatorsStack.getMutators().get(0).getType(), + FlutterMutatorsStack.FlutterMutatorType.OPACITY); + assertEquals(mutatorsStack.getMutators().get(0).getOpacity(), .5f, 0f); + } + + @Test + public void defaultOpacity() { + final FlutterMutatorsStack mutatorsStack = new FlutterMutatorsStack(); + + assertEquals(1f, mutatorsStack.getFinalOpacity(), 0f); + } + + @Test + public void layeredOpacity() { + final FlutterMutatorsStack mutatorsStack = new FlutterMutatorsStack(); + mutatorsStack.pushOpacity(.5f); + mutatorsStack.pushOpacity(.6f); + mutatorsStack.pushOpacity(1f); + + assertEquals(.3f, mutatorsStack.getFinalOpacity(), 1 / 255); + } +} From 091514dc580cc0255b9149d7e766121ea349f3f2 Mon Sep 17 00:00:00 2001 From: WasserEsser Date: Thu, 13 Jan 2022 20:43:11 +0100 Subject: [PATCH 4/5] Avoid construction of Paint object every draw call --- .../embedding/engine/mutatorsstack/FlutterMutatorView.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java index 7c08ec7214665..58fc74e37f7dc 100644 --- a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java +++ b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java @@ -29,6 +29,7 @@ public class FlutterMutatorView extends FrameLayout { private int top; private int prevLeft; private int prevTop; + private Paint paint; private final AndroidTouchProcessor androidTouchProcessor; @@ -43,6 +44,7 @@ public FlutterMutatorView( super(context, null); this.screenDensity = screenDensity; this.androidTouchProcessor = androidTouchProcessor; + paint = new Paint(); } /** Initialize the FlutterMutatorView. */ @@ -145,9 +147,8 @@ public void draw(Canvas canvas) { } // Apply the final opacity value on the parent canvas. - Paint transparency = new Paint(); - transparency.setAlpha((int) (mutatorsStack.getFinalOpacity() * 255)); - this.setLayerType(LAYER_TYPE_HARDWARE, transparency); + paint.setAlpha((int) (mutatorsStack.getFinalOpacity() * 255)); + this.setLayerType(LAYER_TYPE_HARDWARE, paint); super.draw(canvas); canvas.restore(); From 7770a6c28806498870947d4c94f3969411cedd5a Mon Sep 17 00:00:00 2001 From: WasserEsser Date: Thu, 13 Jan 2022 20:44:02 +0100 Subject: [PATCH 5/5] Use this.paint --- .../embedding/engine/mutatorsstack/FlutterMutatorView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java index 58fc74e37f7dc..fd9be7a86235a 100644 --- a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java +++ b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java @@ -44,7 +44,7 @@ public FlutterMutatorView( super(context, null); this.screenDensity = screenDensity; this.androidTouchProcessor = androidTouchProcessor; - paint = new Paint(); + this.paint = new Paint(); } /** Initialize the FlutterMutatorView. */