20

I'm developing an app with React Native for both iOS and Android, and I am trying to prevent device-specific scaling of the display in the app.

For text/font size scaling, putting the following code in the root-level App.js file solves the issue for both iOS and Android:

if (Text.defaultProps == null) {
    Text.defaultProps = {};
}
Text.defaultProps.allowFontScaling = false;

However, Android devices have the following Display size setting that is still being applied:

Android Device Display size setting

I've tried (unsuccessfully) to piece together a variety of "solutions" to this issue that I've found in answers to the following questions:

Change the system display size programatically Android N

Disabling an app or activity zoom if Setting -> Display -> Display size changed to Large or small

how to prevent system font-size changing effects to android application?

I've often found references to a BaseActivity class that extends the Activity class. My understanding is that it is inside of that class where I would be writing a method (let's call it adjustDisplayScale) to make changes to the Configuration of the Context that I get from Resources, and that then I would be calling adjustDisplayScale within the onCreate() method after super.onCreate() in the MainApplication.java file.

As of now, in this directory I just have two files - MainApplication.java and MainActivity.java.

I've attempted creating a new Module and associated Package file to implement adjustDisplayScale following these instructions and it did not work: https://facebook.github.io/react-native/docs/text.html

I've attempted placing implementing the functionality of adjustDisplayScale within the onCreate() like this and it did not work:

@Override
public void onCreate() {
    super.onCreate();

    Context context = getApplicationContext();
    Resources res = context.getResources();
    Configuration configuration = res.getConfiguration();

    configuration.fontScale = 1f;
    DisplayMetrics metrics = res.getDisplayMetrics();

    WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
    wm.getDefaultDisplay().getMetrics(metrics);
    metrics.scaledDensity = 1f;
    configuration.densityDpi = (int) res.getDisplayMetrics().xdpi;
    context = context.createConfigurationContext(configuration);

    SoLoader.init(this, /* native exopackage */ false);
}

A potentially promising answer included the following:

protected override void AttachBaseContext(Context @base) {
    var configuration = new Configuration(@base.Resources.Configuration);
    configuration.FontScale = 1f;
    var config =  Application.Context.CreateConfigurationContext(configuration);
    base.AttachBaseContext(config);
}

But when I tried to utilize this, I got errors about not recognizing the symbol @base.

Some background... I've done 99% of my work on this project in JavaScript / React Native and I have almost no understanding about things like Resources, Context, Configuration, and DisplayMetrics associated with Android development AND the last time I wrote code in Java was 10 years ago. I've spent a number of agonizing hours trying to figure this out and any help would be greatly appreciated.

ps. I am well-aware that accessibility settings exist for a good reason so please spare me the diatribe I've seen in so many "answers" on why I need to fix my UI to work with accessibility settings rather than disable them.

guipivoto
  • 18,327
  • 9
  • 60
  • 75
Emilio
  • 1,814
  • 3
  • 9
  • 11

1 Answers1

18

NOTE

I strongly discourage applying such a solution. In a certain way, Screen Zoom just "emulates" different screen sizes and densities in the same device. So, if your app can't handle well a specific screen zoom level, it means that your app may not be displayed correctly on a real screen out there. If your app can't support screen changes, tell the user about it...

There are some docs about screen sizes in the Android Developer and that's how you should handle different screen sizes.

On Android 12, it seems this the context created via context.createConfigurationContext(configuration) is imuttable. So, you may have problems on Android 12 when rotating the device, for example. context.getResources().getxxxxx() may return portrait resources (because the context was created in portrait) instead of landscape resources (the new orientation)

Support Different Screen Sizes

supports-screens-element

The answer below is just a "hack" where I tried to circumvent the screen zoom feature. I don't use that on my apps and I strongly recommend dealing with the screen zoom in a more conventional way.

Answer

My first answer does not work if you change the screen resolution. On Samsung devices, you can change the screen zoom but you can also change the screen resolution on some models (Settings->Display->Screen Resolution-> HD, FHD, WQHD etc).

So, I came up with a different code which seems to work with that feature as well. Just, please, note I can't fully test this code since I don't have too many devices to test. On those devices I tested, it seems to work.

