8

I'm trying to understand the sp unit in Android, but I can't really figure out how it works. The documentation say:

sp

Scale-independent Pixels - This is like the dp unit, but it is also scaled by the user's font size preference. It is recommend you use this unit when specifying font sizes, so they will be adjusted for both the screen density and the user's preference.

Using DisplayMetrics it is possible to get the scaledDensity which is:

A scaling factor for fonts displayed on the display. This is the same as density, except that it may be adjusted in smaller increments at runtime based on a user preference for the font size.

So given this information I assume that the scaledDensity will change as I change the system font size and that I can use this information to know the ratio between 1dp and 1sp (scaledDensity / density).

However when I'm testing this on my phone (Nexus 4) I am not getting the results I'm expecting. DisplayMetrics.scaledDensity always returns the same value (equal to DisplayMetrics.density) regardless of what font size I have set my phone to use (via settings -> accessibility -> use large font or settings -> display -> font size).

When I change the font settings my sp specified fonts changes size, so the sp unit works as expected but the scaledDensity value doesn't change at all.

The code I'm using to get the scaledDensity is:

DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
metrics.scaledDensity

Am I doing something wrong or have I misunderstood what DisplayMetrics.scaledDensity actually does?

Update

Here are three screenshots that illustrates what I mean:

enter image description here

And this is the code for the application:

DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);

String result = "\n"
    + "density : " + metrics.density + " (24 dp = " + 24 * metrics.density + " px)\n"
    + "scaledDensity: " + metrics.scaledDensity + " (24 sp = " + 24 * metrics.scaledDensity + " px)\n";

((TextView) this.findViewById(R.id. label)).setText(result);
((TextView) this.findViewById(R.id. sp)).setTextSize(android.util.TypedValue. COMPLEX_UNIT_SP , 24);
((TextView) this.findViewById(R.id.dp )).setTextSize(android.util.TypedValue. COMPLEX_UNIT_DIP, 24);

I want to be able to calculate the amount of pixels needed y-wise to draw a certain string. If I specify the size of the string using dp I can easily calculate the required pixels using displayMetrics.density. But when I use sp and try to calculate the amount of pixels using displayMetrics.scaledDensity I do not get the correct values.

nibarius
  • 4,007
  • 2
  • 38
  • 56

2 Answers2

17

I was able to figure out this myself. After digging trough the Android source code I found that the scaledDensity property is calculated as scaledDensity = density * fontScale where fontScale depends on the system setting for font size. So this was just as I thought it should be.

The reason why scaledDensity always had the same value for me regardless of font size was because I was using the DisplayMetrics for the wrong Display. I got the DisplayMetrics from the default display and regarding this the documentation say:

Returns the Display upon which this WindowManager instance will create new windows.

Despite the name of this method, the display that is returned is not necessarily the primary display of the system (see DEFAULT_DISPLAY). The returned display could instead be a secondary display that this window manager instance is managing. Think of it as the display that this WindowManager instance uses by default.

In this case the default display is not the same display as the primary display of the system, so when I change the system font setting that setting is changed on the primary display and not on the default display. If I update my code to get the DisplayMetrics for the display that is actually used scaledDensity returns the expected values scaled according to the system font settings.

With the following updated code that uses getResources().getDisplayMetrics() everything works as expected:

DisplayMetrics metrics = getResources().getDisplayMetrics();
String result = "\n"
    + "density : " + metrics.density + " (24 dp = " + 24 * metrics.density + " px)\n"
    + "scaledDensity: " + metrics.scaledDensity + " (24 sp = " + 24 * metrics.scaledDensity + " px)\n" ;

((TextView) this.findViewById(R.id.label)).setText(result);
((TextView) this.findViewById(R.id.sp)).setTextSize(TypedValue.COMPLEX_UNIT_SP, 24);
((TextView) this.findViewById(R.id.dp)).setTextSize(TypedValue.COMPLEX_UNIT_DIP, 24);

With this way of getting the display metrics this is the result I get (observe how the scaled density changes with font size):

enter image description here

nibarius
  • 4,007
  • 2
  • 38
  • 56
3

Scaled density is a unit of measurement not a value of font-size. Let me try to give you an example, feel free to follow up with a comment if you would like more explanation.

Say you have a font size 18sp. This needs to get converted to pixels in order to display on the screen. The following calculation is made: pixels = fontSize * scaledDensity * scale. Now, when you change your phone's settings, you are changing the scale. This results in the same calculation being made, but let's say with a scale of 1.5 now instead of 1. The unit of scaledDensity is not changed because your screen's pixel density has not changed.

I hope that helps!

Dan Harms
  • 4,725
  • 2
  • 18
  • 28
  • I'm not sure I understand. In my application I set the font size of a TextView to 12sp and I have the system font size set to "normal". Since scaledDensity returns 2.0 in this case I assume the amount of pixels required to draw this text is 12 * 2.0 = 24. Then I go in to the system settings and change font size to huge. When I restart my app I can see that the text in the TextView is drawn much bigger on the screen even though the font size of the TextView is still set to 12sp. So here I expect the scaledDensity value to have increased to account for the larger amount of pixels drawn. – nibarius Apr 07 '14 at 15:31
  • Also, documentation of scaledDensity say: "This is the same as density, except that it *may be adjusted in smaller increments at runtime based on a user preference for the font size*." So it should be able to change even though the pixel density of the screen is constant. – nibarius Apr 07 '14 at 15:34
  • It looks like I was missing a factor in calculating font size when using sp. Take a look at my edit. – Dan Harms Apr 07 '14 at 15:36
  • Thanks for the update, but why does the documentation say that scaledDensity may be changed during runtime depending on user preferences for font size? And what's the difference between scaledDensity and density if scaledDensity depends only on the screen's pixel density? – nibarius Apr 07 '14 at 15:44
  • From how I understand it, density and scaledDensity do not reflect the true density of the screen, but rather are stepped up or down specified increments based on real screen density. The way I read that is Android has the ability to subdivide that increment for scaledDensity if the user has set their font preferences in one extreme or the other to improve the results. I can't say that this is 100% what that means, but that is how I understand it. – Dan Harms Apr 07 '14 at 15:52
  • It's correct that they do not represent the true density of the screen. I need to try this theory tomorrow on a couple of phones and see if I can find any phone where scaledDensity is not set to density. – nibarius Apr 07 '14 at 16:02
  • Is there something about your application that this will cause issues, or are you just trying to understand the moving pieces behind the scenes? – Dan Harms Apr 07 '14 at 16:34
  • I'm experimenting with using the inch/mm units instead of dp for a component that contain text to get a fixed physical size regardless of device. So to get the text to scale nicely in this component I want to give the text base value in inch/mm/pt and then scale it the same amount as the sp unit is scaled based on the current device settings for fonts. If scaledDensity would describe how the sp unit scales I would be able to do this, but since it doesn't I can't scale it. But since this doesn't seem to be possible I just want to know how things work and what the purpose of scaledDensity is. – nibarius Apr 15 '14 at 09:17
  • @nibarius it looks like this answer was helpful to you. It didn't answer your question to the degree of accuracy that you wanted, but it did help you find the correct answer (and this answer is basically a subset of your answer). So you should upvote it. Leave your answer as "accepted". Kind of give credit where credit is due. – Christopher Rucinski Nov 11 '15 at 14:17
  • 2
    I was scaling images based on the screensize (calculated by square rooting the sum of the squares of height/ydpi and width/xdpi) and getting inconsistencies. When I factored in the scaledDensity, everything became consistent. So it's not just for text. – Moe Singh Jan 05 '17 at 21:45