Android: application-wide font-size preference
Asked Answered
C

4

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 ;-)

Carnay answered 2/2, 2011 at 16:41 Comment(2)
possible duplicate of Dynamically change size of multiple textview from code (without a "on disk" xml theme)?Richie
This question has been discussed a number of times, see #4473897 for a list of other relevant links.Richie
N
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.

Northington answered 25/9, 2012 at 22:24 Comment(15)
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</item>", it crashes as it is unknown to the activity. So this works only if I call getTheme().applyStyle() is it?Hoyt
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.Northington
Is "applyStyle" take effect immediately?Or do I miss something?Shadchan
@Northington This is not working in an ArrayAdapter. Error is: Binary XML file line #53: Error inflating class android.widget.TextViewBlossom
@surfer190 Something wrong with your layout. Please attach a link to your project (on GitHub, for example).Northington
@Northington I sorted it out by calling getTheme().applyStyle() in the constructor of the ArrayAdapterBlossom
@surfer190 You should call getTheme().applyStyle() in Activity.onCreate()Northington
@Northington 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 ArrayAdapterBlossom
@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.Northington
This code works perfectly. But how do I restart the application instead of finishing it?Misestimate
@azurth You need to restart Activity: Intent intent = getIntent(); finish(); startActivity(intent);Northington
Is it possible to make changes take effect at runtime? So I don't have to restart the app?Wraparound
@RobinDijkhof No, you have to restart activity.Northington
@Northington 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?Araarab
@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.Northington
L
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);
Lanceted answered 2/2, 2011 at 17:59 Comment(6)
That works for text that appears in TextViews, 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..Richie
@Mayra: that right - I was forced to do so... I mean it's not great fun, but result is fascinating :)Lanceted
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.Carnay
@barmaley i followed your solution but i am not able scale all the textview in application.Does it works?Babu
It is not working for me. I had already fix setGlobalTextSize to setGlobalSize but no effect in my application. Have you tested it.Oberg
@barmaley This is not working... can you please update the answer?Decentralization
T
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
Towill answered 20/4, 2011 at 21:24 Comment(1)
This is going into the right direction. I already tried to override the system wide font scale, 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.Carnay
L
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.

Latinity answered 13/1, 2016 at 12:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.