How to set the theme for the application, to avoid wrong color transitions?

前端 未结 4 1420
天命终不由人
天命终不由人 2020-12-14 03:21

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.

<
相关标签:
4条回答
  • 2020-12-14 04:04

    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.
    0 讨论(0)
  • 2020-12-14 04:07

    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.

    0 讨论(0)
  • 2020-12-14 04:17

    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.

    0 讨论(0)
  • 2020-12-14 04:18

    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!

    0 讨论(0)
提交回复
热议问题