27

Is it possible to make an application-wide setting for the font-size to be used by all views displaying text? I would like to provide a Preference to the user which should allow scaling all text in the app.

Android explicitly allows using the "sp" dimension unit for scalable text, however there is no actual way to set the "user's font size preference" in a global way.

Iterating through all views on Activity instantiation is not really an option ;-)

ge0rg
  • 1,816
  • 1
  • 26
  • 38
  • possible duplicate of [Dynamically change size of multiple textview from code (without a "on disk" xml theme)?](http://stackoverflow.com/questions/4473397/dynamically-change-size-of-multiple-textview-from-code-without-a-on-disk-xml-t) – Cheryl Simon Feb 02 '11 at 17:15
  • This question has been discussed a number of times, see http://stackoverflow.com/questions/4473397/dynamically-change-size-of-multiple-textview-from-code-without-a-on-disk-xml-t for a list of other relevant links. – Cheryl Simon Feb 02 '11 at 17:15

4 Answers4

55

Here it's how I made it for my app. In a few words - in Activity.onCreate() you get resource id of style with specific set of font sizes and apply this style to theme of activity. Then with preferences activity you can switch between these sets.

First of all in values/attrs.xml declare attributes for set of font sizes:

<declare-styleable name="FontStyle">
    <attr name="font_small" format="dimension" />
    <attr name="font_medium" format="dimension" />
    <attr name="font_large" format="dimension" />
    <attr name="font_xlarge" format="dimension" />
</declare-styleable>

Then in values/styles.xml declare few sets of font sizes:

<style name="FontStyle">
</style>

<style name="FontStyle.Small">
    <item name="font_small">14sp</item>
    <item name="font_medium">16sp</item>
    <item name="font_large">18sp</item>
    <item name="font_xlarge">20sp</item>
</style>

<style name="FontStyle.Medium">
    <item name="font_small">18sp</item>
    <item name="font_medium">20sp</item>
    <item name="font_large">22sp</item>
    <item name="font_xlarge">24sp</item>
</style>

<style name="FontStyle.Large">
    <item name="font_small">26sp</item>
    <item name="font_medium">28sp</item>
    <item name="font_large">30sp</item>
    <item name="font_xlarge">32sp</item>
</style>

Then in onCreate() method of every activity add:

getTheme().applyStyle(new Preferences(this).getFontStyle().getResId(), true);

where Preferences is a facade to SharedPreferences object:

public class Preferences {
    private final static String FONT_STYLE = "FONT_STYLE";

    private final Context context;

    public Preferences(Context context) {
        this.context = context;
    }

    protected SharedPreferences open() {
        return context.getSharedPreferences("prefs", Context.MODE_PRIVATE);
    }

    protected Editor edit() {
        return open().edit();
    }

    public FontStyle getFontStyle() {
        return FontStyle.valueOf(open().getString(FONT_STYLE,
            FontStyle.Medium.name()));
    }

    public void setFontStyle(FontStyle style) {
        edit().putString(FONT_STYLE, style.name()).commit();
    }
}

and FontStyle is:

public enum FontStyle {
    Small(R.style.FontStyle_Small, "Small"), 
    Medium(R.style.FontStyle_Medium, "Medium"), 
    Large(R.style.FontStyle_Large, "Large");

    private int resId;
    private String title;

    public int getResId() {
        return resId;
    }

    public String getTitle() {
        return title;
    }

    FontStyle(int resId, String title) {
        this.resId = resId;
        this.title = title;
    }
}

And FontStyle.values() is used as items for Spinner in your PreferencesActivity. That's how mine looks like:

public class PreferencesActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
    getTheme().applyStyle(new Preferences(this).getFontStyle().getResId(), true);
    super.onCreate(savedInstanceState);

    setContentView(R.layout.preferences);

    Preferences prefs = new Preferences(this);

    Spinner fontStylesView = (Spinner) findViewById(R.id.font_styles);
    FontStylesAdapter adapter = new FontStylesAdapter(this,
            R.layout.font_styles_row, FontStyle.values());
    fontStylesView.setAdapter(adapter);

    fontStylesView.setSelection(prefs.getFontStyle().ordinal());
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getSupportMenuInflater();
    inflater.inflate(R.menu.preferences, menu);
    return super.onCreateOptionsMenu(menu);
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
    case R.id.menu_done:
        onMenuDone();
        finish();
        return true;
    case R.id.menu_cancel:
        finish();
        return true;
    default:
        return false;
    }
}