One additional note. Ideally, you don't need to use such kind of code to circumvent the screen zoom. In a certain way, the screen zoom is just "simulating" bigger or smaller screens. So, if your app properly supports different screen sizes, you don't need to completely "disable" the screen zoom.

public class BaseActivity extends AppCompatActivity {

    @TargetApi(Build.VERSION_CODES.N)
    private static final int[] ORDERED_DENSITY_DP_N = {
            DisplayMetrics.DENSITY_LOW,
            DisplayMetrics.DENSITY_MEDIUM,
            DisplayMetrics.DENSITY_TV,
            DisplayMetrics.DENSITY_HIGH,
            DisplayMetrics.DENSITY_280,
            DisplayMetrics.DENSITY_XHIGH,
            DisplayMetrics.DENSITY_360,
            DisplayMetrics.DENSITY_400,
            DisplayMetrics.DENSITY_420,
            DisplayMetrics.DENSITY_XXHIGH,
            DisplayMetrics.DENSITY_560,
            DisplayMetrics.DENSITY_XXXHIGH
    };

    @TargetApi(Build.VERSION_CODES.N_MR1)
    private static final int[] ORDERED_DENSITY_DP_N_MR1 = {
            DisplayMetrics.DENSITY_LOW,
            DisplayMetrics.DENSITY_MEDIUM,
            DisplayMetrics.DENSITY_TV,
            DisplayMetrics.DENSITY_HIGH,
            DisplayMetrics.DENSITY_260,
            DisplayMetrics.DENSITY_280,
            DisplayMetrics.DENSITY_XHIGH,
            DisplayMetrics.DENSITY_340,
            DisplayMetrics.DENSITY_360,
            DisplayMetrics.DENSITY_400,
            DisplayMetrics.DENSITY_420,
            DisplayMetrics.DENSITY_XXHIGH,
            DisplayMetrics.DENSITY_560,
            DisplayMetrics.DENSITY_XXXHIGH
    };

    @TargetApi(Build.VERSION_CODES.P)
    private static final int[] ORDERED_DENSITY_DP_P = {
            DisplayMetrics.DENSITY_LOW,
            DisplayMetrics.DENSITY_MEDIUM,
            DisplayMetrics.DENSITY_TV,
            DisplayMetrics.DENSITY_HIGH,
            DisplayMetrics.DENSITY_260,
            DisplayMetrics.DENSITY_280,
            DisplayMetrics.DENSITY_XHIGH,
            DisplayMetrics.DENSITY_340,
            DisplayMetrics.DENSITY_360,
            DisplayMetrics.DENSITY_400,
            DisplayMetrics.DENSITY_420,
            DisplayMetrics.DENSITY_440,
            DisplayMetrics.DENSITY_XXHIGH,
            DisplayMetrics.DENSITY_560,
            DisplayMetrics.DENSITY_XXXHIGH
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.v("TESTS", "Dimension: " + getResources().getDimension(R.dimen.test_dimension));
    }

    @Override
    protected void attachBaseContext(final Context baseContext) {

        Context newContext = baseContext;

        // Screen zoom is supported from API 24+
        if(Build.VERSION.SDK_INT >= VERSION_CODES.N) {

            Resources resources = baseContext.getResources();
            DisplayMetrics displayMetrics = resources.getDisplayMetrics();
            Configuration configuration = resources.getConfiguration();

            Log.v("TESTS", "attachBaseContext: currentDensityDp: " + configuration.densityDpi
                    + " widthPixels: " + displayMetrics.widthPixels + " deviceDefault: " + DisplayMetrics.DENSITY_DEVICE_STABLE);

            if (displayMetrics.densityDpi != DisplayMetrics.DENSITY_DEVICE_STABLE) {
                // display_size_forced exists for Samsung Devices that allow user to change screen resolution
                // (screen resolution != screen zoom.. HD, FHD, WQDH etc)
                // This check can be omitted.. It seems this code works even if the device supports screen zoom only
                if(Settings.Global.getString(baseContext.getContentResolver(), "display_size_forced") != null) {
                    Log.v("TESTS", "attachBaseContext: This device supports screen resolution changes");

                    // density is densityDp / 160
                    float defaultDensity = (DisplayMetrics.DENSITY_DEVICE_STABLE / (float) DisplayMetrics.DENSITY_DEFAULT);
                    float defaultScreenWidthDp = displayMetrics.widthPixels / defaultDensity;
                    Log.v("TESTS", "attachBaseContext: defaultDensity: " + defaultDensity + " defaultScreenWidthDp: " + defaultScreenWidthDp);
                    configuration.densityDpi = findDensityDpCanFitScreen((int) defaultScreenWidthDp);
                } else {
                    // If the device does not allow the user to change the screen resolution, we can
                    // just set the default density
                    configuration.densityDpi = DisplayMetrics.DENSITY_DEVICE_STABLE;
                }
                Log.v("TESTS", "attachBaseContext: result: " + configuration.densityDpi);
                newContext = baseContext.createConfigurationContext(configuration);
            }
        }
        super.attachBaseContext(newContext);
    }

