Question

Background

I'm developing a themes chooser feature in my "app manager" app, and I've succeeded setting the theme dynamically for each of the activities.

AGAIN : this is not about setting the theme for the activities. This actually works fine for me.

The problem

The acitivties are showing the correct theme, but the application itself, when launching the app, is showing the wrong one, no matter what I do.

This is a problem because when the user opens the app, he will see the background of the theme of the app, and only after a few moments the activity will be shown with the theme the user has chosen.

So, if the application has a white background, and the user has chosen a theme with a black background, the order will be:

Application shows a white background -> activity is starting and shows a black background.

In screenshots:

enter image description here

So this is wrong. In this case, I need it to show black-to-black background.

Only if the user has chosen a Holo-light based theme (which the app has by default), it works fine, as the color matches the one on the activity that is shown right when opening the app.

What I've tried

I had an idea of setting the app's theme to be empty of everything, hoping that no transition will be shown, using something like:

<application
    ...
    android:theme="@android:style/Theme.Translucent.NoTitleBar" >

In fact, some people here suggested a similar solution.

This works, but it causes a bad experience, since on some devices it takes some time till the first activity is shown, and as such, there is a significant amount of time the user sees nothing at all, as if the app isn't being launched.

The question

How do I solve this?

I've already tried setting the theme in the class that extends from Application, but it doesn't do anything, no matter where in this class I call it.

Was it helpful?

Solution 2

Transparent application theme with fade-in animation

My original suggestion was to use a Transparent full screen application theme (no action bar).

Combined with that, I always suggest an alpha-animation to fade across from the application theme to the activity theme. This prevents jarring to the user when the action bar appears.

OP's code would remain almost identical, except for changing the manifest theme, and adding the alpha animation in your onCreate() method of some base activity class as in examples below:


manifest theme defined as:

android:theme="@android:style/Theme.Translucent.NoTitleBar"

base activity onCreate() method:

@Override
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);

    // set your custom theme here before setting layout
    super.setTheme(android.R.style.Theme_Holo_Light_DarkActionBar);

    setContentView(R.layout.activity_main);

    overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
}

basic fade in:

<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:fromAlpha="0.0"
    android:toAlpha="1.0" />

basic fade out (not really needed, but for completeness):

<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:fromAlpha="1.0"
    android:toAlpha="0.0" />

Of course the animation durations here are way longer than you'd put in production - they are long so you can see them in your development stages.


Update #1:

It has been noted subsequently in comments by @EmanuelMoecklin, @androiddeveloper that this was considered. It is also included in answer by dentex. However, as the OP states, the weakness particularly on older devices is that the user gets no feedback when they try to launch the app. It appears the app takes too long to launch.

On KitKat, this is not the case, since the status bar & soft-keys change from transparent to black, while the rest of the screen is still transparent.

Another take on this approach would be to use a full-screen black background as the application theme. This is what was done by Bitspin for Timely, who were bought by Google apparently on the basis of the stunning UI in that app. It seems this method is therefore quite acceptable in many cases.


Update #2:

In order to speed up the perception of the launch, an alternative to the plain black theme is to use a full-screen image with the app's logo in the centre - "splash screen" style. Again fading across to the activity once launched.

This is not possible for the transparent theme, using a transparent full-screen image. Android ignores the transparency of the image (or overlays the transparent image onto a black background). This was pointed out by OP in the comments.

We can either have a transparent theme without an image, or an opaque theme with an image (an interesting topic for another question perhaps).


A note on using Manifest aliases

Another suggestion by @sergio91pt is to use aliases for different activities in the manifest.

While this can be a useful technique in some circumstances, in this case it has some drawbacks:

  1. Any HOME screen shortcut the user has created for the activity will stop working when the main launcher alias is changed i.e. each time the user changes themes.
  2. Some devices / launchers are quite slow to activate & deactivate the different aliases. In my experience this can take seconds (Galaxy Nexus 4.1 iirc), during which time you either have no visible launch icon, or you have 2 icons.
  3. Each possibly theme requires a different alias - this may prove cumbersome if there are many different themes.

OTHER TIPS

A bit late, but this may be the answer. I discovered it by chance.

No entrance activity, no custom animations, no hacking. Simply an attribute in theme. Android buried this deep inside its resources.

Add the following attribute to your app theme:

<!--
  ~ From Theme.NoDisplay, this disables the empty preview window probably
  ~ with an incorrect theme.
  -->
<item name="android:windowDisablePreview">true</item>

And your are done. Enjoy it!

To fix any flickering (action bar, title...) upon app's start, I have set into the manifest

android:theme="@android:style/Theme.NoTitleBar"