private void onMenuDone() {
    Preferences prefs = new Preferences(this);

    Spinner fontStylesView = (Spinner) findViewById(R.id.font_styles);
    prefs.setFontStyle((FontStyle) fontStylesView.getSelectedItem());
}
}

And finally you can use your font size preferences:

<TextView android:textSize="?attr/font_large" />

Or I prefer using styles, in values/styles.xml add:

<style name="Label" parent="@android:style/Widget.TextView">
    <item name="android:textSize">?attr/font_medium</item>
    <item name="android:layout_width">wrap_content</item>
    <item name="android:layout_height">wrap_content</item>
</style>

<style name="Label.XLarge">
    <item name="android:textSize">?attr/font_xlarge</item>
</style>

And you can use it in this way:

<TextView style="@style/Label.XLarge" />

I hope my answer will help you.

mixel
  • 25,177
  • 13
  • 126
  • 165
  • Suppose I have some theme already set to my activity. Now I want to set FontStyle.Medium as default to all the activities. If I define `textSize="?attr/font_medium"`, it crashes as it is unknown to the activity. So this works only if I call `getTheme().applyStyle()` is it? – Sudarshan Bhat Jan 16 '13 at 14:21
  • Yes. With declare-styleable you declare some attributes. In styles you define values for these attributes. And then you need to apply one of these styles to activity theme. Which one is defined by saved preferences. – mixel Jan 17 '13 at 08:43
  • Is "applyStyle" take effect immediately?Or do I miss something? – wangqi060934 May 29 '15 at 06:41
  • @mixel This is not working in an `ArrayAdapter`. Error is: `Binary XML file line #53: Error inflating class android.widget.TextView` – tread Jul 13 '15 at 09:37
  • @surfer190 Something wrong with your layout. Please attach a link to your project (on GitHub, for example). – mixel Jul 13 '15 at 12:54
  • @mixel I sorted it out by calling `getTheme().applyStyle()` in the constructor of the `ArrayAdapter` – tread Jul 13 '15 at 13:49
  • @surfer190 You should call `getTheme().applyStyle()` in `Activity.onCreate()` – mixel Jul 13 '15 at 14:03
  • @mixel I do call it `onCreate()` but then there is a fragment opened with a navigationDrawer that shows a `ListView` with `ArrayAdapter` and it fails if it isn't in the Constrcutor of the `ArrayAdapter` – tread Jul 13 '15 at 15:06
  • @surfer190 If you solved your issue then ok. If not, you can create test project with minimal amount of code required to reproduce your issue, place it on GitHub and I'll take a look. – mixel Jul 13 '15 at 15:26
  • This code works perfectly. But how do I restart the application instead of finishing it? – azurh Sep 05 '15 at 21:09
  • @azurth You need to restart Activity: `Intent intent = getIntent(); finish(); startActivity(intent);` – mixel Sep 05 '15 at 22:31
  • Is it possible to make changes take effect at runtime? So I don't have to restart the app? – Robin Dijkhof Nov 02 '16 at 22:04
  • @RobinDijkhof No, you have to restart activity. – mixel Nov 02 '16 at 22:24
  • @mixel have used this in the past and it works very well but I'm wondering: is this still the best way to do this on newest android? I would think there would be a less 'hacky' way of doing this by now as long as you don't have to support older devices. Is there? – Aspiring Dev Feb 24 '17 at 14:09
  • @rpgmaker Yes, it's the best way. Android style styleable attributes give you flexibility in implementing this and there is no 'hacking' - It's just simple Android SDK features. – mixel Feb 24 '17 at 14:31