    @TargetApi(Build.VERSION_CODES.N)
    private static int findDensityDpCanFitScreen(final int densityDp) {
        int[] orderedDensityDp;

        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            orderedDensityDp = ORDERED_DENSITY_DP_P;
        } else if(Build.VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
            orderedDensityDp = ORDERED_DENSITY_DP_N_MR1;
        } else {
            orderedDensityDp = ORDERED_DENSITY_DP_N;
        }

        int index = 0;
        while (densityDp >= orderedDensityDp[index]) {
            index++;
        }
        return orderedDensityDp[index];
    }
}

ORIGINAL ANSWER

You can try following code (overriding attachBaseContext). This will "disable" the screen zoom in on your app. This is the way to re-scale the whole screen at once.

@Override
protected void attachBaseContext(final Context baseContext) {

    Context newContext;

    if(Build.VERSION.SDK_INT >= VERSION_CODES.N) {

        DisplayMetrics displayMetrics = baseContext.getResources().getDisplayMetrics();
        Configuration configuration = baseContext.getResources().getConfiguration();

        if (displayMetrics.densityDpi != DisplayMetrics.DENSITY_DEVICE_STABLE) {
            // Current density is different from Default Density. Override it
            configuration.densityDpi = DisplayMetrics.DENSITY_DEVICE_STABLE;
            newContext = baseContext.createConfigurationContext(configuration);
        } else {
            // Same density. Just use same context
            newContext = baseContext;
        }
    } else {
        // Old API. Screen zoom not supported
        newContext = baseContext;
    }
    super.attachBaseContext(newContext);
}

On that code, I check if the current density is different from the Device's default density. If they are different, I create a new context using default density (and not the current one). Then, I attach this modified context.

You must do that on every Activity. So, you can create a BaseActivity and add that code there. Then, you just need to update your activities in order to extend BaseActivity

public class BaseActivity extends AppCompatActivity {
    @Override
    protected void attachBaseContext(final Context baseContext) {
        ....
    }
}

Then, in your activities:

public class MainActivity extends BaseActivity {
    // Since I'm extending BaseActivity, I don't need to add the code
    // on attachBaseContext again
    // If you don't want to create a base activity, you must copy/paste that
    // attachBaseContext code into all activities
}

I tested this code with:

Log.v("Test", "Dimension: " + getResources().getDimension(R.dimen.test_dimension));

Different Screen Zoom (using that code):

2019-06-26 16:38:17.193 16312-16312/com.test.testapplication V/Test: Dimension: 105.0
2019-06-26 16:38:35.545 16312-16312/com.test.testapplication V/Test: Dimension: 105.0
2019-06-26 16:38:43.021 16579-16579/com.test.testapplication V/Test: Dimension: 105.0

Different Screen Zoom (without that code):

2019-06-26 16:42:53.807 17090-17090/com.test.testapplication V/Test: Dimension: 135.0
2019-06-26 16:43:19.381 17090-17090/com.test.testapplication V/Test: Dimension: 120.0
2019-06-26 16:44:00.125 17090-17090/com.test.testapplication V/Test: Dimension: 105.0

