How can I make a true splash screen in Android? I don't want timers or delays. Just a splash screen that is shown until your application has loaded.
A solution, with code, appears below which seems to have worked flawlessly on a number of test devices for the past six weeks.
However, there are a few preliminaries that should be considered before plunging into a full splash screen.
First of all, if you can avoid the need for a splash screen by bringing up your app's main view immediately, giving the user immediate access to your app, that is your very best option.
You can often accomplish this by immediately bringing up the graphics of your main display, and then creating a worker thread to do any time-consuming initialization tasks, such as loading a table that is always used by the app.
However, it may be that the graphics of your main view themselves take a long time to set up and display, and you want something else to be seen during that initialization.
Now, if your main activity has a simple (e.g., default), light or black, non-transparent background, that will provide immediate confirmation that at least something is occurring when the user launches your app. Background themes that I have personally found to work as primitive "splash" displays include the following (to be added to the activity tag of your main activity in your manifest file):
android:theme="@style/Theme.Light.NoTitleBar"
android:theme="@style/Theme.Black.NoTitleBar"
I would note in this regard that if your activity background requires any kind of bitmap, or animation, or other drawable beyond a default theme (or a simple light or black theme as shown above), my experience is that the activity background will not display until your main view has displayed anyway, and so merely changing the background of your activity to itself be your splash display does not (in my experience) accomplish a more immediate response than your main screen already provides.
Although the above simple themes will work as primitive "splashes," maybe you think that a simple light or black activity background is too nondescript a cue that your app has launched, and you want something that shows the name or logo of your app while the user is waiting. Or, maybe your activity background must be transparent, because you want to be able to overlay some other app with your own app's views (such a transparent background is, of course, invisible during startup, and so will not cue the user that your app has been started).
If, after considering all of the alternatives presented above, you still think that you need a splash screen, here is an approach that I have personally found to work very well.
For this approach, you will need to define a new class that extends LinearLayout. The reason you need your own class is because you need to receive positive confirmation that your splash screen has actually displayed, so you can immediately move on to displaying your main view without some kludge of a timer that can only guess at how long it will take your splash screen to appear. I would note in this regard that if you start the display of your main view too quickly after displaying your splash view, the splash view will never be seen; using this approach avoids that possibility.
Here is an example of such a class:
public class SplashView extends LinearLayout {
public interface SplashEvents {
//This event is signaled after the splash and all of its child views,
// if any, have been drawn.
// As currently implemented, it will trigger BEFORE any scrollbars are drawn.
// We are assuming that there will BE no scrollbars on a SplashView.
public void onSplashDrawComplete();
}
private SplashEvents splashEventHandler = null;
public void setSplashEventHandler(SplashEvents splashEventHandler) {
this.splashEventHandler = splashEventHandler;
}
private void fireSplashDrawCompleteEvent() {
if(this.splashEventHandler != null) {
this.splashEventHandler.onSplashDrawComplete();
}
}
public SplashView(Context context) {
super(context);
//This is set by default for a LinearLayout, but just making sure!
this.setWillNotDraw(true);
//If the cache is not enabled, then I think that helps to ensure that
// dispatchDraw override WILL
// get called. Because if caching were enabled, then the
//drawing might not occur.
this.setDrawingCacheEnabled(false);
setGravity(Gravity.CENTER);
//This splices in your XML definition (see below) to the SplashView layout
LayoutInflater.from(context).inflate(R.layout.splashscreen, this, true);
}
@Override
protected void dispatchDraw(Canvas canvas) {
//Draw any child views
super.dispatchDraw(canvas);
//Signal client objects (in this case, your main activity) that
// we have finished initializing and drawing the view.
fireSplashDrawCompleteEvent();
}
}
Because we are loading our XML from inside the view, we need to define it in XML using a <merge>
tag to "splice" in the XML-defined elements as children of our SplashView class. Here is an example (to be placed in your app's res/layout folder), which you can tailor to your own needs:
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_horizontal"
>
<TextView android:id="@+id/tvHeading"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center"
android:textSize="30dp"
android:textStyle="bold"
android:text="Loading..."
android:layout_weight="1.0"
android:textColor="#00ff00"
android:background="#AA000000"
/>
</merge>
Note that the TextView is defined with a translucent black background, so that it will cause a dimming of the launcher display, with the text "Loading..." superimposed on top in green.
All that remains is to edit something like the following into (and above) the onCreate() method of your main activity:
private Handler uiThreadHandler = new Handler();
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Create an instance of the splash view, and perform a setContentView()
SplashView splashView = new SplashView(this);
//Doing this allows you to access the "this" pointer of the main
// activity inside the Runnable below.
final main mainThis = this;
// Set an event handler on the SplashView object, so that as soon
// as it completes drawing we are
// informed. In response to that cue, we will *then* put up the main view,
// replacing the content view of the main activity with that main view.
splashView.setSplashEventHandler(new SplashView.SplashEvents() {
@Override
public void onSplashDrawComplete() {
//Post the runnable that will put up the main view
uiThreadHandler.post(new Runnable() {
@Override
public void run() {
//This method is where you will have moved
// the entire initialization of your app's
// main display, which normally would have been
// in your onCreate() method prior to adding the
// splash display.
launchMainView(mainThis, savedInstanceState);
}
});
}
});
//This will cause your splash view to draw. When it finishes, it will trigger the code above.
this.setContentView(splashView);
//At this point, do *not* move directly on to performing a setContentView() on your main view.
// If you do, you will never see the splash view at all.
// You simply wait for the splash view to signal you that it has completed drawing itself, and
// *then* call launchMainView(), which will itself call setContentView() again, passing it
// your main view.
}
//Here is a stripped-down version of launchMainView(). You will typically have some additional
// initializations here - whatever might have been present originally in your onCreate() method.
public void launchMainView(main mainThis, Bundle savedInstanceState) {
myRootView = new MyRootView(mainThis);
setContentView(myRootView);
}
The above approach is working very well for me. I have used it targeting API level 8 only, and have tested that code on various devices, including both phones and tablets, running Android 2.2.1, 2.3.3 and 4.0.1 (ICS).
The potential liability of the above approach is the possibility that, for some combination of circumstances, the splash view might not signal that it has completed, and the splash would therefore get "stuck" on the main display, with no main view to replace it. That has never happened to me, but I'd like to solicit comments here regarding whether the override of dispatchDraw() in the SplashView above might ever not get called. I performed a visual inspection of the code that triggers dispatchDraw(), and it looks to me as if it will always get called, given the initializations I've done in the SplashView constructor.
If someone has a better method to override for that same purpose, I'd appreciate hearing about it. I was surprised that I was not able to find any override specifically tailored to fire when a view had finished displaying, and so, if one exists and I somehow missed it, please post a comment about that below. Comments affirming that this approach will work are also very welcome!