OSDN Git Service

Convert the BrightnessController to a log scale control.
authorMichael Wright <michaelwr@google.com>
Fri, 13 Apr 2018 17:15:27 +0000 (18:15 +0100)
committerMichael Wright <michaelwr@google.com>
Mon, 16 Apr 2018 13:47:33 +0000 (14:47 +0100)
Currently, the BrightnessController's UI is a linear scale control on
top of a linear backlight control, but humans perceive brightness on a
roughly logarithmic scale. By moving to a non-linear control, we both
give users more fine-grained control over the brightness of the display
as well as a UI that works more intuitively.

Test: manual
Bug: 73810208
Change-Id: I67090ad7c4ced0420314458473c9124cb9c61906

core/java/android/util/MathUtils.java
packages/SystemUI/src/com/android/systemui/settings/BrightnessController.java

index e865b6e..b2e24c3 100644 (file)
@@ -56,6 +56,10 @@ public final class MathUtils {
         return (float) Math.pow(a, b);
     }
 
+    public static float sqrt(float a) {
+        return (float) Math.sqrt(a);
+    }
+
     public static float max(float a, float b) {
         return a > b ? a : b;
     }
index f2a7adf..1736f38 100644 (file)
@@ -36,6 +36,7 @@ import android.provider.Settings;
 import android.service.vr.IVrManager;
 import android.service.vr.IVrStateCallbacks;
 import android.util.Log;
+import android.util.MathUtils;
 import android.widget.ImageView;
 
 import com.android.internal.logging.MetricsLogger;
