diff --git a/shell/common/null_rasterizer.cc b/shell/common/null_rasterizer.cc index 81c0e1f458b00..cba8dbda2f882 100644 --- a/shell/common/null_rasterizer.cc +++ b/shell/common/null_rasterizer.cc @@ -47,4 +47,8 @@ void NullRasterizer::Draw( })); } +void NullRasterizer::AddNextFrameCallback(ftl::Closure nextFrameCallback) { + // Null rasterizer. Nothing to do. +} + } // namespace shell diff --git a/shell/common/null_rasterizer.h b/shell/common/null_rasterizer.h index eba9a57e4f3ef..c4a3b4d8ff36e 100644 --- a/shell/common/null_rasterizer.h +++ b/shell/common/null_rasterizer.h @@ -30,6 +30,8 @@ class NullRasterizer : public Rasterizer { void Draw(ftl::RefPtr> pipeline) override; + void AddNextFrameCallback(ftl::Closure nextFrameCallback) override; + private: std::unique_ptr surface_; ftl::WeakPtrFactory weak_factory_; diff --git a/shell/common/rasterizer.h b/shell/common/rasterizer.h index 37c4eb9826144..1873d12905c9e 100644 --- a/shell/common/rasterizer.h +++ b/shell/common/rasterizer.h @@ -35,6 +35,9 @@ class Rasterizer { virtual void Draw( ftl::RefPtr> pipeline) = 0; + + // Set a callback to be called once when the next frame is drawn. + virtual void AddNextFrameCallback(ftl::Closure nextFrameCallback) = 0; }; } // namespace shell diff --git a/shell/gpu/gpu_rasterizer.cc b/shell/gpu/gpu_rasterizer.cc index 6c64e25942e5e..8b2b3096f25a6 100644 --- a/shell/gpu/gpu_rasterizer.cc +++ b/shell/gpu/gpu_rasterizer.cc @@ -114,6 +114,8 @@ void GPURasterizer::DoDraw(std::unique_ptr layer_tree) { DrawToSurface(*layer_tree); + NotifyNextFrameOnce(); + last_layer_tree_ = std::move(layer_tree); } @@ -140,4 +142,21 @@ void GPURasterizer::DrawToSurface(flow::LayerTree& layer_tree) { frame->Submit(); } +void GPURasterizer::AddNextFrameCallback(ftl::Closure nextFrameCallback) { + nextFrameCallback_ = nextFrameCallback; +} + +void GPURasterizer::NotifyNextFrameOnce() { + if (nextFrameCallback_) { + blink::Threads::Platform()->PostTask([weak_this = weak_factory_.GetWeakPtr()] { + TRACE_EVENT0("flutter", "GPURasterizer::NotifyNextFrameOnce"); + if (weak_this) { + ftl::Closure callback = weak_this->nextFrameCallback_; + callback(); + weak_this->nextFrameCallback_ = nullptr; + } + }); + } +} + } // namespace shell diff --git a/shell/gpu/gpu_rasterizer.h b/shell/gpu/gpu_rasterizer.h index 97c0dda4575bd..722e6175701d2 100644 --- a/shell/gpu/gpu_rasterizer.h +++ b/shell/gpu/gpu_rasterizer.h @@ -35,16 +35,25 @@ class GPURasterizer : public Rasterizer { void Draw(ftl::RefPtr> pipeline) override; + // Set a callback to be called once when the next frame is drawn. + void AddNextFrameCallback(ftl::Closure nextFrameCallback) override; + private: std::unique_ptr surface_; flow::CompositorContext compositor_context_; std::unique_ptr last_layer_tree_; + // A closure to be called when the underlaying surface presents a frame the + // next time. NULL if there is no callback or the callback was set back to + // NULL after being called. + ftl::Closure nextFrameCallback_; ftl::WeakPtrFactory weak_factory_; void DoDraw(std::unique_ptr layer_tree); void DrawToSurface(flow::LayerTree& layer_tree); + void NotifyNextFrameOnce(); + FTL_DISALLOW_COPY_AND_ASSIGN(GPURasterizer); }; diff --git a/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java b/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java index 0a7bf20fd5af3..35fc8e92f802e 100644 --- a/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java +++ b/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java @@ -8,12 +8,20 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; +import android.content.res.Resources.NotFoundException; +import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; import android.view.Window; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; +import android.widget.FrameLayout; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.PluginRegistry.ActivityResultListener; @@ -50,6 +58,11 @@ public final class FlutterActivityDelegate implements FlutterActivityEvents, FlutterView.Provider, PluginRegistry { + private static final String LAUNCH_DRAWABLE_META_DATA_KEY = "io.flutter.app.LaunchScreen"; + private static final String TAG = "FlutterActivityDelegate"; + private static final LayoutParams matchParent = + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + /** * Specifies the mechanism by which Flutter views are created during the * operation of a {@code FlutterActivityDelegate}. @@ -71,6 +84,7 @@ public interface ViewFactory { private final List userLeaveHintListeners = new ArrayList<>(0); private FlutterView flutterView; + private View launchView; public FlutterActivityDelegate(Activity activity, ViewFactory viewFactory) { this.activity = Preconditions.checkNotNull(activity); @@ -138,9 +152,9 @@ public void onCreate(Bundle savedInstanceState) { flutterView = viewFactory.createFlutterView(activity); if (flutterView == null) { flutterView = new FlutterView(activity); - flutterView.setLayoutParams( - new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - activity.setContentView(flutterView); + flutterView.setLayoutParams(matchParent); + launchView = createLaunchView(); + setContentView(); } if (loadIntent(activity.getIntent())) { @@ -272,6 +286,96 @@ private boolean loadIntent(Intent intent) { return false; } + /** + * Creates a {@link View} containing the same {@link Drawable} as the one set as the + * {@code windowBackground} of the parent activity for use as a launch splash view. + * + * Returns null if no {@code windowBackground} is set for the activity. + */ + private View createLaunchView() { + final Drawable launchScreenDrawable = getLaunchScreenDrawableFromActivityTheme(); + if (launchScreenDrawable == null) { + return null; + } + final View view = new View(activity); + view.setLayoutParams(matchParent); + view.setBackground(launchScreenDrawable); + return view; + } + + /** + * Extracts a {@link Drawable} from the parent activity's {@code windowBackground}. + * + * {@code android:windowBackground} is specifically reused instead of a custom defined meta-data + * because the Android framework can display it fast enough when launching the app as opposed + * to anything defined in the Activity subclass. + * + * Returns null if no {@code windowBackground} is set for the activity. + */ + private Drawable getLaunchScreenDrawableFromActivityTheme() { + TypedValue typedValue = new TypedValue(); + if (!activity.getTheme().resolveAttribute( + android.R.attr.windowBackground, + typedValue, + true)) {; + return null; + } + if (typedValue.resourceId == 0) { + return null; + } + try { + return activity.getResources().getDrawable(typedValue.resourceId); + } catch (NotFoundException e) { + Log.e(TAG, "Referenced launch screen drawable resource '" + + LAUNCH_DRAWABLE_META_DATA_KEY + "' does not exist"); + return null; + } + } + + /** + * Sets the root content view of the activity. + * + * If no launch screens are defined in the user application's AndroidManifest.xml as the + * activity's {@code windowBackground}, then set the {@link FlutterView} as the root. + * + * Otherwise, extract the {@code windowBackground}'s {@link Drawable} onto a new launch View to + * put in front of the {@link FlutterView}, remove the activity's {@code windowBackground}, + * and finally remove the launch view when the {@link FlutterView} renders its first frame. + */ + private void setContentView() { + // No transient launch screen. Set the FlutterView as root. + if (launchView == null) { + activity.setContentView(flutterView); + return; + } + + final FrameLayout layout = new FrameLayout(activity); + layout.setLayoutParams(matchParent); + + layout.addView(flutterView); + layout.addView(launchView); + + flutterView.addFirstFrameListener(new FlutterView.FirstFrameListener() { + @Override + public void onFirstFrame() { + // Views need to be unparented before adding directly to activity. + layout.removeAllViews(); + FlutterActivityDelegate.this.activity.setContentView( + FlutterActivityDelegate.this.flutterView); + FlutterActivityDelegate.this.launchView = null; + FlutterActivityDelegate.this.flutterView.removeFirstFrameListener(this); + } + }); + + activity.setContentView(layout); + // Resets the activity theme from the one containing the launch screen in the window + // background to a blank one since the launch screen is now in a view in front of the + // FlutterView. + // + // We can make this configurable if users want it. + activity.setTheme(android.R.style.Theme_Black_NoTitleBar); + } + private class FlutterRegistrar implements Registrar { private final String pluginKey; diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 1ebfad1bb1cc1..e38acb8a89716 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -30,7 +30,7 @@ import android.view.accessibility.AccessibilityNodeProvider; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; - +import io.flutter.app.FlutterActivity; import io.flutter.plugin.common.*; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.editing.TextInputPlugin; @@ -101,6 +101,7 @@ static final class ViewportMetrics { private final BasicMessageChannel mFlutterSystemChannel; private final BroadcastReceiver mDiscoveryReceiver; private final List mActivityLifecycleListeners; + private final List mFirstFrameListeners; private long mNativePlatformView; private boolean mIsSoftwareRenderingEnabled = false; // using the software renderer or not @@ -157,6 +158,7 @@ public void surfaceDestroyed(SurfaceHolder holder) { mMessageHandlers = new HashMap<>(); mActivityLifecycleListeners = new ArrayList<>(); + mFirstFrameListeners = new ArrayList<>(); // Configure the platform plugins and flutter channels. mFlutterLocalizationChannel = new MethodChannel(this, "flutter/localization", @@ -247,6 +249,21 @@ public void onMemoryPressure() { mFlutterSystemChannel.send(message); } + /** + * Provide a listener that will be called once when the FlutterView renders its first frame + * to the underlaying SurfaceView. + */ + public void addFirstFrameListener(FirstFrameListener listener) { + mFirstFrameListeners.add(listener); + } + + /** + * Remove an existing first frame listener. + */ + public void removeFirstFrameListener(FirstFrameListener listener) { + mFirstFrameListeners.remove(listener); + } + public void setInitialRoute(String route) { mFlutterNavigationChannel.invokeMethod("setInitialRoute", route); } @@ -693,6 +710,13 @@ private void updateSemantics(ByteBuffer buffer, String[] strings) { } } + // Called by native to notify first Flutter frame rendered. + private void onFirstFrame() { + for (FirstFrameListener listener : mFirstFrameListeners) { + listener.onFirstFrame(); + } + } + // ACCESSIBILITY private boolean mAccessibilityEnabled = false; @@ -864,4 +888,11 @@ public void onReceive(Context context, Intent intent) { } } } + + /** + * Listener will be called on the Android UI thread once when Flutter renders the first frame. + */ + public interface FirstFrameListener { + void onFirstFrame(); + } } diff --git a/shell/platform/android/platform_view_android.cc b/shell/platform/android/platform_view_android.cc index 4debdd98f777b..6d4ca29bdc55f 100644 --- a/shell/platform/android/platform_view_android.cc +++ b/shell/platform/android/platform_view_android.cc @@ -140,6 +140,15 @@ void PlatformViewAndroid::Attach() { UpdateThreadPriorities(); PostAddToShellTask(); + + rasterizer_->AddNextFrameCallback( + [this]() { + JNIEnv* env = fml::jni::AttachCurrentThread(); + fml::jni::ScopedJavaLocalRef view = flutter_view_.get(env); + if (!view.is_null()) { + FlutterViewOnFirstFrame(env, view.obj()); + } + }); } void PlatformViewAndroid::Detach() { diff --git a/shell/platform/android/platform_view_android_jni.cc b/shell/platform/android/platform_view_android_jni.cc index 1f1a0806009b5..b191eeb2f2b2e 100644 --- a/shell/platform/android/platform_view_android_jni.cc +++ b/shell/platform/android/platform_view_android_jni.cc @@ -49,6 +49,12 @@ void FlutterViewUpdateSemantics(JNIEnv* env, FTL_CHECK(env->ExceptionCheck() == JNI_FALSE); } +static jmethodID g_on_first_frame_method = nullptr; +void FlutterViewOnFirstFrame(JNIEnv* env, jobject obj) { + env->CallVoidMethod(obj, g_on_first_frame_method); + FTL_CHECK(env->ExceptionCheck() == JNI_FALSE); +} + // Called By Java static jlong Attach(JNIEnv* env, jclass clazz, jobject flutterView) { @@ -342,6 +348,12 @@ bool PlatformViewAndroid::Register(JNIEnv* env) { return false; } + g_on_first_frame_method = + env->GetMethodID(g_flutter_view_class->obj(), "onFirstFrame", "()V"); + + if (g_on_first_frame_method == nullptr) { + return false; + } return true; } diff --git a/shell/platform/android/platform_view_android_jni.h b/shell/platform/android/platform_view_android_jni.h index 1bdfec32744b1..848ffc8a01798 100644 --- a/shell/platform/android/platform_view_android_jni.h +++ b/shell/platform/android/platform_view_android_jni.h @@ -27,6 +27,8 @@ void FlutterViewUpdateSemantics(JNIEnv* env, jobject buffer, jobjectArray strings); +void FlutterViewOnFirstFrame(JNIEnv* env, jobject obj); + } // namespace shell #endif // FLUTTER_SHELL_PLATFORM_ANDROID_PLATFORM_VIEW_ANDROID_JNI_H_ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index e793c8d2831ea..d858b3262dbaa 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -72,6 +72,7 @@ @implementation FlutterViewController { fml::scoped_nsprotocol _textInputChannel; fml::scoped_nsprotocol _lifecycleChannel; fml::scoped_nsprotocol _systemChannel; + fml::scoped_nsprotocol _launchView; bool _platformSupportsTouchTypes; bool _platformSupportsTouchPressure; bool _platformSupportsTouchOrientationAndTilt; @@ -126,9 +127,22 @@ - (void)performCommonViewControllerInitialization { _orientationPreferences = UIInterfaceOrientationMaskAll; _statusBarStyle = UIStatusBarStyleDefault; - _platformView = - std::make_shared(reinterpret_cast(self.view.layer)); - _platformView->Attach(); + _platformView = std::make_shared( + reinterpret_cast(self.view.layer)); + + _platformView->Attach( + // First frame callback. + [self]() { + TRACE_EVENT0("flutter", "First Frame"); + if (_launchView) { + [UIView animateWithDuration:0.2 + animations:^{ _launchView.get().alpha = 0; } + completion:^(BOOL finished){ + [_launchView.get() removeFromSuperview]; + _launchView.reset(); + }]; + } + }); _platformView->SetupResourceContextOnIOThread(); _localizationChannel.reset([[FlutterMethodChannel alloc] @@ -277,6 +291,21 @@ - (void)loadView { self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [view release]; + + // Show the launch screen view again on top of the FlutterView if available. + // This launch screen view will be removed once the first Flutter frame is rendered. + NSString* launchStoryboardName = + [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchStoryboardName"]; + if (launchStoryboardName && !self.isBeingPresented && !self.isMovingToParentViewController) { + UIViewController* launchViewController = + [[UIStoryboard storyboardWithName:launchStoryboardName + bundle:nil] instantiateInitialViewController]; + _launchView.reset([launchViewController.view retain]); + _launchView.get().frame = self.view.bounds; + _launchView.get().autoresizingMask = + UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.view addSubview:_launchView.get()]; + } } #pragma mark - Surface creation and teardown updates diff --git a/shell/platform/darwin/ios/platform_view_ios.h b/shell/platform/darwin/ios/platform_view_ios.h index b8e36dc624f94..182840f3e0c2a 100644 --- a/shell/platform/darwin/ios/platform_view_ios.h +++ b/shell/platform/darwin/ios/platform_view_ios.h @@ -11,6 +11,7 @@ #include "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h" #include "flutter/shell/platform/darwin/ios/framework/Source/platform_message_router.h" #include "flutter/shell/platform/darwin/ios/ios_surface.h" +#include "lib/ftl/functional/closure.h" #include "lib/ftl/macros.h" #include "lib/ftl/memory/weak_ptr.h" @@ -25,7 +26,9 @@ class PlatformViewIOS : public PlatformView { ~PlatformViewIOS() override; - virtual void Attach() override; + void Attach() override; + + void Attach(ftl::Closure firstFrameCallback); void NotifyCreated(); @@ -56,6 +59,7 @@ class PlatformViewIOS : public PlatformView { std::unique_ptr ios_surface_; PlatformMessageRouter platform_message_router_; std::unique_ptr accessibility_bridge_; + ftl::Closure firstFrameCallback_; ftl::WeakPtrFactory weak_factory_; void SetupAndLoadFromSource(const std::string& assets_directory, diff --git a/shell/platform/darwin/ios/platform_view_ios.mm b/shell/platform/darwin/ios/platform_view_ios.mm index d32a1fbb7f6cc..dd3db9915cb5e 100644 --- a/shell/platform/darwin/ios/platform_view_ios.mm +++ b/shell/platform/darwin/ios/platform_view_ios.mm @@ -26,8 +26,21 @@ PlatformViewIOS::~PlatformViewIOS() = default; void PlatformViewIOS::Attach() { + Attach(NULL); +} + +void PlatformViewIOS::Attach(ftl::Closure firstFrameCallback) { CreateEngine(); PostAddToShellTask(); + if (firstFrameCallback) { + firstFrameCallback_ = firstFrameCallback; + rasterizer_->AddNextFrameCallback([weakSelf = GetWeakPtr()] { + if (weakSelf) { + weakSelf->firstFrameCallback_(); + weakSelf->firstFrameCallback_ = nullptr; + } + }); + } } void PlatformViewIOS::NotifyCreated() {