for both my main activities (a tab container and a settings activity, from where I switch the themes, based on holo dark and light)

If you use some "launcher activity" or "splash activity" apply Theme.NoTitleBar also for them, then:

having declared Theme.NoTitleBar, for each activity, in onCreate you have to:

  1. set the title properly with setTitle(...) and THEN

  2. set the theme with setTheme(R.style.CustomAppTheme) BEFORE setContentView(...)
    (and you already do this);

This will prevent the flashing of the action bar/title when switching theme (if done "on-the-fly") and upon app's start.

If you want a custom action bar appearance, this means that the default holo action bar will not flash before yours.

The transition color is retrieved from the activity theme on the manifest (or the application if not set).

Currently the only way around this limitation is to create a dummy subclass for each real Activity, eg. MyActivityLight, to declare a different theme. Activity alias won't work, the attribute will be ignored.

For activities with IntentFilter's, you should only maintain one of each "type" enabled, using PackageManager#setComponentEnabledSetting(). Note that the change may take some seconds.

For activities that are started by class name, you can infer the correct prefix according to the user's theme.


So lets suppose you have 2 themes: AppTheme.Dark and AppTheme.Light and some activities. The dark theme is the default one.

Original manifest:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example">
    <application android:theme="@style/AppTheme.Dark">
        <activity 
                android:name=".PrivateActivity" 
                android:exported="false" />

        <activity android:name=".ShowActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
             </intent-filter>
        </activity>
    </application>
</manifest>

Change all activities above as abstract classes and create dummy subclasses suffixed by Light and Dark.

Then the manifest should be changed like this:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example">

    <!-- No application theme -->
    <application>
        <activity android:name=".PrivateActivityDark" 
            android:theme="@style/AppTheme.Dark"
            android:exported="false" />
        <activity android:name=".PrivateActivityLight" 
            android:theme="@style/AppTheme.Light"
            android:exported="false"
            android:enabled="false" />

        <activity 
            android:name=".ShowActivityDark"
            android:theme="@style/AppTheme.Dark">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
             </intent-filter>
        </activity>
        <activity 
            android:name=".ShowActivityLight" 
            android:enabled="false"
            android:theme="@style/AppTheme.Light">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
             </intent-filter>
        </activity>
    </application>
</manifest>

Then you could have something like this to get the themed Activity class, given an abstract Activity:

public static ComponentName getThemedActivityName(
        Context ctx, 
        Class<? extends Activity> clazz) {

    // Probably gets some value off SharedPreferences
    boolean darkTheme = isUsingDarkTheme(ctx);

    String baseName = clazz.getName();
    String name += (darkTheme) ? "Dark" : "Light";
    return new ComponentName(ctx, name);
}

public static void startThemedActivity(
        Activity ctx, 
        Class<? extends Activity> clazz) {
    Intent intent = new Intent();
    intent.setComponent(getThemedActivityName(ctx, clazz));
    ctx.startActivity(intent);
}

And also change the enabled status where needed, when the theme is changed.

public void onThemeChanged(Context ctx, boolean dark) {
    // save theme to SharedPreferences or similar and...

    final PackageManager pm = ctx.getPackageManager();
    final String pckgName = ctx.getPackageName();

    final PackageInfo pckgInfo;
    try {
        final int flags = PackageManager.GET_ACTIVITIES 
                             | PackageManager.GET_DISABLED_COMPONENTS;
        pckgInfo = pm.getPackageInfo(pckgName, flags);
    } catch (PackageManager.NameNotFoundException e) {
        throw new RuntimeException(e);
    }

    final ActivityInfo[] activities = pckgInfo.activities;

    for (ActivityInfo info: activities) {
        final boolean enable;
        if (info.theme == R.style.AppTheme_Light) {
            enable = !dark;
        } else if (info.theme == R.style.AppTheme_Dark) {
           enable = dark;
        } else {
           continue;
        }

        final int state = (enable) ? 
                PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
                PackageManager.COMPONENT_ENABLED_STATE_DISABLED;

        final String name = info.targetActivity;
        final ComponentName cmp = new ComponentName(pckgName, name);
        pm.setComponentEnabledSetting(cmp, state, PackageManager.DONT_KILL_APP);
    }
}

If doing IPC on a loop scares you, you can do this asynchronously on a helper thread, as long as multiple calls to onThemeChanged() run sequentially.

Note that in this example I change the enabled status of all activities (that have a known theme) but only had to do that for the ones with intent filters. If the activities aren't hardcoded its easier this way.

Important Note: As Richard Le Mesurier and other have pointed out, using this technique on Launcher Activities removes or disables the shortcut on the home screen, if it exists. This is just a solution for non launcher Activities.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top