5

Yes, it's possible. To do that you need to:

  1. Declare your own class extending TextView
  2. Use in all your dialogs/activities only it

Like:

public class SimpleTextView extends TextView
{
    private static final float DEFAULT_TEXT_SIZE=12.0;
    private static float textSize=DEFAULT_TEXT_SIZE;

    public SimpleTextView(Context context)
    {
        super(context);
        this.setTextSize(textSize);
    }

    public SimpleTextView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        this.setTextSize(textSize);
    }

    public SimpleTextView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
        this.setTextSize(textSize);
    }

    public static void setGlobalSize(float size)
    {
        textSize=size;
    }

    public static float getGlobalSize()
    {
        return textSize;
    }
}

And now whereever you're you can globally change all text sizes to 20 in all textviews just calling:

SimpleTextView.setGlobalTextSize(20);
Barmaley
  • 16,638
  • 18
  • 73
  • 146
  • 4
    That works for text that appears in `TextView`s, but what about all of the `TextView` subclasses like `EditText`, `Button`, `CheckedTextView` etc.? You would basically need to create subclasses of every Widget type that you use.. – Cheryl Simon Feb 02 '11 at 18:10
  • @Mayra: that right - I was forced to do so... I mean it's not great fun, but result is fascinating :) – Barmaley Feb 02 '11 at 19:46
  • This solution requires to derive from any View class with text display, and some more work needs to be done to add a scaling factor (so I can have a 10sp and a 20sp text scale while keeping the 2x size relation), but I see where it is heading, thanks. – ge0rg Feb 03 '11 at 00:31
  • @barmaley i followed your solution but i am not able scale all the textview in application.Does it works? – Krishna Shrestha Nov 19 '12 at 04:31
  • It is not working for me. I had already fix setGlobalTextSize to setGlobalSize but no effect in my application. Have you tested it. – Robi Kumar Tomar Apr 02 '14 at 11:11
  • @barmaley This is not working... can you please update the answer? – Sandeep Yohans Jul 21 '18 at 13:23
1

For those who have problems with inflating views with custom attributes from @mixels answer.

If your views are in fragment, you need to apply FontStyle also in fragment's onCreateView() or set values of these attributes in application's theme.

For more detailes see my aswer here.

Community
  • 1
  • 1
Tomask
  • 2,344
  • 3
  • 27
  • 37
1

Shooting from the hip here's idea to consider (no custom TextView implementation required)

  1. Declare property something like UNIVERSAL_FONT_SIZE with the idea that it can be changed from settings but will be retained between app invocations
  2. In onCreate method of each of your Activities get value of that property and save as a field
  3. Make your code use that for each text-resizable component
  4. Nothing will actually stop you from creating several properties such as BUTTONS_TXT_SIZE, TEXT_SIZE, LIST_TXT_SIZE, etc. and then have logic that takes for example percent of text increase and calculated proper sizes for each type of control (since you may have different sizes for different controls)

Along the same lines, say you want to make this to work dynamically? Create a simple class (say TextSetter) that holds internal list and have 3 methods: add, remove and setSize

  1. In Activity#onCreate identify each control you want to adjust and use TextSetter#set to add it to the list
  2. When user wants to increase/decrease font size maybe from the menu, when you handle that just execute TextSetter#setSize in which you will loop through the list of controls, detect which type it is and adjust text size accordingly
Bostone
  • 36,858
  • 39
  • 167
  • 227
  • This is going into the right direction. I already tried to override the system wide [font scale](http://android.git.kernel.org/?p=platform/development.git;a=blob;f=apps/SpareParts/src/com/android/spare_parts/SpareParts.java;h=099f27a9ba76e8a39accf36b45bb699e69d05b9c;hb=HEAD#l187), however the `ActivityManagerNative` seems to be only usable from inside Android itself. The SpareParts example app, which provides the linked code, allows to set the font in an app-global way. I do not want to employ dirty tricks to get access to that API, but it seems there is no really elegant solution to that. – ge0rg Apr 21 '11 at 12:04