@@ -49,8 +50,15 @@ public class BrightnessController implements ToggleSlider.Listener {
     private static final String TAG = "StatusBar.BrightnessController";
     private static final boolean SHOW_AUTOMATIC_ICON = false;
 
+    private static final int SLIDER_MAX = 1023;
     private static final int SLIDER_ANIMATION_DURATION = 3000;
 
+    // Hybrid Log Gamma constant values
+    private static final float R = 0.5f;
+    private static final float A = 0.17883277f;
+    private static final float B = 0.28466892f;
+    private static final float C = 0.55991073f;
+
     private static final int MSG_UPDATE_ICON = 0;
     private static final int MSG_UPDATE_SLIDER = 1;
     private static final int MSG_SET_CHECKED = 2;
@@ -60,8 +68,10 @@ public class BrightnessController implements ToggleSlider.Listener {
 
     private final int mMinimumBacklight;
     private final int mMaximumBacklight;
+    private final int mDefaultBacklight;
     private final int mMinimumBacklightForVr;
     private final int mMaximumBacklightForVr;
+    private final int mDefaultBacklightForVr;
 
     private final Context mContext;
     private final ImageView mIcon;
@@ -203,21 +213,18 @@ public class BrightnessController implements ToggleSlider.Listener {
     private final Runnable mUpdateSliderRunnable = new Runnable() {
         @Override
         public void run() {
-            if (mIsVrModeEnabled) {
-                int value = Settings.System.getIntForUser(mContext.getContentResolver(),
-                        Settings.System.SCREEN_BRIGHTNESS_FOR_VR, mMaximumBacklight,
+            final int val;
+            final boolean inVrMode = mIsVrModeEnabled;
+            if (inVrMode) {
+                val = Settings.System.getIntForUser(mContext.getContentResolver(),
+                        Settings.System.SCREEN_BRIGHTNESS_FOR_VR, mDefaultBacklightForVr,
                         UserHandle.USER_CURRENT);
-                mHandler.obtainMessage(MSG_UPDATE_SLIDER,
-                        mMaximumBacklightForVr - mMinimumBacklightForVr,
-                        value - mMinimumBacklightForVr).sendToTarget();
             } else {
-                int value;
-                value = Settings.System.getIntForUser(mContext.getContentResolver(),
-                        Settings.System.SCREEN_BRIGHTNESS, mMaximumBacklight,
+                val = Settings.System.getIntForUser(mContext.getContentResolver(),
+                        Settings.System.SCREEN_BRIGHTNESS, mDefaultBacklight,
                         UserHandle.USER_CURRENT);
-                mHandler.obtainMessage(MSG_UPDATE_SLIDER, mMaximumBacklight - mMinimumBacklight,
-                        value - mMinimumBacklight).sendToTarget();
             }
+            mHandler.obtainMessage(MSG_UPDATE_SLIDER, val, inVrMode ? 1 : 0).sendToTarget();
         }
     };
 
@@ -239,8 +246,7 @@ public class BrightnessController implements ToggleSlider.Listener {
                         updateIcon(msg.arg1 != 0);
                         break;
                     case MSG_UPDATE_SLIDER:
-                        mControl.setMax(msg.arg1);
-                        animateSliderTo(msg.arg2);
+                        updateSlider(msg.arg1, msg.arg2 != 0);
                         break;
                     case MSG_SET_CHECKED:
                         mControl.setChecked(msg.arg1 != 0);
@@ -267,6 +273,7 @@ public class BrightnessController implements ToggleSlider.Listener {
         mContext = context;
         mIcon = icon;
         mControl = control;
+        mControl.setMax(SLIDER_MAX);
         mBackgroundHandler = new Handler((Looper) Dependency.get(Dependency.BG_LOOPER));
         mUserTracker = new CurrentUserTracker(mContext) {
             @Override
@@ -277,11 +284,13 @@ public class BrightnessController implements ToggleSlider.Listener {
         };
         mBrightnessObserver = new BrightnessObserver(mHandler);
 
-        PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+        PowerManager pm = context.getSystemService(PowerManager.class);
         mMinimumBacklight = pm.getMinimumScreenBrightnessSetting();
         mMaximumBacklight = pm.getMaximumScreenBrightnessSetting();
+        mDefaultBacklight = pm.getDefaultScreenBrightnessSetting();
         mMinimumBacklightForVr = pm.getMinimumScreenBrightnessForVrSetting();
         mMaximumBacklightForVr = pm.getMaximumScreenBrightnessForVrSetting();
+        mDefaultBacklightForVr = pm.getDefaultScreenBrightnessForVrSetting();
 
         mAutomaticAvailable = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_automatic_brightness_available);
@@ -350,38 +359,39 @@ public class BrightnessController implements ToggleSlider.Listener {
             mSliderAnimator.cancel();
         }
 
+        final int min;
+        final int max;
+        final int metric;
+        final String setting;
+
         if (mIsVrModeEnabled) {
-            final int val = value + mMinimumBacklightForVr;
-            if (stopTracking) {
-                MetricsLogger.action(mContext, MetricsEvent.ACTION_BRIGHTNESS_FOR_VR, val);
-            }
-            setBrightness(val);
-            if (!tracking) {
-                AsyncTask.execute(new Runnable() {
-                        public void run() {
-                            Settings.System.putIntForUser(mContext.getContentResolver(),
-                                    Settings.System.SCREEN_BRIGHTNESS_FOR_VR, val,
-                                    UserHandle.USER_CURRENT);
-                        }
-                    });
-            }
+            metric = MetricsEvent.ACTION_BRIGHTNESS_FOR_VR;
+            min = mMinimumBacklightForVr;
+            max = mMaximumBacklightForVr;
+            setting = Settings.System.SCREEN_BRIGHTNESS_FOR_VR;
         } else {
-            final int val = value + mMinimumBacklight;
-            if (stopTracking) {
-                final int metric = mAutomatic ?
-                        MetricsEvent.ACTION_BRIGHTNESS_AUTO : MetricsEvent.ACTION_BRIGHTNESS;
-                MetricsLogger.action(mContext, metric, val);
-            }
-            setBrightness(val);
-            if (!tracking) {
-                AsyncTask.execute(new Runnable() {
-                        public void run() {
-                            Settings.System.putIntForUser(mContext.getContentResolver(),
-                                    Settings.System.SCREEN_BRIGHTNESS, val,
-                                    UserHandle.USER_CURRENT);
-                        }
-                    });
-            }
+            metric = mAutomatic
+                    ? MetricsEvent.ACTION_BRIGHTNESS_AUTO
+                    : MetricsEvent.ACTION_BRIGHTNESS;
+            min = mMinimumBacklight;
+            max = mMaximumBacklight;
+            setting = Settings.System.SCREEN_BRIGHTNESS;
+        }
+
+        final int val = convertGammaToLinear(value, min, max);
+
+        if (stopTracking) {
+            MetricsLogger.action(mContext, metric, val);
+        }
+
+        setBrightness(val);
+        if (!tracking) {
+            AsyncTask.execute(new Runnable() {
+                    public void run() {
+                        Settings.System.putIntForUser(mContext.getContentResolver(),
+                                setting, val, UserHandle.USER_CURRENT);
+                    }
+                });
         }
 
         for (BrightnessStateChangeCallback cb : mChangeCallbacks) {
@@ -430,6 +440,28 @@ public class BrightnessController implements ToggleSlider.Listener {
         }
     }
 
+    private void updateSlider(int val, boolean inVrMode) {
+        final int min;
+        final int max;
+        if (inVrMode) {
+            min = mMinimumBacklightForVr;
+            max = mMaximumBacklightForVr;
+        } else {
+            min = mMinimumBacklight;
+            max = mMaximumBacklight;
+        }
+        if (val == convertGammaToLinear(mControl.getValue(), min, max)) {
+            // If we have more resolution on the slider than we do in the actual setting, then
+            // multiple slider positions will map to the same setting value. Thus, if we see a
+            // setting value here that maps to the current slider position, we don't bother to
+            // calculate the new slider position since it may differ and look like a brightness
+            // change to the user even though it isn't one.
+            return;
+        }
+        final int sliderVal = convertLinearToGamma(val, min, max);
+        animateSliderTo(sliderVal);
+    }
+
     private void animateSliderTo(int target) {
         if (!mControlValueInitialized) {
             // Don't animate the first value since it's default state isn't meaningful to users.
@@ -448,4 +480,77 @@ public class BrightnessController implements ToggleSlider.Listener {
         mSliderAnimator.setDuration(SLIDER_ANIMATION_DURATION);
         mSliderAnimator.start();
     }
+
+    /**
+     * A function for converting from the linear space that the setting works in to the
+     * gamma space that the slider works in.
+     *
+     * The gamma space effectively provides us a way to make linear changes to the slider that
+     * result in linear changes in perception. If we made changes to the slider in the linear space
+     * then we'd see an approximately logarithmic change in perception (c.f. Fechner's Law).
+     *
+     * Internally, this implements the Hybrid Log Gamma opto-electronic transfer function, which is
+     * a slight improvement to the typical gamma transfer function for displays whose max
+     * brightness exceeds the 120 nit reference point, but doesn't set a specific reference
+     * brightness like the PQ function does.
+     *
+     * Note that this transfer function is only valid if the display's backlight value is a linear
+     * control. If it's calibrated to be something non-linear, then a different transfer function
+     * should be used.
+     *
+     * @param val The brightness setting value.
+     * @param min The minimum acceptable value for the setting.
+     * @param max The maximum acceptable value for the setting.
+     *
+     * @return The corresponding slider value
+     */
+    private static final int convertLinearToGamma(int val, int min, int max) {
+        // For some reason, HLG normalizes to the range [0, 12] rather than [0, 1]
+        final float normalizedVal = MathUtils.norm(min, max, val) * 12;
+        final float ret;
+        if (normalizedVal <= 1f) {
+            ret = MathUtils.sqrt(normalizedVal) * R;
+        } else {
+            ret = A * MathUtils.log(normalizedVal - B) + C;
+        }
+
+        return Math.round(MathUtils.lerp(0, SLIDER_MAX, ret));
+    }
+
+    /**
+     * A function for converting from the gamma space that the slider works in to the
+     * linear space that the setting works in.
+     *
+     * The gamma space effectively provides us a way to make linear changes to the slider that
+     * result in linear changes in perception. If we made changes to the slider in the linear space
+     * then we'd see an approximately logarithmic change in perception (c.f. Fechner's Law).
+     *
+     * Internally, this implements the Hybrid Log Gamma electro-optical transfer function, which is
+     * a slight improvement to the typical gamma transfer function for displays whose max
+     * brightness exceeds the 120 nit reference point, but doesn't set a specific reference
+     * brightness like the PQ function does.
+     *
+     * Note that this transfer function is only valid if the display's backlight value is a linear
+     * control. If it's calibrated to be something non-linear, then a different transfer function
+     * should be used.
+     *
+     * @param val The slider value.
+     * @param min The minimum acceptable value for the setting.
+     * @param max The maximum acceptable value for the setting.
+     *
+     * @return The corresponding setting value.
+     */
+    private static final int convertGammaToLinear(int val, int min, int max) {
+        final float normalizedVal = MathUtils.norm(0, SLIDER_MAX, val);
+        final float ret;
+        if (normalizedVal <= R) {
+            ret = MathUtils.sq(normalizedVal/R);
+        } else {
+            ret = MathUtils.exp((normalizedVal - C) / A) + B;
+        }
+
+        // HLG is normalized to the range [0, 12], so we need to re-normalize to the range [0, 1]
+        // in order to derive the correct setting value.
+        return Math.round(MathUtils.lerp(min, max, ret / 12));
+    }
 }