10

Let's say I have an application with 2 themes: masculine and feminine. The themes simply change out the color palette and a few drawables to appeal to the user's preferred tastes.

Many thanks to http://www.androidengineer.com/2010/06/using-themes-in-android-applications.html for his hints at making that work.

But now lets say I want to get a little cuter with the app and not only change the colors and drawables, but I also want to change the strings. For instance, I might want to add a pirate theme and then "Submit" would be "Arrrrgh!"

So, my basic question is: How can I change the strings throughout my app via user selectable themes?

Edit:

Making this up: the app has 12 buttons and 32 text views I'd like to have theme dependent and I'd like to accomplish this without a giant mapping or a slew of custom attrs.

All 3 of the current solutions will work. Looking for something cleaner though I don't know that such a beast exists.

Bill Mote
  • 12,644
  • 7
  • 58
  • 82
  • http://www.41post.com/4941/programming/android-get-string-resource-at-another-xml-namespace – Illegal Argument Jun 21 '14 at 16:06
  • @IllegalArgument I must be missing something in that blog post. I don't see why he's even doing what he's doing because he's still defining 3 separate strings and has 3 separate buttons pointing to those strings. – Bill Mote Jun 21 '14 at 17:21

5 Answers5

12

Yes, it can be done, and here's how: first you'll have to define a theme attribute, like so:

<attr name="myStringAttr" format="string|reference" />

Then, in your themes, add this line

<item name="myStringAttr">Yarrrrr!</item>

or

<item name="myStringAttr">@string/yarrrrr</item>

You can then use this attribute in an XML file like so (note the ? instead of @).

<TextView 
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="?attr/myStringAttr" />

or, from code, like so:

public CharSequence resolveMyStringAttr(Context context) 
{
    Theme theme = context.getTheme();
    TypedValue value = new TypedValue();

    if (!theme.resolveAttribute(R.attr.myStringAttr, value, true)) {
        return null;
    }

    return value.string;
}
jclehner
  • 1,440
  • 10
  • 18
  • That's fantastic. Does it scale? So, there are (making this up) 12 buttons and 32 TextViews. If I'm reading your solution correctly; I'd have to have a special attr for every string I wanted to make theme dependent, correct? – Bill Mote Jun 21 '14 at 16:54
  • 1
    Well, that's the downside of this approach. You'd have to add 44 new attributes in that case, but I can't think of any other way to do it using just XML. – jclehner Jun 21 '14 at 16:57
  • Much appreciated. I couldn't think of any other way either, but I didn't want to stymie anyone's creativity by seeding them with an answer :) – Bill Mote Jun 21 '14 at 17:01
  • aren't you then loosing a translation ability? – Ivan Jul 11 '22 at 11:23
2

Let's say I have an application with 2 themes: masculine and feminine. The themes simply change out the color palette and a few drawables to appeal to the user's preferred tastes.

How about we pretend that you're doing something else? This is a design anti-pattern, associating particular colors based on gender (e.g., "girls like pink").

This is not to say that your technical objective is bad, just that this is a really stereotypical example.

For instance, I might want to add a pirate theme and then "Submit" would be "Arrrrgh!"

Only if "Cancel" maps to "Avast!".

How can I change the strings throughout my app via user selectable themes?

You have not said where those strings are coming from. Are they string resources? Database entries? Ones that you are retrieving from a Web service? Something else?

I will assume for the moment that these are string resources. By definition, you will need to have N copies of the strings, one per theme.

Since gender and piratical status are not things tracked by Android as possible resource set qualifiers, you can't have those string resources be in different resource sets. While they could be in different files (e.g., res/values/strings_theme1.xml), filenames are not part of resource identifiers for strings. So, you will wind up having to use some sort of prefix/suffix to keep track of which strings belong with which themes (e.g., @string/btn_submit_theme1).

If these strings are not changing at runtime -- it's just whatever is in your layout resource -- you could take a page from Chris Jenkins' Calligraphy library. He has his own subclass of LayoutInflater, used to overload some of the standard XML attributes. In his case, his focus is on android:fontFamily, where he supports that mapping to a font file in assets.

In your case, you could overload android:text. In your layout file, rather than it pointing to any of your actual strings, you could have it be the base name of your desired string resource, sans any theme identifier (e.g., if the real strings are @string/btn_submit_theme1 and kin, you could have android:text="btn_submit"). Your LayoutInflater subclass would grab that value, append the theme name suffix, use getIdentifier() on your Resources to look up the actual string resource ID, and from there get the string tied to your theme.

A variation on this would be to put the base name in android:tag instead of android:text. android:text might point to one of your real string resources, to help with GUI design and such. Your LayoutInflater would grab the tag and use that to derive the right string at runtime.

If you will be replacing text with other text pulled from theme-based string resources, you could isolate your get-the-string-given-the-base-name logic into a static utility method somewhere that you could apply.

While getting this right initially will take a bit of work, it will scale to arbitrary complexity, in terms of the number of affected UI widgets and strings. You still have to remember to add values for all themes for any new strings you define (bonus points for creating a custom Lint check or Gradle task for validating this).

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • I guess I could have used musky/flowery rather than masculine/feminine, but I did state "user's" preference so the gender of the user may have no bearing on their theme decision ;) You were correct in assuming I'd like to use string resources. I like where you took this and that seems reasonably doable. When I get some code thrown together I'll post a follow up here. Regards! – Bill Mote Jun 22 '14 at 19:57
1

