1 // Copyright 2013 The Flutter Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package io.flutter.embedding.android; 6 7 import android.content.Context; 8 import android.os.Bundle; 9 import android.os.Parcel; 10 import android.os.Parcelable; 11 import android.support.annotation.NonNull; 12 import android.support.annotation.Nullable; 13 import android.util.AttributeSet; 14 import android.view.View; 15 import android.widget.FrameLayout; 16 17 import io.flutter.Log; 18 import io.flutter.embedding.engine.FlutterEngine; 19 import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener; 20 21 /** 22 * {@code View} that displays a {@link SplashScreen} until a given {@link FlutterView} 23 * renders its first frame. 24 */ 25 /* package */ final class FlutterSplashView extends FrameLayout { 26 private static String TAG = "FlutterSplashView"; 27 28 @Nullable 29 private SplashScreen splashScreen; 30 @Nullable 31 private FlutterView flutterView; 32 @Nullable 33 private View splashScreenView; 34 @Nullable 35 private Bundle splashScreenState; 36 @Nullable 37 private String transitioningIsolateId; 38 @Nullable 39 private String previousCompletedSplashIsolate; 40 41 @NonNull 42 private final FlutterView.FlutterEngineAttachmentListener flutterEngineAttachmentListener = new FlutterView.FlutterEngineAttachmentListener() { 43 @Override 44 public void onFlutterEngineAttachedToFlutterView(@NonNull FlutterEngine engine) { 45 flutterView.removeFlutterEngineAttachmentListener(this); 46 displayFlutterViewWithSplash(flutterView, splashScreen); 47 } 48 49 @Override 50 public void onFlutterEngineDetachedFromFlutterView() {} 51 }; 52 53 @NonNull 54 private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() { 55 @Override 56 public void onFirstFrameRendered() { 57 if (splashScreen != null) { 58 transitionToFlutter(); 59 } 60 } 61 }; 62 63 @NonNull 64 private final Runnable onTransitionComplete = new Runnable() { 65 @Override 66 public void run() { 67 removeView(splashScreenView); 68 previousCompletedSplashIsolate = transitioningIsolateId; 69 } 70 }; 71 FlutterSplashView(@onNull Context context)72 public FlutterSplashView(@NonNull Context context) { 73 this(context, null, 0); 74 } 75 FlutterSplashView(@onNull Context context, @Nullable AttributeSet attrs)76 public FlutterSplashView(@NonNull Context context, @Nullable AttributeSet attrs) { 77 this(context, attrs, 0); 78 } 79 FlutterSplashView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)80 public FlutterSplashView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 81 super(context, attrs, defStyleAttr); 82 83 setSaveEnabled(true); 84 } 85 86 @Nullable 87 @Override onSaveInstanceState()88 protected Parcelable onSaveInstanceState() { 89 Parcelable superState = super.onSaveInstanceState(); 90 SavedState savedState = new SavedState(superState); 91 savedState.previousCompletedSplashIsolate = previousCompletedSplashIsolate; 92 savedState.splashScreenState = splashScreen != null ? splashScreen.saveSplashScreenState() : null; 93 return savedState; 94 } 95 96 @Override onRestoreInstanceState(Parcelable state)97 protected void onRestoreInstanceState(Parcelable state) { 98 SavedState savedState = (SavedState) state; 99 super.onRestoreInstanceState(savedState.getSuperState()); 100 previousCompletedSplashIsolate = savedState.previousCompletedSplashIsolate; 101 splashScreenState = savedState.splashScreenState; 102 } 103 104 /** 105 * Displays the given {@code splashScreen} on top of the given {@code flutterView} until 106 * Flutter has rendered its first frame, then the {@code splashScreen} is transitioned away. 107 * <p> 108 * If no {@code splashScreen} is provided, this {@code FlutterSplashView} displays the 109 * given {@code flutterView} on its own. 110 */ displayFlutterViewWithSplash( @onNull FlutterView flutterView, @Nullable SplashScreen splashScreen )111 public void displayFlutterViewWithSplash( 112 @NonNull FlutterView flutterView, 113 @Nullable SplashScreen splashScreen 114 ) { 115 // If we were displaying a previous FlutterView, remove it. 116 if (this.flutterView != null) { 117 this.flutterView.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener); 118 removeView(this.flutterView); 119 } 120 // If we were displaying a previous splash screen View, remove it. 121 if (splashScreenView != null) { 122 removeView(splashScreenView); 123 } 124 125 // Display the new FlutterView. 126 this.flutterView = flutterView; 127 addView(flutterView); 128 129 this.splashScreen = splashScreen; 130 131 // Display the new splash screen, if needed. 132 if (splashScreen != null) { 133 if (isSplashScreenNeededNow()) { 134 Log.v(TAG, "Showing splash screen UI."); 135 // This is the typical case. A FlutterEngine is attached to the FlutterView and we're 136 // waiting for the first frame to render. Show a splash UI until that happens. 137 splashScreenView = splashScreen.createSplashView(getContext(), splashScreenState); 138 addView(this.splashScreenView); 139 flutterView.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener); 140 } else if (isSplashScreenTransitionNeededNow()) { 141 Log.v(TAG, "Showing an immediate splash transition to Flutter due to previously interrupted transition."); 142 splashScreenView = splashScreen.createSplashView(getContext(), splashScreenState); 143 addView(splashScreenView); 144 transitionToFlutter(); 145 } else if (!flutterView.isAttachedToFlutterEngine()) { 146 Log.v(TAG, "FlutterView is not yet attached to a FlutterEngine. Showing nothing until a FlutterEngine is attached."); 147 flutterView.addFlutterEngineAttachmentListener(flutterEngineAttachmentListener); 148 } 149 } 150 } 151 152 /** 153 * Returns true if current conditions require a splash UI to be displayed. 154 * <p> 155 * This method does not evaluate whether a previously interrupted splash transition needs 156 * to resume. See {@link #isSplashScreenTransitionNeededNow()} to answer that question. 157 */ isSplashScreenNeededNow()158 private boolean isSplashScreenNeededNow() { 159 return flutterView != null 160 && flutterView.isAttachedToFlutterEngine() 161 && !flutterView.hasRenderedFirstFrame() 162 && !hasSplashCompleted(); 163 } 164 165 /** 166 * Returns true if a previous splash transition was interrupted by recreation, e.g., an 167 * orientation change, and that previous transition should be resumed. 168 * <p> 169 * Not all splash screens are capable of remembering their transition progress. In those 170 * cases, this method will return false even if a previous visual transition was 171 * interrupted. 172 */ isSplashScreenTransitionNeededNow()173 private boolean isSplashScreenTransitionNeededNow() { 174 return flutterView != null 175 && flutterView.isAttachedToFlutterEngine() 176 && splashScreen != null 177 && splashScreen.doesSplashViewRememberItsTransition() 178 && wasPreviousSplashTransitionInterrupted(); 179 } 180 181 /** 182 * Returns true if a splash screen was transitioning to a Flutter experience and was then 183 * interrupted, e.g., by an Android configuration change. Returns false otherwise. 184 * <p> 185 * Invoking this method expects that a {@code flutterView} exists and it is attached to a 186 * {@code FlutterEngine}. 187 */ wasPreviousSplashTransitionInterrupted()188 private boolean wasPreviousSplashTransitionInterrupted() { 189 if (flutterView == null) { 190 throw new IllegalStateException("Cannot determine if previous splash transition was " + 191 "interrupted when no FlutterView is set."); 192 } 193 if (!flutterView.isAttachedToFlutterEngine()) { 194 throw new IllegalStateException("Cannot determine if previous splash transition was " 195 + "interrupted when no FlutterEngine is attached to our FlutterView. This question " 196 + "depends on an isolate ID to differentiate Flutter experiences."); 197 } 198 return flutterView.hasRenderedFirstFrame() && !hasSplashCompleted(); 199 } 200 201 /** 202 * Returns true if a splash UI for a specific Flutter experience has already completed. 203 * <p> 204 * A "specific Flutter experience" is defined as any experience with the same Dart isolate 205 * ID. The purpose of this distinction is to prevent a situation where a user gets past a 206 * splash UI, rotates the device (or otherwise triggers a recreation) and the splash screen 207 * reappears. 208 * <p> 209 * An isolate ID is deemed reasonable as a key for a completion event because a Dart isolate 210 * cannot be entered twice. Therefore, a single Dart isolate cannot return to an "un-rendered" 211 * state after having previously rendered content. 212 */ hasSplashCompleted()213 private boolean hasSplashCompleted() { 214 if (flutterView == null) { 215 throw new IllegalStateException("Cannot determine if splash has completed when no FlutterView " 216 + "is set."); 217 } 218 if (!flutterView.isAttachedToFlutterEngine()) { 219 throw new IllegalStateException("Cannot determine if splash has completed when no " 220 + "FlutterEngine is attached to our FlutterView. This question depends on an isolate ID " 221 + "to differentiate Flutter experiences."); 222 } 223 224 // A null isolate ID on a non-null FlutterEngine indicates that the Dart isolate has not 225 // been initialized. Therefore, no frame has been rendered for this engine, which means 226 // no splash screen could have completed yet. 227 return flutterView.getAttachedFlutterEngine().getDartExecutor().getIsolateServiceId() != null 228 && flutterView.getAttachedFlutterEngine().getDartExecutor().getIsolateServiceId().equals(previousCompletedSplashIsolate); 229 } 230 231 /** 232 * Transitions a splash screen to the Flutter UI. 233 * <p> 234 * This method requires that our FlutterView be attached to an engine, and that engine have 235 * a Dart isolate ID. It also requires that a {@code splashScreen} exist. 236 */ transitionToFlutter()237 private void transitionToFlutter() { 238 transitioningIsolateId = flutterView.getAttachedFlutterEngine().getDartExecutor().getIsolateServiceId(); 239 Log.v(TAG, "Transitioning splash screen to a Flutter UI. Isolate: " + transitioningIsolateId); 240 splashScreen.transitionToFlutter(onTransitionComplete); 241 } 242 243 public static class SavedState extends BaseSavedState { 244 public static Creator CREATOR = new Creator() { 245 @Override 246 public SavedState createFromParcel(Parcel source) { 247 return new SavedState(source); 248 } 249 250 @Override 251 public SavedState[] newArray(int size) { 252 return new SavedState[size]; 253 } 254 }; 255 256 private String previousCompletedSplashIsolate; 257 private Bundle splashScreenState; 258 SavedState(Parcelable superState)259 SavedState(Parcelable superState) { 260 super(superState); 261 } 262 SavedState(Parcel source)263 SavedState(Parcel source) { 264 super(source); 265 previousCompletedSplashIsolate = source.readString(); 266 splashScreenState = source.readBundle(getClass().getClassLoader()); 267 } 268 269 @Override writeToParcel(Parcel out, int flags)270 public void writeToParcel(Parcel out, int flags) { 271 super.writeToParcel(out, flags); 272 out.writeString(previousCompletedSplashIsolate); 273 out.writeBundle(splashScreenState); 274 } 275 } 276 } 277