• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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