Since a resource is just an int at heart you could store a number of them at runtime and them substitute them in procedurally as you use them.


<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="OK_NORMAL">Okay</string>
    <string name="OK_PIRATE">Yaaarrrr!</string>
    <string name="OK_NINJA">Hooooaaa!</string>
</resources>

public enum ThemeMode {
    PIRATE,
    NINJA,
    NORMAL;
}

public class MyThemeStrings {
    public static int OK_PIRATE = R.string.OK_PIRATE;
    public static int OK_NINJA = R.string.OK_NINJA;
    public static int OK_NORMAL = R.string.OK_NORMAL;
}

public setOkButtonText(ThemeMode themeMode) {
    // buttonOk is instantiated elsewhere
    switch (themeMode) {
        case PIRATE:
            buttonOk.setText(MyThemeStrings.OK_PIRATE);
            break;
        case NINJA:
            buttonOk.setText(MyThemeStrings.OK_NINJA);
            break; 
        default:
            Log.e(TAG, "Unhandled ThemeMode: " + themeMode.name());
            // no break here so it flows into the NORMAL base case as a default
        case NORMAL:
            buttonOk.setText(MyThemeStrings.OK_NORMAL);
            break;
    }
}

Although, having written all that, there is probably a better way to do all this through separate XML files. I'll look into it now and write a second solution if I find one.

indivisible
  • 4,892
  • 4
  • 31
  • 50
  • I fear that a giant mapping is likely what I'm in store for. Your solution will work, but damn could that end up being a lot of code :) – Bill Mote Jun 21 '14 at 16:58
  • 1
    As long as you can tidy/abstract it all away into its own package and string_something.xml files then just expose a single getter method accepting an `enum Theme` argument for each String I'd say it's not too unwieldy an option and does allow for internationalisation with separate `values-locale` folders. But yeah, there's a couple of things to keep an eye on when extending the functionality. – indivisible Jun 21 '14 at 17:04
1

Ok, I have a second option which may actually be easier to maintain and keep your code cleaner although it may be more resource hungry due to loading an array for each String. I've not benchmarked it but will offer it as another choice but I wouldn't use it if you offer too many theme choices.


public enum ThemeMode {
    NORMAL(0),
    PIRATE(1),
    NINJA(2);

    private int index;
    private ThemeMode(int index) {
        this.index = index;
    }

    public int getIndex() {
        return this.index;
    }
}

<resources>
    <!-- ALWAYS define strings in the correct order according to 
         the index values defined in the enum -->
    <string-array
        name="OK_ARRAY">
        <item>OK</item>
        <item>Yaarrrr!</item>
        <item>Hooaaaa!</item>
    </string-array>
    <string-array
        name="CANCEL_ARRAY">
        <item>Cancel</item>
        <item>Naarrrrr!</item>
        <item>Wha!</item>
    </string-array>
</resources>

public setButtonTexts(Context context, ThemeMode themeMode) {
    // buttons are instantiated elsewhere
    buttonOk.setText(context.getResources()
            .getStringArray(R.array.CANCEL_ARRAY)[themeMode.getIndex()]);
    buttonCancel.setText(context.getResources()
            .getStringArray(R.array.OK_ARRAY)[themeMode.getIndex()]);
}
indivisible
  • 4,892
  • 4
  • 31
  • 50
  • That cuts down on a little code. Nice. I'm going to keep digging for a while, but this might be my go-to solution. – Bill Mote Jun 21 '14 at 16:59
  • 1
    Like I said at the top though, be careful about using this one with too many theme choices as each theme adds another String to every array. Depending on how many you load up at any time and how many themes you want to offer it could grow your app's resource usage a noticeable amount. If you plan to do this for lots of resources throughout your app it would definitely be worth doing some benchmarking on the three solutions offered here. If it's just for a couple of places as a minor bit of novelty than just go with the easiest to implement as it wouldn't matter so much. – indivisible Jun 21 '14 at 17:08
1

So, I have not had a chance to test this, but from reading the file on Locale it looks like you can create your own location.

http://developer.android.com/reference/java/util/Locale.html

and help from another stackoverflow

Set Locale programmatically

A little bit of combination leads me to:

Locale pirate = new Locale("Pirate");
Configuration config = new Configuration();
config.locale = pirate;
this.getActivity().getBaseContext().getResources()
   .updateConfiguration(config, 
    this.getActivity().getBaseContext().getResources().getDisplayMetrics());

I do believe this would let you have res/values-pirate/strings as an actual valid resource that would get used when you are a pirate. Any strings or settings you don't override would then revert to the res/values/... by default so you could do this for as many themes as you want. Again assuming it works.

Community
  • 1
  • 1
Zeppie
  • 179
  • 6
  • That's pretty fantastic. I'm going to have to put together an example for this answer ;) – Bill Mote Feb 21 '15 at 14:49
  • If it works, I think it will be a lot less involved than the other methods. You just need to select this new 'language' somewhere, and then maybe save it as part of SharedPreferences for that app. – Zeppie Feb 21 '15 at 16:33
  • This doesn't work. Android will not allow you to create a `res/values-pirate/` directory...Arrgh! – thepner Nov 26 '19 at 01:59