Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Launch Screens: From a Tap to Your App

Launch Screens: From a Tap to Your App

The launch screen is a user’s first experience of your application and, hence, should be designed with great care. In this talk, we will deep dive into the concept of launch screen, discover how to measure, debug & optimise them efficiently, and learn more about how to implement them correctly. In other words, this talk is all about discussing for ±45 min about screens displayed less than 5 seconds.

Cyril Mottier

June 17, 2016
Tweet

More Decks by Cyril Mottier

Other Decks in Programming

Transcript

  1. 13 launches per month 23 % apps used only once

    38 sec average interaction time Source: Mobile Life Research Centre, University of Stockholm & Localytics
  2. Branded launch /ˈbrændɪd lɔːntʃ/ Display your logo or other elements

    that improve brand recognition to portray a brand while the app is loading.
  3. Source: iOS Human Interface Guidelines, Apple If you think that

    following these guidelines will result in a plain, boring launch image, you’re right. Remember, the launch image doesn’t provide you with an opportunity for artistic expression. It’s solely intended to enhance the user’s perception of your app as quick to launch and immediately ready for use. “
  4. User taps on icon Timeline of a launch 1 2

    Fit to screen animation starts 3 4 5
  5. User taps on icon Timeline of a launch 1 2

    Fit to screen animation starts 3 4 5 Fit to screen animation ends
  6. User taps on icon Timeline of a launch 1 2

    Fit to screen animation starts 3 4 5 Fit to screen animation ends Window switching starts
  7. User taps on icon Timeline of a launch 1 2

    Fit to screen animation starts 3 4 5 Fit to screen animation ends Window switching starts App content finishes loading
  8. User taps on icon Timeline of a launch 1 2

    Fit to screen animation starts 3 4 5 Fit to screen animation ends Window switching starts App content finishes loading
  9. 1 2 3 4 5 Launcher & Intent triggering Entering

    animation Static launch screen Content switching / fading Application fully drawn 1 2 3 4 5
  10. 1 2 3 5 System App Launcher & Intent triggering

    Entering animation Static launch screen Content switching / fading Application fully drawn 1 2 3 4 5 4
  11. 1 Load and launch the app 2 Display a starting/preview

    window 3 Create the app process 4 Create the app object 5 Launch the main thread 6 Create the main activity 7 Inflate views 8 Lay out the screen 9 Perform the initial draw
  12. 1 Load and launch the app 2 Display a starting/preview

    window 3 Create the app process 4 Create the app object 5 Launch the main thread 6 Create the main activity 7 Inflate views 8 Lay out the screen 9 Perform the initial draw
  13. System 1 Load and launch the app 2 Display a

    starting/preview window 3 Create the app process 4 Create the app object 5 Launch the main thread 6 Create the main activity 7 Inflate views 8 Lay out the screen 9 Perform the initial draw
  14. System App 1 Load and launch the app 2 Display

    a starting/preview window 3 Create the app process 4 Create the app object 5 Launch the main thread 6 Create the main activity 7 Inflate views 8 Lay out the screen 9 Perform the initial draw
  15. System App Cold start 1 Load and launch the app

    2 Display a starting/preview window 3 Create the app process 4 Create the app object 5 Launch the main thread 6 Create the main activity 7 Inflate views 8 Lay out the screen 9 Perform the initial draw
  16. System App Warm start 1 Load and launch the app

    2 Display a starting/preview window 3 Create the app process 4 Create the app object 5 Launch the main thread 6 Create the main activity 7 Inflate views 8 Lay out the screen 9 Perform the initial draw
  17. PhoneWindowManager.java Context context = mContext; try { context = context.createPackageContext(packageName,

    0); context.setTheme(theme); } catch (PackageManager.NameNotFoundException e) { // Ignore } final PhoneWindow win = new PhoneWindow(context); win.setType(WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); win.setFlags(/*...*/); final WindowManager.LayoutParams params = win.getAttributes(); // Tweak params ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)). addView(win.getDecorView(), params);
  18. PhoneWindowManager.java Context context = mContext; try { context = context.createPackageContext(packageName,

    0); context.setTheme(theme); } catch (PackageManager.NameNotFoundException e) { // Ignore } final PhoneWindow win = new PhoneWindow(context); win.setType(WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); win.setFlags(/*...*/); final WindowManager.LayoutParams params = win.getAttributes(); // Tweak params ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)). addView(win.getDecorView(), params); Create Context for the started application context = context.createPackageContext(packageName, 0);
  19. PhoneWindowManager.java Context context = mContext; try { context = context.createPackageContext(packageName,

    0); context.setTheme(theme); } catch (PackageManager.NameNotFoundException e) { // Ignore } final PhoneWindow win = new PhoneWindow(context); win.setType(WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); win.setFlags(/*...*/); final WindowManager.LayoutParams params = win.getAttributes(); // Tweak params ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)). addView(win.getDecorView(), params); Set the target theme context.setTheme(theme);
  20. PhoneWindowManager.java Context context = mContext; try { context = context.createPackageContext(packageName,

    0); context.setTheme(theme); } catch (PackageManager.NameNotFoundException e) { // Ignore } final PhoneWindow win = new PhoneWindow(context); win.setType(WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); win.setFlags(/*...*/); final WindowManager.LayoutParams params = win.getAttributes(); // Tweak params ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)). addView(win.getDecorView(), params); final PhoneWindow win = new PhoneWindow(context); win.setType(WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); win.setFlags(/*...*/); Create new Window and set it up final WindowManager.LayoutParams params = win.getAttributes(); // Tweak params
  21. PhoneWindowManager.java Context context = mContext; try { context = context.createPackageContext(packageName,

    0); context.setTheme(theme); } catch (PackageManager.NameNotFoundException e) { // Ignore } final PhoneWindow win = new PhoneWindow(context); win.setType(WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); win.setFlags(/*...*/); final WindowManager.LayoutParams params = win.getAttributes(); // Tweak params ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)). addView(win.getDecorView(), params); ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)). addView(win.getDecorView(), params); Display the newly created Window
  22. public class LaunchTimeActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Void> {
 
 private

    static final int LOADER_YO = 0xcafe;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_launch_time); 
 getSupportLoaderManager().initLoader(LOADER_YO, null, this);
 }
 
 @Override
 public void onLoadFinished(Loader<Void> loader, Void data) {
 switch (loader.getId()) {
 case LOADER_YO:
 // ...
 break;
 }
 reportFullyDrawn();
 }
 // ... }
  23. public class LaunchTimeActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Void> {
 
 private

    static final int LOADER_YO = 0xcafe;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_launch_time); 
 getSupportLoaderManager().initLoader(LOADER_YO, null, this);
 }
 
 @Override
 public void onLoadFinished(Loader<Void> loader, Void data) {
 switch (loader.getId()) {
 case LOADER_YO:
 // ...
 break;
 }
 reportFullyDrawn();
 }
 // ... } reportFullyDrawn();
  24. public class LaunchTimeActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Void> {
 
 private

    static final int LOADER_YO = 0xcafe;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_launch_time); 
 getSupportLoaderManager().initLoader(LOADER_YO, null, this);
 }
 
 @Override
 public void onLoadFinished(Loader<Void> loader, Void data) {
 switch (loader.getId()) {
 case LOADER_YO:
 // ...
 break;
 }
 reportFullyDrawn();
 }
 // ... } SecurityException on KitKat… reportFullyDrawn();
  25. public final class ActivityCompatAdditions {
 
 private static final Impl

    IMPL;
 
 static {
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
 IMPL = new LollipopImpl();
 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
 IMPL = new KitKatImpl();
 } else {
 IMPL = new BaseImpl();
 }
 }
 
 private ActivityCompatAdditions() {
 // No instances
 } 
 
 public static void reportFullyDrawn(Activity activity) {
 IMPL.reportFullyDrawn(activity);
 } 
 
 private interface Impl {
 void reportFullyDrawn(Activity activity);
 } 

  26. private static final class BaseImpl implements Impl {
 @Override
 public

    void reportFullyDrawn(Activity activity) {
 // No-op
 }
 }
 
 private static final class KitKatImpl implements Impl {
 @Override @TargetApi(Build.VERSION_CODES.KITKAT)
 public void reportFullyDrawn(Activity activity) {
 try {
 activity.reportFullyDrawn();
 } catch (SecurityException se) {
 // Prevent an Exception on KitKat
 }
 }
 }
 
 private static final class LollipopImpl implements Impl {
 @Override @TargetApi(Build.VERSION_CODES.KITKAT)
 public void reportFullyDrawn(Activity activity) {
 activity.reportFullyDrawn();
 }
 }
 
 }
  27. public class LaunchTimeActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Void> {
 
 private

    static final int LOADER_YO = 0xcafe;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_launch_time); 
 getSupportLoaderManager().initLoader(LOADER_YO, null, this);
 }
 
 @Override
 public void onLoadFinished(Loader<Void> loader, Void data) {
 switch (loader.getId()) {
 case LOADER_YO:
 // ...
 break;
 }
 
 }
 // ... } reportFullyDrawn();
  28. public class LaunchTimeActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Void> {
 
 private

    static final int LOADER_YO = 0xcafe;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_launch_time); 
 getSupportLoaderManager().initLoader(LOADER_YO, null, this);
 }
 
 @Override
 public void onLoadFinished(Loader<Void> loader, Void data) {
 switch (loader.getId()) {
 case LOADER_YO:
 // ...
 break;
 }
 
 }
 // ... } reportFullyDrawn();
  29. public class LaunchTimeActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Void> {
 
 private

    static final int LOADER_YO = 0xcafe;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_launch_time); 
 getSupportLoaderManager().initLoader(LOADER_YO, null, this);
 }
 
 @Override
 public void onLoadFinished(Loader<Void> loader, Void data) {
 switch (loader.getId()) {
 case LOADER_YO:
 // ...
 break;
 }
 }
 // ... } ActivityCompatAdditions.reportFullyDrawn(this);
  30. $ adb logcat | grep “ActivityManager” ActivityManager: Displayed com.launchtime/.LaunchTimeActivity: +666ms

    ActivityManager: Fully drawn com.launchtime/.LaunchTimeActivity: +1s440ms API 19+
  31. $ adb shell am start -W com.launchtime/.LaunchScreenActivity Starting: Intent {

    act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.launchtime/.LaunchScreenActivity } Status: ok Activity: com.launchtime/.LaunchScreenActivity ThisTime: 2426 TotalTime: 2426 WaitTime: 2488 Complete
  32. public class LazyProvider extends ContentProvider {
 
 private static final

    int MODEL = 101;
 private static final int MODEL_ID = 102;
 
 private static final UriMatcher URI_MATCHER;
 static {
 URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
 URI_MATCHER.addURI(LazyContract.AUTHORITY, "/model", MODEL);
 URI_MATCHER.addURI(LazyContract.AUTHORITY, "/model/#", MODEL_ID);
 // ...
 }
 
 private SQLiteOpenHelper mOpenHelper;
 
 @Override
 public boolean onCreate() {
 mOpenHelper = new LoadOpenHelper(getContext());
 return true;
 }
 // ...
 } LazyProvider.java
  33. public class LazyProvider extends ContentProvider {
 
 private static final

    int MODEL = 101;
 private static final int MODEL_ID = 102;
 
 private static final UriMatcher URI_MATCHER;
 static {
 URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
 URI_MATCHER.addURI(LazyContract.AUTHORITY, "/model", MODEL);
 URI_MATCHER.addURI(LazyContract.AUTHORITY, "/model/#", MODEL_ID);
 // ...
 }
 
 private SQLiteOpenHelper mOpenHelper;
 
 @Override
 public boolean onCreate() {
 mOpenHelper = new LoadOpenHelper(getContext());
 return true;
 }
 // ...
 } LazyProvider.java private static final UriMatcher URI_MATCHER; Created at class loading but used only in CRUD…
  34. public abstract class LazyGetter<T> {
 
 private final AtomicReference<Optional<T>> mCachedValue

    = new AtomicReference<>(Optional.<T>empty());
 
 public final T get() {
 Optional<T> value = mCachedValue.get();
 if (!value.isPresent()) {
 synchronized (mCachedValue) {
 value = mCachedValue.get();
 if (!value.isPresent()) {
 mCachedValue.set(Optional.ofNullable(initialValue()));
 }
 }
 }
 return mCachedValue.get().get();
 }
 
 protected abstract T initialValue();
 
 } LazyGetter.java
  35. public class LazyProvider extends ContentProvider {
 
 private static final

    int MODEL = 101;
 private static final int MODEL_ID = 102;
 
 private static final LazyGetter<UriMatcher> URI_MATCHER = new LazyGetter<UriMatcher>() {
 @Override
 protected UriMatcher initialValue() {
 final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
 uriMatcher.addURI(LazyContract.AUTHORITY, "/model", MODEL);
 uriMatcher.addURI(LazyContract.AUTHORITY, "/model/#", MODEL_ID);
 // ...
 return uriMatcher;
 }
 }; 
 // ...
 } LazyProvider.java
  36. <?xml version="1.0" encoding="utf-8"?>
 <manifest package="com.cyrilmottier.android.launchtime"
 xmlns:android="http://schemas.android.com/apk/res/android">
 
 <application
 android:icon="@mipmap/ic_launcher"
 android:label="@string/app_name"


    android:theme="@style/Theme.LaunchTime">
 
 <activity
 android:name=".LaunchScreenActivity">
 <intent-filter>
 <action android:name="android.intent.action.MAIN" />
 <category android:name="android.intent.category.LAUNCHER" />
 </intent-filter>
 </activity>
 
 </application>
 
 </manifest> AndroidManifest.xml
  37. <?xml version="1.0" encoding="utf-8"?>
 <manifest package="com.cyrilmottier.android.launchtime"
 xmlns:android="http://schemas.android.com/apk/res/android">
 
 <application
 android:icon="@mipmap/ic_launcher"
 android:label="@string/app_name"


    android:theme="@style/Theme.LaunchTime">
 
 <activity
 android:name=".LaunchScreenActivity">
 <intent-filter>
 <action android:name="android.intent.action.MAIN" />
 <category android:name="android.intent.category.LAUNCHER" />
 </intent-filter>
 </activity>
 
 </application>
 
 </manifest> Default theme applies to all Activities AndroidManifest.xml android:theme="@style/Theme.LaunchTime">
  38. <?xml version="1.0" encoding="utf-8"?> <resources>
 
 <style name="Theme.LaunchTime" parent="android:Theme.Material.Light.DarkActionBar">
 <item name="android:colorPrimary">@color/colorPrimary</item>


    <item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
 <item name="android:colorAccent">@color/colorAccent</item>
 <item name="android:windowBackground">@color/appBackground</item>
 </style>
 
 </resources>
 values/themes.xml
  39. <?xml version="1.0" encoding="utf-8"?>
 <resources>
 
 <style name=“PreviewTheme.LaunchTime" parent=“android:Theme.Holo.Light.DarkActionBar"> 
 <item

    name=“android:actionBarSize"> @dimen/abc_action_bar_default_height_material </item>
 <item name="android:actionBarStyle">@style/PreviewWidget.ActionBar</item> <item name="android:windowBackground">@color/appBackground</item> <item name=“ltRuntimeTheme">@style/Theme.LaunchTime</item> 
 </style>
 
 </resources> values/themes_preview.xml
  40. <?xml version="1.0" encoding="utf-8"?>
 <resources>
 
 <style name=“PreviewTheme.LaunchTime" parent=“android:Theme.Holo.Light.DarkActionBar"> 
 <item

    name=“android:actionBarSize"> @dimen/abc_action_bar_default_height_material </item>
 <item name="android:actionBarStyle">@style/PreviewWidget.ActionBar</item> <item name="android:windowBackground">@color/appBackground</item> <item name=“ltRuntimeTheme">@style/Theme.LaunchTime</item> 
 </style>
 
 </resources> values/themes_preview.xml <item name=“android:actionBarSize"> @dimen/abc_action_bar_default_height_material </item> Force pre-Lollipop ActionBars height to 48 / 56 / 64dp
  41. <?xml version="1.0" encoding="utf-8"?>
 <resources>
 
 <style name=“PreviewTheme.LaunchTime" parent=“android:Theme.Holo.Light.DarkActionBar"> 
 <item

    name=“android:actionBarSize"> @dimen/abc_action_bar_default_height_material </item>
 <item name="android:actionBarStyle">@style/PreviewWidget.ActionBar</item> <item name="android:windowBackground">@color/appBackground</item> <item name=“ltRuntimeTheme">@style/Theme.LaunchTime</item> 
 </style>
 
 </resources> values/themes_preview.xml <item name=“ltRuntimeTheme">@style/Theme.LaunchTime</item> Define the theme to use at runtime
  42. <?xml version="1.0" encoding="utf-8"?>
 <resources>
 
 <style name="PreviewWidget.ActionBar" parent=“android:Widget.Holo.Light.ActionBar.Solid.Inverse"> 
 <item

    name="android:background">@color/colorPrimary</item>
 <item name=“android:displayOptions">showTitle</item> 
 </style>
 
 </resources> values/styles_preview.xml
  43. <?xml version="1.0" encoding="utf-8"?>
 <resources>
 
 <style name="PreviewTheme.LaunchTime" parent=“android:Theme.Material.Light.DarkActionBar"> 
 <item

    name="android:colorPrimary">@color/colorPrimary</item>
 <item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
 <item name="android:colorAccent">@color/colorAccent</item>
 <item name="android:windowBackground">@color/appBackground</item>
 <item name=“ltRuntimeTheme">@style/Theme.LaunchTime</item> 
 </style>
 
 </resources> values-v21/themes_preview.xml
  44. <?xml version="1.0" encoding="utf-8"?>
 <resources>
 
 <style name="PreviewTheme.LaunchTime" parent=“android:Theme.Material.Light.DarkActionBar"> 
 <item

    name="android:colorPrimary">@color/colorPrimary</item>
 <item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
 <item name="android:colorAccent">@color/colorAccent</item>
 <item name="android:windowBackground">@color/appBackground</item>
 <item name=“ltRuntimeTheme">@style/Theme.LaunchTime</item> 
 </style>
 
 </resources> values-v21/themes_preview.xml parent=“android:Theme.Material.Light.DarkActionBar" Use Theme.Material on v-21+ devices
  45. <?xml version="1.0" encoding="utf-8"?>
 <resources>
 
 <style name="PreviewTheme.LaunchTime" parent=“android:Theme.Material.Light.DarkActionBar"> 
 <item

    name="android:colorPrimary">@color/colorPrimary</item>
 <item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
 <item name="android:colorAccent">@color/colorAccent</item>
 <item name="android:windowBackground">@color/appBackground</item>
 <item name=“ltRuntimeTheme">@style/Theme.LaunchTime</item> 
 </style>
 
 </resources> values-v21/themes_preview.xml Define the theme to use at runtime <item name=“ltRuntimeTheme">@style/Theme.LaunchTime</item>
  46. <?xml version="1.0" encoding="utf-8"?>
 <manifest package="com.cyrilmottier.android.launchtime"
 xmlns:android="http://schemas.android.com/apk/res/android">
 
 <application
 android:icon="@mipmap/ic_launcher"
 android:label="@string/app_name"


    android:theme="@style/PreviewTheme.LaunchTime">
 
 <activity android:name=".LaunchScreenActivity">
 <intent-filter>
 <action android:name="android.intent.action.MAIN" />
 <category android:name="android.intent.category.LAUNCHER" />
 </intent-filter>
 </activity>
 
 </application>
 
 </manifest> AndroidManifest.xml
  47. <?xml version="1.0" encoding="utf-8"?>
 <manifest package="com.cyrilmottier.android.launchtime"
 xmlns:android="http://schemas.android.com/apk/res/android">
 
 <application
 android:icon="@mipmap/ic_launcher"
 android:label="@string/app_name"


    android:theme="@style/PreviewTheme.LaunchTime">
 
 <activity android:name=".LaunchScreenActivity">
 <intent-filter>
 <action android:name="android.intent.action.MAIN" />
 <category android:name="android.intent.category.LAUNCHER" />
 </intent-filter>
 </activity>
 
 </application>
 
 </manifest> AndroidManifest.xml android:theme="@style/PreviewTheme.LaunchTime" Tell the system to use a temporary preview theme
  48. public final class ThemeUtils { private ThemeUtils() { // No

    instances } @UiThread public static void ensureRuntimeTheme(Context context) { final TypedValue tv = new TypedValue(); context.getTheme().resolveAttribute(R.attr.ltRuntimeTheme, tv, true); if (tv.resourceId <= 0) { throw new IllegalArgumentException("ltRuntimeTheme " + "not defined in the preview theme"); } context.setTheme(tv.resourceId); } } ThemeUtils.java
  49. public class LaunchScreenActivity extends AppCompatActivity {
 
 @Override
 protected void

    onCreate(Bundle savedInstanceState) {
 ThemeUtils.ensureRuntimeTheme(this);
 super.onCreate(savedInstanceState); 
 setContentView(R.layout.activity_launch_screen);
 // ...
 }
 
 } LaunchScreenActivity.java
  50. public class LaunchScreenActivity extends AppCompatActivity {
 
 @Override
 protected void

    onCreate(Bundle savedInstanceState) {
 ThemeUtils.ensureRuntimeTheme(this);
 super.onCreate(savedInstanceState); 
 setContentView(R.layout.activity_launch_screen);
 // ...
 }
 
 } LaunchScreenActivity.java Inherit AppCompatActivity to get AppCompat behaviour public class LaunchScreenActivity extends AppCompatActivity {
  51. public class LaunchScreenActivity extends AppCompatActivity {
 
 @Override
 protected void

    onCreate(Bundle savedInstanceState) {
 ThemeUtils.ensureRuntimeTheme(this);
 super.onCreate(savedInstanceState); 
 setContentView(R.layout.activity_launch_screen);
 // ...
 }
 
 } LaunchScreenActivity.java ThemeUtils.ensureRuntimeTheme(this); Dynamically switch to the runtime theme