未验证 提交 3aaeca3a 编写于 作者: M Matt Carroll 提交者: GitHub

Android Embedding Refactor PR36: Add splash screen support. (#9525)

上级 268533e8
......@@ -549,11 +549,14 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterFragmentActi
FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/DrawableSplashScreen.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSplashView.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreen.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEnginePluginRegistry.java
......
......@@ -123,11 +123,14 @@ action("flutter_shell_java") {
"io/flutter/app/FlutterPluginRegistry.java",
"io/flutter/embedding/android/AndroidKeyProcessor.java",
"io/flutter/embedding/android/AndroidTouchProcessor.java",
"io/flutter/embedding/android/DrawableSplashScreen.java",
"io/flutter/embedding/android/FlutterActivity.java",
"io/flutter/embedding/android/FlutterFragment.java",
"io/flutter/embedding/android/FlutterSplashView.java",
"io/flutter/embedding/android/FlutterSurfaceView.java",
"io/flutter/embedding/android/FlutterTextureView.java",
"io/flutter/embedding/android/FlutterView.java",
"io/flutter/embedding/android/SplashScreen.java",
"io/flutter/embedding/engine/FlutterEngine.java",
"io/flutter/embedding/engine/FlutterEngineAndroidLifecycle.java",
"io/flutter/embedding/engine/FlutterEnginePluginRegistry.java",
......
// Copyright 2013 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.
package io.flutter.embedding.android;
import android.animation.Animator;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
/**
* {@link SplashScreen} that displays a given {@link Drawable}, which then fades its alpha
* to zero when instructed to {@link #transitionToFlutter(Runnable)}.
*/
public final class DrawableSplashScreen implements SplashScreen {
private final Drawable drawable;
private final ImageView.ScaleType scaleType;
private final long crossfadeDurationInMillis;
private DrawableSplashScreenView splashView;
/**
* Constructs a {@code DrawableSplashScreen} that displays the given {@code drawable} and
* crossfades to Flutter content in 500 milliseconds.
*/
public DrawableSplashScreen(@NonNull Drawable drawable) {
this(drawable, ImageView.ScaleType.FIT_XY, 500);
}
/**
* Constructs a {@code DrawableSplashScreen} that displays the given {@code drawable} and
* crossfades to Flutter content in the given {@code crossfadeDurationInMillis}.
* <p>
* @param drawable The {@code Drawable} to be displayed as a splash screen.
* @param scaleType The {@link ImageView.ScaleType} to be applied to the {@code Drawable} when the
* {@code Drawable} is displayed full-screen.
*/
public DrawableSplashScreen(@NonNull Drawable drawable, @NonNull ImageView.ScaleType scaleType, long crossfadeDurationInMillis) {
this.drawable = drawable;
this.scaleType = scaleType;
this.crossfadeDurationInMillis = crossfadeDurationInMillis;
}
@Nullable
@Override
public View createSplashView(@NonNull Context context, @Nullable Bundle savedInstanceState) {
splashView = new DrawableSplashScreenView(context);
splashView.setSplashDrawable(drawable, scaleType);
return splashView;
}
@Override
public void transitionToFlutter(@NonNull Runnable onTransitionComplete) {
if (splashView == null) {
onTransitionComplete.run();
return;
}
splashView.animate()
.alpha(0.0f)
.setDuration(crossfadeDurationInMillis)
.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
onTransitionComplete.run();
}
@Override
public void onAnimationCancel(Animator animation) {
onTransitionComplete.run();
}
@Override
public void onAnimationRepeat(Animator animation) {}
}
);
}
// Public for Android OS requirements. This View should not be used by external developers.
public static class DrawableSplashScreenView extends ImageView {
public DrawableSplashScreenView(@NonNull Context context) {
this(context, null, 0);
}
public DrawableSplashScreenView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DrawableSplashScreenView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setSplashDrawable(@Nullable Drawable drawable) {
setSplashDrawable(drawable, ImageView.ScaleType.FIT_XY);
}
public void setSplashDrawable(@Nullable Drawable drawable, @NonNull ImageView.ScaleType scaleType) {
setScaleType(scaleType);
setImageDrawable(drawable);
}
}
}
......@@ -9,6 +9,7 @@ import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
......@@ -16,7 +17,6 @@ import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.util.TypedValue;
......@@ -29,7 +29,6 @@ import android.widget.FrameLayout;
import io.flutter.Log;
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;
......@@ -65,17 +64,70 @@ import io.flutter.view.FlutterMain;
* {@link FlutterView}. Using a {@link FlutterView} requires forwarding some calls from an
* {@code Activity}, as well as forwarding lifecycle calls from an {@code Activity} or a
* {@code Fragment}.
* <p>
* <strong>Launch Screen and Splash Screen</strong>
* <p>
* {@code FlutterActivity} supports the display of an Android "launch screen" as well as a
* Flutter-specific "splash screen". The launch screen is displayed while the Android application
* loads. It is only applicable if {@code FlutterActivity} is the first {@code Activity} displayed
* upon loading the app. After the launch screen passes, a splash screen is optionally displayed.
* The splash screen is displayed for as long as it takes Flutter to initialize and render its
* first frame.
* <p>
* Use Android themes to display a launch screen. Create two themes: a launch theme and a normal
* theme. In the launch theme, set {@code windowBackground} to the desired {@code Drawable} for
* the launch screen. In the normal theme, set {@code windowBackground} to any desired background
* color that should normally appear behind your Flutter content. In most cases this background
* color will never be seen, but for possible transition edge cases it is a good idea to explicitly
* replace the launch screen window background with a neutral color.
* <p>
* Do not change aspects of system chrome between a launch theme and normal theme. Either define
* both themes to be fullscreen or not, and define both themes to display the same status bar and
* navigation bar settings. To adjust system chrome once the Flutter app renders, use platform
* channels to instruct Android to do so at the appropriate time. This will avoid any jarring visual
* changes during app startup.
* <p>
* In the AndroidManifest.xml, set the theme of {@code FlutterActivity} to the defined launch theme.
* In the metadata section for {@code FlutterActivity}, defined the following reference to your
* normal theme:
*
* {@code
* <meta-data
* android:name="io.flutter.embedding.android.NormalTheme"
* android:resource="@style/YourNormalTheme"
* />
* }
*
* With themes defined, and AndroidManifest.xml updated, Flutter displays the specified launch
* screen until the Android application is initialized.
* <p>
* Flutter also requires initialization time. To specify a splash screen for Flutter initialization,
* subclass {@code FlutterActivity} and override {@link #provideSplashScreen()}. See
* {@link SplashScreen} for details on implementing a splash screen.
* <p>
* Flutter ships with a splash screen that automatically displays the exact same
* {@code windowBackground} as the launch theme discussed previously. To use that splash screen,
* include the following metadata in AndroidManifest.xml for this {@code FlutterActivity}:
*
* {@code
* <meta-data
* android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
* android:value="true"
* />
* }
*/
// TODO(mattcarroll): explain each call forwarded to Fragment (first requires resolution of PluginRegistry API).
public class FlutterActivity extends FragmentActivity
implements FlutterFragment.FlutterEngineProvider,
FlutterFragment.FlutterEngineConfigurator,
OnFirstFrameRenderedListener {
FlutterFragment.SplashScreenProvider {
private static final String TAG = "FlutterActivity";
// Meta-data arguments, processed from manifest XML.
protected static final String DART_ENTRYPOINT_META_DATA_KEY = "io.flutter.Entrypoint";
protected static final String INITIAL_ROUTE_META_DATA_KEY = "io.flutter.InitialRoute";
protected static final String SPLASH_SCREEN_META_DATA_KEY = "io.flutter.embedding.android.SplashScreenDrawable";
protected static final String NORMAL_THEME_META_DATA_KEY = "io.flutter.embedding.android.NormalTheme";
// Intent extra arguments.
protected static final String EXTRA_DART_ENTRYPOINT = "dart_entrypoint";
......@@ -94,11 +146,6 @@ public class FlutterActivity extends FragmentActivity
@Nullable
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.
@Nullable
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.
......@@ -187,91 +234,134 @@ public class FlutterActivity extends FragmentActivity
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
switchLaunchThemeForNormalTheme();
super.onCreate(savedInstanceState);
configureWindowForTransparency();
setContentView(createFragmentContainer());
showCoverView();
configureStatusBarForFullscreenFlutterExperience();
ensureFlutterFragmentCreated();
}
/**
* Sets this {@code Activity}'s {@code Window} background to be transparent, and hides the status
* bar, if this {@code Activity}'s desired {@link BackgroundMode} is {@link BackgroundMode#transparent}.
* Switches themes for this {@code Activity} from the theme used to launch this
* {@code Activity} to a "normal theme" that is intended for regular {@code Activity}
* operation.
* <p>
* For {@code Activity} transparency to work as expected, the theme applied to this {@code Activity}
* must include {@code <item name="android:windowIsTranslucent">true</item>}.
* This behavior is offered so that a "launch screen" can be displayed while the
* application initially loads. To utilize this behavior in an app, do the following:
* <ol>
* <li>Create 2 different themes in style.xml: one theme for the launch screen and
* one theme for normal display.
* <li>In the launch screen theme, set the "windowBackground" property to a {@code Drawable}
* of your choice.
* <li>In the normal theme, customize however you'd like.
* <li>In the AndroidManifest.xml, set the theme of your {@code FlutterActivity} to
* your launch theme.
* <li>Add a {@code <meta-data>} property to your {@code FlutterActivity} with a name
* of "io.flutter.embedding.android.NormalTheme" and set the resource to your normal
* theme, e.g., {@code android:resource="@style/MyNormalTheme}.
* </ol>
* With the above settings, your launch theme will be used when loading the app, and
* then the theme will be switched to your normal theme once the app has initialized.
* <p>
* Do not change aspects of system chrome between a launch theme and normal theme. Either define
* both themes to be fullscreen or not, and define both themes to display the same status bar and
* navigation bar settings. If you wish to adjust system chrome once your Flutter app renders, use
* platform channels to instruct Android to do so at the appropriate time. This will avoid any
* jarring visual changes during app startup.
*/
private void configureWindowForTransparency() {
BackgroundMode backgroundMode = getBackgroundMode();
if (backgroundMode == BackgroundMode.transparent) {
getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
);
private void switchLaunchThemeForNormalTheme() {
try {
ActivityInfo activityInfo = getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
if (activityInfo.metaData != null) {
int normalThemeRID = activityInfo.metaData.getInt(NORMAL_THEME_META_DATA_KEY, -1);
if (normalThemeRID != -1) {
setTheme(normalThemeRID);
}
} else {
Log.d(TAG, "Using the launch theme as normal theme.");
}
} catch (PackageManager.NameNotFoundException exception) {
Log.e(TAG, "Could not read meta-data for FlutterActivity. Using the launch theme as normal theme.");
}
}
/**
* Cover all visible {@code Activity} area with a {@code View} that paints everything the same
* color as the {@code Window}.
* Extracts a {@link Drawable} from the {@code Activity}'s {@code windowBackground}.
* <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.
* Returns null if no {@code windowBackground} is set for the activity.
*/
private void showCoverView() {
if (getBackgroundMode() == BackgroundMode.transparent) {
// Don't display an opaque cover view if the Activity is intended to be transparent.
return;
private Drawable getLaunchScreenDrawableFromActivityTheme() {
TypedValue typedValue = new TypedValue();
if (!getTheme().resolveAttribute(
android.R.attr.windowBackground,
typedValue,
true)) {
return null;
}
Log.v(TAG, "Showing cover view until first frame is rendered.");
// Create the coverView.
if (coverView == null) {
coverView = new View(this);
addContentView(coverView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
if (typedValue.resourceId == 0) {
return null;
}
try {
return getResources().getDrawable(typedValue.resourceId, getTheme());
} catch (Resources.NotFoundException e) {
Log.e(TAG, "Splash screen requested in AndroidManifest.xml, but no windowBackground"
+ " is available in the theme.");
return null;
}
}
// Pain the coverView with the Window's background.
Drawable background = createCoverViewBackground();
if (background != null) {
coverView.setBackground(background);
@Nullable
@Override
public SplashScreen provideSplashScreen() {
Drawable manifestSplashDrawable = getSplashScreenFromManifest();
if (manifestSplashDrawable != null) {
return new DrawableSplashScreen(manifestSplashDrawable);
} 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.
return null;
}
}
/**
* Returns a {@link Drawable} to be used as a splash screen as requested by meta-data in the
* {@code AndroidManifest.xml} file, or null if no such splash screen is requested.
* <p>
* See {@link #SPLASH_SCREEN_META_DATA_KEY} for the meta-data key to be used in a
* manifest file.
*/
@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 {
private Drawable getSplashScreenFromManifest() {
try {
ActivityInfo activityInfo = getPackageManager().getActivityInfo(
getComponentName(),
PackageManager.GET_META_DATA|PackageManager.GET_ACTIVITIES
);
Bundle metadata = activityInfo.metaData;
Integer splashScreenId = metadata != null ? metadata.getInt(SPLASH_SCREEN_META_DATA_KEY) : null;
return splashScreenId != null ? getResources().getDrawable(splashScreenId, getTheme()) : null;
} catch (PackageManager.NameNotFoundException e) {
// This is never expected to happen.
return null;
}
}
/**
* Hides the cover {@code View}.
* Sets this {@code Activity}'s {@code Window} background to be transparent, and hides the status
* bar, if this {@code Activity}'s desired {@link BackgroundMode} is {@link BackgroundMode#transparent}.
* <p>
* This method should be called when Flutter renders its first frame. See {@link #showCoverView()}
* for details.
* For {@code Activity} transparency to work as expected, the theme applied to this {@code Activity}
* must include {@code <item name="android:windowIsTranslucent">true</item>}.
*/
private void hideCoverView() {
if (coverView != null) {
coverView.setVisibility(View.GONE);
private void configureWindowForTransparency() {
BackgroundMode backgroundMode = getBackgroundMode();
if (backgroundMode == BackgroundMode.transparent) {
getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
);
}
}
......@@ -550,12 +640,6 @@ public class FlutterActivity extends FragmentActivity
return (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
}
@Override
public void onFirstFrameRendered() {
Log.v(TAG, "First frame has been rendered. Hiding cover view.");
hideCoverView();
}
/**
* The mode of the background of a {@code FlutterActivity}, either opaque or transparent.
*/
......
......@@ -10,6 +10,7 @@ import android.app.Activity;
import android.arch.lifecycle.Lifecycle;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
......@@ -295,6 +296,8 @@ public class FlutterFragment extends Fragment {
private FlutterEngine flutterEngine;
private boolean isFlutterEngineFromActivity;
@Nullable
private FlutterSplashView flutterSplashView;
@Nullable
private FlutterView flutterView;
@Nullable
private PlatformPlugin platformPlugin;
......@@ -470,7 +473,30 @@ public class FlutterFragment extends Fragment {
Log.v(TAG, "Creating FlutterView.");
flutterView = new FlutterView(getActivity(), getRenderMode(), getTransparencyMode());
flutterView.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
return flutterView;
flutterSplashView = new FlutterSplashView(getContext());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
flutterSplashView.setId(View.generateViewId());
} else {
// TODO(mattcarroll): Find a better solution to this ID. This is a random, static ID.
// It might conflict with other Views, and it means that only a single FlutterSplashView
// can exist in a View hierarchy at one time.
flutterSplashView.setId(486947586);
}
flutterSplashView.displayFlutterViewWithSplash(flutterView, provideSplashScreen());
return flutterSplashView;
}
@Nullable
protected SplashScreen provideSplashScreen() {
FragmentActivity parentActivity = getActivity();
if (parentActivity instanceof SplashScreenProvider) {
SplashScreenProvider splashScreenProvider = (SplashScreenProvider) parentActivity;
return splashScreenProvider.provideSplashScreen();
}
return null;
}
/**
......@@ -853,4 +879,17 @@ public class FlutterFragment extends Fragment {
*/
void configureFlutterEngine(@NonNull FlutterEngine flutterEngine);
}
/**
* Provides a {@link SplashScreen} to display while Flutter initializes and renders its first
* frame.
*/
public interface SplashScreenProvider {
/**
* Provides a {@link SplashScreen} to display while Flutter initializes and renders its first
* frame.
*/
@Nullable
SplashScreen provideSplashScreen();
}
}
// Copyright 2013 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.
package io.flutter.embedding.android;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
/**
* {@code View} that displays a {@link SplashScreen} until a given {@link FlutterView}
* renders its first frame.
*/
/* package */ final class FlutterSplashView extends FrameLayout {
private static String TAG = "FlutterSplashView";
@Nullable
private SplashScreen splashScreen;
@Nullable
private FlutterView flutterView;
@Nullable
private View splashScreenView;
@Nullable
private Bundle splashScreenState;
@Nullable
private String transitioningIsolateId;
@Nullable
private String previousCompletedSplashIsolate;
@NonNull
private final FlutterView.FlutterEngineAttachmentListener flutterEngineAttachmentListener = new FlutterView.FlutterEngineAttachmentListener() {
@Override
public void onFlutterEngineAttachedToFlutterView(@NonNull FlutterEngine engine) {
flutterView.removeFlutterEngineAttachmentListener(this);
displayFlutterViewWithSplash(flutterView, splashScreen);
}
@Override
public void onFlutterEngineDetachedFromFlutterView() {}
};
@NonNull
private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() {
@Override
public void onFirstFrameRendered() {
if (splashScreen != null) {
transitionToFlutter();
}
}
};
@NonNull
private final Runnable onTransitionComplete = new Runnable() {
@Override
public void run() {
removeView(splashScreenView);
previousCompletedSplashIsolate = transitioningIsolateId;
}
};
public FlutterSplashView(@NonNull Context context) {
this(context, null, 0);
}
public FlutterSplashView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public FlutterSplashView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setSaveEnabled(true);
}
@Nullable
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState savedState = new SavedState(superState);
savedState.previousCompletedSplashIsolate = previousCompletedSplashIsolate;
savedState.splashScreenState = splashScreen != null ? splashScreen.saveSplashScreenState() : null;
return savedState;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
previousCompletedSplashIsolate = savedState.previousCompletedSplashIsolate;
splashScreenState = savedState.splashScreenState;
}
/**
* Displays the given {@code splashScreen} on top of the given {@code flutterView} until
* Flutter has rendered its first frame, then the {@code splashScreen} is transitioned away.
* <p>
* If no {@code splashScreen} is provided, this {@code FlutterSplashView} displays the
* given {@code flutterView} on its own.
*/
public void displayFlutterViewWithSplash(
@NonNull FlutterView flutterView,
@Nullable SplashScreen splashScreen
) {
// If we were displaying a previous FlutterView, remove it.
if (this.flutterView != null) {
this.flutterView.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
removeView(this.flutterView);
}
// If we were displaying a previous splash screen View, remove it.
if (splashScreenView != null) {
removeView(splashScreenView);
}
// Display the new FlutterView.
this.flutterView = flutterView;
addView(flutterView);
this.splashScreen = splashScreen;
// Display the new splash screen, if needed.
if (splashScreen != null) {
if (isSplashScreenNeededNow()) {
Log.v(TAG, "Showing splash screen UI.");
// This is the typical case. A FlutterEngine is attached to the FlutterView and we're
// waiting for the first frame to render. Show a splash UI until that happens.
splashScreenView = splashScreen.createSplashView(getContext(), splashScreenState);
addView(this.splashScreenView);
flutterView.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
} else if (isSplashScreenTransitionNeededNow()) {
Log.v(TAG, "Showing an immediate splash transition to Flutter due to previously interrupted transition.");
splashScreenView = splashScreen.createSplashView(getContext(), splashScreenState);
addView(splashScreenView);
transitionToFlutter();
} else if (!flutterView.isAttachedToFlutterEngine()) {
Log.v(TAG, "FlutterView is not yet attached to a FlutterEngine. Showing nothing until a FlutterEngine is attached.");
flutterView.addFlutterEngineAttachmentListener(flutterEngineAttachmentListener);
}
}
}
/**
* Returns true if current conditions require a splash UI to be displayed.
* <p>
* This method does not evaluate whether a previously interrupted splash transition needs
* to resume. See {@link #isSplashScreenTransitionNeededNow()} to answer that question.
*/
private boolean isSplashScreenNeededNow() {
return flutterView != null
&& flutterView.isAttachedToFlutterEngine()
&& !flutterView.hasRenderedFirstFrame()
&& !hasSplashCompleted();
}
/**
* Returns true if a previous splash transition was interrupted by recreation, e.g., an
* orientation change, and that previous transition should be resumed.
* <p>
* Not all splash screens are capable of remembering their transition progress. In those
* cases, this method will return false even if a previous visual transition was
* interrupted.
*/
private boolean isSplashScreenTransitionNeededNow() {
return flutterView != null
&& flutterView.isAttachedToFlutterEngine()
&& splashScreen != null
&& splashScreen.doesSplashViewRememberItsTransition()
&& wasPreviousSplashTransitionInterrupted();
}
/**
* Returns true if a splash screen was transitioning to a Flutter experience and was then
* interrupted, e.g., by an Android configuration change. Returns false otherwise.
* <p>
* Invoking this method expects that a {@code flutterView} exists and it is attached to a
* {@code FlutterEngine}.
*/
private boolean wasPreviousSplashTransitionInterrupted() {
if (flutterView == null) {
throw new IllegalStateException("Cannot determine if previous splash transition was " +
"interrupted when no FlutterView is set.");
}
if (!flutterView.isAttachedToFlutterEngine()) {
throw new IllegalStateException("Cannot determine if previous splash transition was "
+ "interrupted when no FlutterEngine is attached to our FlutterView. This question "
+ "depends on an isolate ID to differentiate Flutter experiences.");
}
return flutterView.hasRenderedFirstFrame() && !hasSplashCompleted();
}
/**
* Returns true if a splash UI for a specific Flutter experience has already completed.
* <p>
* A "specific Flutter experience" is defined as any experience with the same Dart isolate
* ID. The purpose of this distinction is to prevent a situation where a user gets past a
* splash UI, rotates the device (or otherwise triggers a recreation) and the splash screen
* reappears.
* <p>
* An isolate ID is deemed reasonable as a key for a completion event because a Dart isolate
* cannot be entered twice. Therefore, a single Dart isolate cannot return to an "un-rendered"
* state after having previously rendered content.
*/
private boolean hasSplashCompleted() {
if (flutterView == null) {
throw new IllegalStateException("Cannot determine if splash has completed when no FlutterView "
+ "is set.");
}
if (!flutterView.isAttachedToFlutterEngine()) {
throw new IllegalStateException("Cannot determine if splash has completed when no "
+ "FlutterEngine is attached to our FlutterView. This question depends on an isolate ID "
+ "to differentiate Flutter experiences.");
}
// A null isolate ID on a non-null FlutterEngine indicates that the Dart isolate has not
// been initialized. Therefore, no frame has been rendered for this engine, which means
// no splash screen could have completed yet.
return flutterView.getAttachedFlutterEngine().getDartExecutor().getIsolateServiceId() != null
&& flutterView.getAttachedFlutterEngine().getDartExecutor().getIsolateServiceId().equals(previousCompletedSplashIsolate);
}
/**
* Transitions a splash screen to the Flutter UI.
* <p>
* This method requires that our FlutterView be attached to an engine, and that engine have
* a Dart isolate ID. It also requires that a {@code splashScreen} exist.
*/
private void transitionToFlutter() {
transitioningIsolateId = flutterView.getAttachedFlutterEngine().getDartExecutor().getIsolateServiceId();
Log.v(TAG, "Transitioning splash screen to a Flutter UI. Isolate: " + transitioningIsolateId);
splashScreen.transitionToFlutter(onTransitionComplete);
}
public static class SavedState extends BaseSavedState {
public static Creator CREATOR = new Creator() {
@Override
public SavedState createFromParcel(Parcel source) {
return new SavedState(source);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
private String previousCompletedSplashIsolate;
private Bundle splashScreenState;
SavedState(Parcelable superState) {
super(superState);
}
SavedState(Parcel source) {
super(source);
previousCompletedSplashIsolate = source.readString();
splashScreenState = source.readBundle(getClass().getClassLoader());
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeString(previousCompletedSplashIsolate);
out.writeBundle(splashScreenState);
}
}
}
......@@ -73,6 +73,7 @@ public class FlutterView extends FrameLayout {
// Internal view hierarchy references.
@Nullable
private FlutterRenderer.RenderSurface renderSurface;
private final Set<OnFirstFrameRenderedListener> onFirstFrameRenderedListeners = new HashSet<>();
private boolean didRenderFirstFrame;
// Connections to a Flutter execution context.
......@@ -109,6 +110,10 @@ public class FlutterView extends FrameLayout {
@Override
public void onFirstFrameRendered() {
didRenderFirstFrame = true;
for (OnFirstFrameRenderedListener listener : onFirstFrameRenderedListeners) {
listener.onFirstFrameRendered();
}
}
};
......@@ -199,9 +204,6 @@ public class FlutterView extends FrameLayout {
break;
}
// Register a listener for the first frame render event to set didRenderFirstFrame.
renderSurface.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
// FlutterView needs to be focusable so that the InputMethodManager can interact with it.
setFocusable(true);
setFocusableInTouchMode(true);
......@@ -232,7 +234,7 @@ public class FlutterView extends FrameLayout {
* first rendered frame.
*/
public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
renderSurface.addOnFirstFrameRenderedListener(listener);
onFirstFrameRenderedListeners.add(listener);
}
/**
......@@ -240,7 +242,7 @@ public class FlutterView extends FrameLayout {
* {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}.
*/
public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
renderSurface.removeOnFirstFrameRenderedListener(listener);
onFirstFrameRenderedListeners.remove(listener);
}
//------- Start: Process View configuration that Flutter cares about. ------
......@@ -561,8 +563,10 @@ public class FlutterView extends FrameLayout {
this.flutterEngine = flutterEngine;
// Instruct our FlutterRenderer that we are now its designated RenderSurface.
didRenderFirstFrame = false;
this.flutterEngine.getRenderer().attachToRenderSurface(renderSurface);
FlutterRenderer flutterRenderer = this.flutterEngine.getRenderer();
didRenderFirstFrame = flutterRenderer.hasRenderedFirstFrame();
flutterRenderer.attachToRenderSurface(renderSurface);
flutterRenderer.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
// Initialize various components that know how to process Android View I/O
// in a way that Flutter understands.
......@@ -607,6 +611,13 @@ public class FlutterView extends FrameLayout {
for (FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) {
listener.onFlutterEngineAttachedToFlutterView(flutterEngine);
}
// If the first frame has already been rendered, notify all first frame listeners.
// Do this after all other initialization so that listeners don't inadvertently interact
// with a FlutterView that is only partially attached to a FlutterEngine.
if (didRenderFirstFrame) {
onFirstFrameRenderedListener.onFirstFrameRendered();
}
}
/**
......@@ -646,16 +657,11 @@ public class FlutterView extends FrameLayout {
textInputPlugin.destroy();
// Instruct our FlutterRenderer that we are no longer interested in being its RenderSurface.
FlutterRenderer flutterRenderer = flutterEngine.getRenderer();
didRenderFirstFrame = false;
flutterEngine.getRenderer().detachFromRenderSurface();
flutterRenderer.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
flutterRenderer.detachFromRenderSurface();
flutterEngine = null;
// TODO(mattcarroll): clear the surface when JNI doesn't blow up
// if (isSurfaceAvailableForRendering) {
// Canvas canvas = surfaceHolder.lockCanvas();
// canvas.drawColor(Color.RED);
// surfaceHolder.unlockCanvasAndPost(canvas);
// }
}
/**
......
// Copyright 2013 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.
package io.flutter.embedding.android;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
/**
* Splash screen configuration for a given Flutter experience.
* <p>
* Implementations provide a visual representation of a splash screen in
* {@link #createSplashView(Context, Bundle)}, and implement a transition from the
* splash UI to Flutter's UI in {@link #transitionToFlutter(Runnable)}.
*/
public interface SplashScreen {
/**
* Creates a {@code View} to be displayed as a splash screen before
* Flutter renders its first frame.
* <p>
* This method can be called at any time, and may be called multiple times depending on Android
* configuration changes that require recreation of a view hierarchy. Implementers that provide
* a stateful splash view, such as one with animations, should take care to migrate that
* animation state from the previously returned splash view to the newly created splash view.
*/
@Nullable
View createSplashView(@NonNull Context context, @Nullable Bundle savedInstanceState);
/**
* Invoked by Flutter when Flutter has rendered its first frame, and would like
* the {@code splashView} to disappear.
* <p>
* The provided {@code onTransitionComplete} callback must be invoked when
* the splash {@code View} has finished transitioning itself away. The splash
* {@code View} will be removed and destroyed when the callback is invoked.
*/
void transitionToFlutter(@NonNull Runnable onTransitionComplete);
/**
* Returns {@code true} if the splash {@code View} built by this {@code SplashScreen}
* remembers its transition progress across configuration changes by saving that progress to
* {@code View} state. Returns {@code false} otherwise.
* <p>
* The typical return value for this method is {@code false}. When the return value is
* {@code false}, the following can happen:
* <ol>
* <li>Splash {@code View} begins transitioning to the Flutter UI.
* <li>A configuration change occurs, like an orientation change, and the {@code Activity}
* is re-created, along with the {@code View} hierarchy.
* <li>The remainder of the splash transition is skipped and the Flutter UI is displayed.
* </ol>
* In the vast majority of cases, skipping a little bit of the splash transition should be
* acceptable. Most users will never experience such a situation, and those that do are
* unlikely to notice the visual artifact. However, a workaround is available for those
* developers who need it.
* <p>
* Returning {@code true} from this method will cause the given splash {@code View} to be
* displayed in the {@code View} hierarchy, even if Flutter has already rendered its first
* frame. It is then the responsibility of the splash {@code View} to remember its previous
* transition progress, restart any animations, and then trigger its completion callback
* when appropriate. It is also the responsibility of the splash {@code View} to immediately
* invoke the completion callback if it has already completed its transition. By meeting
* these requirements, and returning {@code true} from this method, the splash screen
* experience will be completely seamless, including configuration changes.
*/
// We suppress NewApi because the CI linter thinks that "default" methods are unsupported.
@SuppressLint("NewApi")
default boolean doesSplashViewRememberItsTransition() {
return false;
}
/**
* Returns whatever state is necessary to restore a splash {@code View} after destruction
* and recreation, e.g., orientation change.
*/
// We suppress NewApi because the CI linter thinks that "default" methods are unsupported.
@SuppressLint("NewApi")
@Nullable
default Bundle saveSplashScreenState() {
return null;
}
}
......@@ -41,9 +41,18 @@ public class FlutterRenderer implements TextureRegistry {
private final FlutterJNI flutterJNI;
private final AtomicLong nextTextureId = new AtomicLong(0L);
private RenderSurface renderSurface;
private boolean hasRenderedFirstFrame = false;
private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() {
@Override
public void onFirstFrameRendered() {
hasRenderedFirstFrame = true;
}
};
public FlutterRenderer(@NonNull FlutterJNI flutterJNI) {
this.flutterJNI = flutterJNI;
this.flutterJNI.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
}
/**
......@@ -78,8 +87,16 @@ public class FlutterRenderer implements TextureRegistry {
}
}
public boolean hasRenderedFirstFrame() {
return hasRenderedFirstFrame;
}
public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
flutterJNI.addOnFirstFrameRenderedListener(listener);
if (hasRenderedFirstFrame) {
listener.onFirstFrameRendered();
}
}
public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
......@@ -283,6 +300,9 @@ public class FlutterRenderer implements TextureRegistry {
*/
void detachFromRenderer();
// TODO(mattcarroll): convert old FlutterView to use FlutterEngine instead of individual
// components, then use FlutterEngine's FlutterRenderer to watch for the first frame and
// remove the following methods from this interface.
/**
* The {@link FlutterRenderer} corresponding to this {@code RenderSurface} has painted its
* first frame since being initialized.
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册