So, using that code, I can get the same dimension in pixels regardless of the zoom level.

Edit

guipivoto
  • 18,327
  • 9
  • 60
  • 75
  • Thank you so much! I don't really understand the following though: "You must do that one every activity. So, you can create a BaseActivity and add that code there. Then, you just need to update your activities in order to extend BaseActivity" What is the proper way to create a BaseActivity and have it be utilized by the MainApplication and/or MainActivity? Do I just create a new file called BaseActivity and copy the formatting of MainActivity and then just add that method inside of the class definition? – Emilio Jun 26 '19 at 20:06
  • "If you don't want to create a base activity, you must copy/paste that attachBaseContext code into all activities" - when you say all activities... I might be totally ignorant but afaik I only have a single activity right now - MainActivity - is that likely/possible? – Emilio Jun 26 '19 at 20:14
  • And for this part "public class MainActivity extends BaseActivity" - currently my MainActivity extends ReactActivity. Do you anticipate replacing that with BaseActivity causing issues? Would I potentially need to have BaseActivity extend ReactActivity instead of AppCompatActivity to account for that? – Emilio Jun 26 '19 at 20:25
  • 1
    I gave an example for a regular android app.. I think you can replace AppCompatActivity as you wish – guipivoto Jun 26 '19 at 20:34
  • Did you have to make any changes in your AndroidManifest.xml? – Emilio Jun 27 '19 at 14:55
  • Also, do I have to call this attachBaseContext method somewhere in the MainApplication.java file to have it be executed? – Emilio Jun 27 '19 at 15:21
  • "baseContext.createConfigurationContext(configuration);" must be enabled otherwise no change. Also "displayMetrics" is completely irrelevant given you can only check and set the "configuration" element, – George Feb 06 '20 at 17:50
  • 3
    This will work fine in most of the cases but in samsung devices in which we can change the resolution to HD+, FHD+ or WQHD+ it will distorts the UI. In FHD+ it works fine but in HD+ the screen zoom in and WQHD+ screen zoom out. Do you have any idea about it? – Gopal Awasthi Feb 08 '20 at 17:14
  • @George even after enabling baseContext.createConfigurationContext(configuration); no change. – Girish Apr 30 '20 at 08:54
  • For me, it worked after changing to updateConfiguration(configuration, displayMetrics); but it works above Android8.0. – Girish Apr 30 '20 at 09:28
  • Can confirm, this worked to fix some strange issues we were seeing when people changed "Display Size" (or "Screen Zoom" on bloatware'd versions of Android OS). I doubt it'll fix the crazy resolution changing stuff that Samsung provides, but it's a start! – DiscDev Aug 25 '20 at 18:24
  • @DiscDev I updated my answer to support screen resolution.. – guipivoto Aug 26 '20 at 00:18
  • @GopalAwasthi updated my answer to support screen resolution.. – guipivoto Aug 26 '20 at 00:18
  • @W0rmH0le thanks! I'll give it a try. Note that our app supports screen sizes perfectly. The issue here is we have a way to generate a jpg that's formatted perfectly for posting on Instagram. To achieve this we draw the view off screen then render the view to a bitmap. We want the content on this image to be the exact same size regardless of the device it's generated with. The display size, screen zoom, and device resolution impact the results. Our app works beautifully to adjust it's content to different settings, but in this case we need to disable things that mess with the bitmap. – DiscDev Aug 26 '20 at 12:43
  • @W0rmH0le As I understand, baseContext is a singelton. Why do we have to copy `attachBaseContext` to all activities? – Ali_Habeeb Dec 26 '20 at 23:12
  • For some reason, your code actually shrinks my UI in the 2 cases where I've zoomed in or out. Perhaps that satisfies the requirements, but for anyone else looking to totally ignore the user's zoom settings, this is not the solution unfortunately. – Richard Le Mesurier Oct 05 '21 at 14:22
  • Thank you very much for this. I was pulling my hair out trying to prevent the system display size change from affecting my entries/buttons etc. – Ryan Wilson Nov 09 '21 at 17:08