Skip to content

Commit

Permalink
Android Embedding PR25: Prevent black rectangle when launching Flutte…
Browse files Browse the repository at this point in the history
…rActivity (flutter#8460)
  • Loading branch information
matthew-carroll authored Apr 5, 2019
1 parent 99da038 commit d3fbaea
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
Expand All @@ -24,6 +27,7 @@

import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.view.FlutterMain;

Expand Down Expand Up @@ -61,7 +65,7 @@
* {@code Fragment}.
*/
// TODO(mattcarroll): explain each call forwarded to Fragment (first requires resolution of PluginRegistry API).
public class FlutterActivity extends FragmentActivity {
public class FlutterActivity extends FragmentActivity implements OnFirstFrameRenderedListener {
private static final String TAG = "FlutterActivity";

// Meta-data arguments, processed from manifest XML.
Expand All @@ -82,6 +86,10 @@ public class FlutterActivity extends FragmentActivity {
private static final int FRAGMENT_CONTAINER_ID = 609893468; // random number
private FlutterFragment flutterFragment;

// Used to cover the Activity until the 1st frame is rendered so as to
// avoid a brief black flicker from a SurfaceView version of FlutterView.
private View coverView;

/**
* Creates an {@link Intent} that launches a {@code FlutterActivity}, which executes
* a {@code main()} Dart entrypoint, and displays the "/" route as Flutter's initial route.
Expand Down Expand Up @@ -147,10 +155,64 @@ public void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate()");
super.onCreate(savedInstanceState);
setContentView(createFragmentContainer());
showCoverView();
configureStatusBarForFullscreenFlutterExperience();
ensureFlutterFragmentCreated();
}

/**
* Cover all visible {@code Activity} area with a {@code View} that paints everything the same
* color as the {@code Window}.
* <p>
* This cover {@code View} should be displayed at the very beginning of the {@code Activity}'s
* lifespan and then hidden once Flutter renders its first frame. The purpose of this cover is to
* cover {@link FlutterSurfaceView}, which briefly displays a black rectangle before it can make
* itself transparent.
*/
private void showCoverView() {
// Create the coverView.
if (coverView == null) {
coverView = new View(this);
addContentView(coverView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}

// Pain the coverView with the Window's background.
Drawable background = createCoverViewBackground();
if (background != null) {
coverView.setBackground(background);
} else {
// If we can't obtain a window background to replicate then we'd be guessing as to the least
// intrusive color. But there is no way to make an accurate guess. In this case we don't
// give the coverView any color, which means a brief black rectangle will be visible upon
// Activity launch.
}
}

@Nullable
private Drawable createCoverViewBackground() {
TypedValue typedValue = new TypedValue();
boolean hasBackgroundColor = getTheme().resolveAttribute(
android.R.attr.windowBackground,
typedValue,
true
);
if (hasBackgroundColor && typedValue.resourceId != 0) {
return getResources().getDrawable(typedValue.resourceId, getTheme());
} else {
return null;
}
}

/**
* Hides the cover {@code View}.
* <p>
* This method should be called when Flutter renders its first frame. See {@link #showCoverView()}
* for details.
*/
private void hideCoverView() {
coverView.setVisibility(View.GONE);
}

private void configureStatusBarForFullscreenFlutterExperience() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = getWindow();
Expand Down Expand Up @@ -360,4 +422,9 @@ protected String getInitialRoute() {
private boolean isDebuggable() {
return (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
}

@Override
public void onFirstFrameRendered() {
hideCoverView();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.view.FlutterMain;

Expand Down Expand Up @@ -214,16 +215,27 @@ protected static Bundle createArgsBundle(@Nullable String dartEntrypoint,
@Nullable
private PlatformPlugin platformPlugin;

private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() {
@Override
public void onFirstFrameRendered() {
// Notify our subclasses that the first frame has been rendered.
FlutterFragment.this.onFirstFrameRendered();

// Notify our owning Activity that the first frame has been rendered.
FragmentActivity fragmentActivity = getActivity();
if (fragmentActivity != null && fragmentActivity instanceof OnFirstFrameRenderedListener) {
OnFirstFrameRenderedListener activityAsListener = (OnFirstFrameRenderedListener) fragmentActivity;
activityAsListener.onFirstFrameRendered();
}
}
};

public FlutterFragment() {
// Ensure that we at least have an empty Bundle of arguments so that we don't
// need to continually check for null arguments before grabbing one.
setArguments(new Bundle());
}

public void prepareForNavigation() {
flutterView.setAlpha(0.0f);
}

/**
* The {@link FlutterEngine} that backs the Flutter content presented by this {@code Fragment}.
*
Expand Down Expand Up @@ -304,6 +316,7 @@ protected void setupFlutterEngine() {
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
flutterView = new FlutterView(getContext(), getRenderMode());
flutterView.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener);

// We post() the code that attaches the FlutterEngine to our FlutterView because there is
// some kind of blocking logic on the native side when the surface is connected. That lag
Expand Down Expand Up @@ -433,6 +446,7 @@ public void onStop() {
public void onDestroyView() {
super.onDestroyView();
Log.d(TAG, "onDestroyView()");
flutterView.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
flutterView.detachFromFlutterEngine();
}

Expand Down Expand Up @@ -580,6 +594,16 @@ private Context getContextCompat() {
: getActivity();
}

/**
* Invoked after the {@link FlutterView} within this {@code FlutterFragment} renders its first
* frame.
* <p>
* The owning {@code Activity} is also sent this message, if it implements
* {@link OnFirstFrameRenderedListener}. This method is invoked before the {@code Activity}'s
* version.
*/
protected void onFirstFrameRendered() {}

/**
* Provides a {@link FlutterEngine} instance to be used by a {@code FlutterFragment}.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import java.util.HashSet;
import java.util.Set;

import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;

/**
* Paints a Flutter UI on a {@link android.view.Surface}.
Expand All @@ -36,6 +40,8 @@ public class FlutterSurfaceView extends SurfaceView implements FlutterRenderer.R
private boolean isAttachedToFlutterRenderer = false;
@Nullable
private FlutterRenderer flutterRenderer;
@NonNull
private Set<OnFirstFrameRenderedListener> onFirstFrameRenderedListeners = new HashSet<>();

// Connects the {@code Surface} beneath this {@code SurfaceView} with Flutter's native code.
// Callbacks are received by this Object and then those messages are forwarded to our
Expand Down Expand Up @@ -177,11 +183,33 @@ private void disconnectSurfaceFromRenderer() {
flutterRenderer.surfaceDestroyed();
}

/**
* Adds the given {@code listener} to this {@code FlutterSurfaceView}, to be notified upon Flutter's
* first rendered frame.
*/
@Override
public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
onFirstFrameRenderedListeners.add(listener);
}

/**
* Removes the given {@code listener}, which was previously added with
* {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}.
*/
@Override
public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
onFirstFrameRenderedListeners.remove(listener);
}

@Override
public void onFirstFrameRendered() {
// TODO(mattcarroll): decide where this method should live and what it needs to do.
Log.d(TAG, "onFirstFrameRendered()");
// Now that a frame is ready to display, take this SurfaceView from transparent to opaque.
setAlpha(1.0f);

for (OnFirstFrameRenderedListener listener : onFirstFrameRenderedListeners) {
listener.onFirstFrameRendered();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
import android.view.Surface;
import android.view.TextureView;

import java.util.HashSet;
import java.util.Set;

import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;

/**
* Paints a Flutter UI on a {@link SurfaceTexture}.
Expand All @@ -37,6 +41,8 @@ public class FlutterTextureView extends TextureView implements FlutterRenderer.R
private boolean isAttachedToFlutterRenderer = false;
@Nullable
private FlutterRenderer flutterRenderer;
@NonNull
private Set<OnFirstFrameRenderedListener> onFirstFrameRenderedListeners = new HashSet<>();

// Connects the {@code SurfaceTexture} beneath this {@code TextureView} with Flutter's native code.
// Callbacks are received by this Object and then those messages are forwarded to our
Expand Down Expand Up @@ -181,9 +187,31 @@ private void disconnectSurfaceFromRenderer() {
flutterRenderer.surfaceDestroyed();
}

/**
* Adds the given {@code listener} to this {@code FlutterTextureView}, to be notified upon Flutter's
* first rendered frame.
*/
@Override
public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
onFirstFrameRenderedListeners.add(listener);
}

/**
* Removes the given {@code listener}, which was previously added with
* {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}.
*/
@Override
public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
onFirstFrameRenderedListeners.remove(listener);
}

@Override
public void onFirstFrameRendered() {
// TODO(mattcarroll): decide where this method should live and what it needs to do.
Log.d(TAG, "onFirstFrameRendered()");

for (OnFirstFrameRenderedListener listener : onFirstFrameRenderedListeners) {
listener.onFirstFrameRendered();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.view.AccessibilityBridge;

Expand Down Expand Up @@ -147,6 +148,22 @@ private void init() {
}
}

/**
* Adds the given {@code listener} to this {@code FlutterView}, to be notified upon Flutter's
* first rendered frame.
*/
public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
renderSurface.addOnFirstFrameRenderedListener(listener);
}

/**
* Removes the given {@code listener}, which was previously added with
* {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}.
*/
public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
renderSurface.removeOnFirstFrameRenderedListener(listener);
}

//------- Start: Process View configuration that Flutter cares about. ------
/**
* Sends relevant configuration data from Android to Flutter when the Android
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,18 @@ public interface RenderSurface {
* never be called.
*/
void onFirstFrameRendered();

/**
* Adds the given {@code listener} to this {@code FlutterRenderer}, to be notified upon Flutter's
* first rendered frame.
*/
void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener);

/**
* Removes the given {@code listener}, which was previously added with
* {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}.
*/
void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener);
}

/**
Expand Down
7 changes: 7 additions & 0 deletions shell/platform/android/io/flutter/view/FlutterNativeView.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.FlutterRenderer.RenderSurface;
import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
import io.flutter.plugin.common.*;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;
Expand Down Expand Up @@ -199,6 +200,12 @@ public void onFirstFrameRendered() {
}
mFlutterView.onFirstFrame();
}

@Override
public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {}

@Override
public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {}
}

private final class EngineLifecycleListenerImpl implements EngineLifecycleListener {
Expand Down

0 comments on commit d3fbaea

Please sign in to comment.