Loading packages/SystemUI/res/values/dimens.xml +8 −2 Original line number Diff line number Diff line Loading @@ -41,10 +41,16 @@ <!-- Size of the nav bar edge panels, should be greater to the edge sensitivity + the drag threshold --> <dimen name="navigation_edge_panel_width">52dp</dimen> <dimen name="navigation_edge_panel_height">52dp</dimen> <dimen name="navigation_edge_panel_width">76dp</dimen> <!-- Padding at the end of the navigation panel to allow the arrow not to be clipped off --> <dimen name="navigation_edge_panel_padding">24dp</dimen> <dimen name="navigation_edge_panel_height">84dp</dimen> <!-- The threshold to drag to trigger the edge action --> <dimen name="navigation_edge_action_drag_threshold">16dp</dimen> <!-- The minimum display position of the arrow on the screen --> <dimen name="navigation_edge_arrow_min_y">64dp</dimen> <!-- The amount by which the arrow is shifted to avoid the finger--> <dimen name="navigation_edge_finger_offset">48dp</dimen> <!-- Luminance threshold to determine black/white contrast for the navigation affordances --> <item name="navigation_luminance_threshold" type="dimen" format="float">0.5</item> Loading packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java +56 −11 Original line number Diff line number Diff line Loading @@ -48,6 +48,7 @@ import android.view.InputMonitor; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; import android.view.WindowManagerGlobal; Loading Loading @@ -126,6 +127,12 @@ public class EdgeBackGestureHandler implements DisplayListener { private final float mTouchSlop; // Minimum distance to move so that is can be considerd as a back swipe private final float mSwipeThreshold; // The threshold where the touch needs to be at most, such that the arrow is displayed above the // finger, otherwise it will be below private final int mMinArrowPosition; // The amount by which the arrow is shifted to avoid the finger private final int mFingerOffset; private final int mNavBarHeight; Loading @@ -147,6 +154,8 @@ public class EdgeBackGestureHandler implements DisplayListener { private NavigationBarEdgePanel mEdgePanel; private WindowManager.LayoutParams mEdgePanelLp; private final Rect mSamplingRect = new Rect(); private RegionSamplingHelper mRegionSamplingHelper; public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService) { final Resources res = context.getResources(); Loading @@ -163,6 +172,9 @@ public class EdgeBackGestureHandler implements DisplayListener { mSwipeThreshold = res.getDimension(R.dimen.navigation_edge_action_drag_threshold); mNavBarHeight = res.getDimensionPixelSize(R.dimen.navigation_bar_frame_height); mMinArrowPosition = res.getDimensionPixelSize( R.dimen.navigation_edge_arrow_min_y); mFingerOffset = res.getDimensionPixelSize(R.dimen.navigation_edge_finger_offset); } /** Loading Loading @@ -208,6 +220,8 @@ public class EdgeBackGestureHandler implements DisplayListener { if (mEdgePanel != null) { mWm.removeView(mEdgePanel); mEdgePanel = null; mRegionSamplingHelper.stop(); mRegionSamplingHelper = null; } if (!mIsEnabled) { Loading Loading @@ -261,6 +275,18 @@ public class EdgeBackGestureHandler implements DisplayListener { mEdgePanelLp.windowAnimations = 0; mEdgePanel.setLayoutParams(mEdgePanelLp); mWm.addView(mEdgePanel, mEdgePanelLp); mRegionSamplingHelper = new RegionSamplingHelper(mEdgePanel, new RegionSamplingHelper.SamplingCallback() { @Override public void onRegionDarknessChanged(boolean isRegionDark) { mEdgePanel.setIsDark(!isRegionDark, true /* animate */); } @Override public Rect getSampledRegion(View sampledView) { return mSamplingRect; } }); } } Loading Loading @@ -291,7 +317,7 @@ public class EdgeBackGestureHandler implements DisplayListener { // Verify if this is in within the touch region and we aren't in immersive mode, and // either the bouncer is showing or the notification panel is hidden int stateFlags = mOverviewProxyService.getSystemUiStateFlags(); mIsOnLeftEdge = ev.getX() < mEdgeWidth; mIsOnLeftEdge = ev.getX() <= mEdgeWidth; mAllowGesture = (stateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0 && ((stateFlags & SYSUI_STATE_BOUNCER_SHOWING) == SYSUI_STATE_BOUNCER_SHOWING || (stateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0) Loading @@ -301,14 +327,13 @@ public class EdgeBackGestureHandler implements DisplayListener { ? (Gravity.LEFT | Gravity.TOP) : (Gravity.RIGHT | Gravity.TOP); mEdgePanel.setIsLeftPanel(mIsOnLeftEdge); mEdgePanelLp.y = MathUtils.constrain( (int) (ev.getY() - mEdgePanelLp.height / 2), 0, mDisplaySize.y); mEdgePanel.handleTouch(ev); updateEdgePanelPosition(ev.getY()); mWm.updateViewLayout(mEdgePanel, mEdgePanelLp); mRegionSamplingHelper.start(mSamplingRect); mDownPoint.set(ev.getX(), ev.getY()); mThresholdCrossed = false; mEdgePanel.handleTouch(ev); } } else if (mAllowGesture) { if (!mThresholdCrossed && ev.getAction() == MotionEvent.ACTION_MOVE) { Loading @@ -333,12 +358,9 @@ public class EdgeBackGestureHandler implements DisplayListener { // forward touch mEdgePanel.handleTouch(ev); if (ev.getAction() == MotionEvent.ACTION_UP) { float xDiff = ev.getX() - mDownPoint.x; boolean exceedsThreshold = mIsOnLeftEdge ? (xDiff > mSwipeThreshold) : (-xDiff > mSwipeThreshold); boolean performAction = exceedsThreshold && Math.abs(xDiff) > Math.abs(ev.getY() - mDownPoint.y); boolean isUp = ev.getAction() == MotionEvent.ACTION_UP; if (isUp) { boolean performAction = mEdgePanel.shouldTriggerBack(); if (performAction) { // Perform back sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); Loading @@ -347,7 +369,30 @@ public class EdgeBackGestureHandler implements DisplayListener { mOverviewProxyService.notifyBackAction(performAction, (int) mDownPoint.x, (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge); } if (isUp || ev.getAction() == MotionEvent.ACTION_CANCEL) { mRegionSamplingHelper.stop(); } else { updateSamplingRect(); mRegionSamplingHelper.updateSamplingRect(); } } } private void updateEdgePanelPosition(float touchY) { float position = touchY - mFingerOffset; position = Math.max(position, mMinArrowPosition); position = (position - mEdgePanelLp.height / 2.0f); mEdgePanelLp.y = MathUtils.constrain((int) position, 0, mDisplaySize.y); updateSamplingRect(); } private void updateSamplingRect() { int top = mEdgePanelLp.y; int left = mIsOnLeftEdge ? 0 : mDisplaySize.x - mEdgePanelLp.width; int right = left + mEdgePanelLp.width; int bottom = top + mEdgePanelLp.height; mSamplingRect.set(left, top, right, bottom); mEdgePanel.adjustRectToBoundingBox(mSamplingRect); } @Override Loading packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java +602 −146 File changed.Preview size limit exceeded, changes collapsed. Show changes packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java 0 → 100644 +240 −0 Original line number Diff line number Diff line /* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.statusbar.phone; import static android.view.Display.DEFAULT_DISPLAY; import android.annotation.Nullable; import android.content.res.Resources; import android.graphics.Rect; import android.os.Handler; import android.os.IBinder; import android.provider.Settings; import android.view.CompositionSamplingListener; import android.view.SurfaceControl; import android.view.View; import android.view.ViewTreeObserver; import com.android.systemui.R; /** * A helper class to sample regions on the screen and inspect its luminosity. */ public class RegionSamplingHelper implements View.OnAttachStateChangeListener, View.OnLayoutChangeListener { private final Handler mHandler = new Handler(); private final View mSampledView; private final CompositionSamplingListener mSamplingListener; private final Runnable mUpdateSamplingListener = this::updateSamplingListener; /** * The requested sampling bounds that we want to sample from */ private final Rect mSamplingRequestBounds = new Rect(); /** * The sampling bounds that are currently registered. */ private final Rect mRegisteredSamplingBounds = new Rect(); private final SamplingCallback mCallback; private boolean mSamplingEnabled = false; private boolean mSamplingListenerRegistered = false; private float mLastMedianLuma; private float mCurrentMedianLuma; private boolean mWaitingOnDraw; // Passing the threshold of this luminance value will make the button black otherwise white private final float mLuminanceThreshold; private final float mLuminanceChangeThreshold; private boolean mFirstSamplingAfterStart; private SurfaceControl mRegisteredStopLayer = null; private ViewTreeObserver.OnDrawListener mUpdateOnDraw = new ViewTreeObserver.OnDrawListener() { @Override public void onDraw() { // We need to post the remove runnable, since it's not allowed to remove in onDraw mHandler.post(mRemoveDrawRunnable); RegionSamplingHelper.this.onDraw(); } }; private Runnable mRemoveDrawRunnable = new Runnable() { @Override public void run() { mSampledView.getViewTreeObserver().removeOnDrawListener(mUpdateOnDraw); } }; public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback) { mSamplingListener = new CompositionSamplingListener( sampledView.getContext().getMainExecutor()) { @Override public void onSampleCollected(float medianLuma) { if (mSamplingEnabled) { updateMediaLuma(medianLuma); } } }; mSampledView = sampledView; mSampledView.addOnAttachStateChangeListener(this); mSampledView.addOnLayoutChangeListener(this); final Resources res = sampledView.getResources(); mLuminanceThreshold = res.getFloat(R.dimen.navigation_luminance_threshold); mLuminanceChangeThreshold = res.getFloat(R.dimen.navigation_luminance_change_threshold); mCallback = samplingCallback; } private void onDraw() { if (mWaitingOnDraw) { mWaitingOnDraw = false; updateSamplingListener(); } } void start(Rect initialSamplingBounds) { if (!mCallback.isSamplingEnabled()) { return; } if (initialSamplingBounds != null) { mSamplingRequestBounds.set(initialSamplingBounds); } mSamplingEnabled = true; // make sure we notify once mLastMedianLuma = -1; mFirstSamplingAfterStart = true; updateSamplingListener(); } void stop() { mSamplingEnabled = false; updateSamplingListener(); } @Override public void onViewAttachedToWindow(View view) { updateSamplingListener(); } @Override public void onViewDetachedFromWindow(View view) { // isAttachedToWindow is only changed after this call to the listeners, so let's post it // instead postUpdateSamplingListener(); } @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { updateSamplingRect(); } private void postUpdateSamplingListener() { mHandler.removeCallbacks(mUpdateSamplingListener); mHandler.post(mUpdateSamplingListener); } private void updateSamplingListener() { boolean isSamplingEnabled = mSamplingEnabled && !mSamplingRequestBounds.isEmpty() && (mSampledView.isAttachedToWindow() || mFirstSamplingAfterStart); if (isSamplingEnabled) { SurfaceControl stopLayerControl = mSampledView.getViewRootImpl().getSurfaceControl(); if (!stopLayerControl.isValid()) { if (!mWaitingOnDraw) { mWaitingOnDraw = true; // The view might be attached but we haven't drawn yet, so wait until the // next draw to update the listener again with the stop layer, such that our // own drawing doesn't affect the sampling. if (mHandler.hasCallbacks(mRemoveDrawRunnable)) { mHandler.removeCallbacks(mRemoveDrawRunnable); } else { mSampledView.getViewTreeObserver().addOnDrawListener(mUpdateOnDraw); } } // If there's no valid surface, let's just sample without a stop layer, so we // don't have to delay stopLayerControl = null; } if (!mSamplingRequestBounds.equals(mRegisteredSamplingBounds) || mRegisteredStopLayer != stopLayerControl) { // We only want to reregister if something actually changed unregisterSamplingListener(); mSamplingListenerRegistered = true; CompositionSamplingListener.register(mSamplingListener, DEFAULT_DISPLAY, stopLayerControl != null ? stopLayerControl.getHandle() : null, mSamplingRequestBounds); mRegisteredSamplingBounds.set(mSamplingRequestBounds); mRegisteredStopLayer = stopLayerControl; } mFirstSamplingAfterStart = false; } else { unregisterSamplingListener(); } } private void unregisterSamplingListener() { if (mSamplingListenerRegistered) { mSamplingListenerRegistered = false; mRegisteredStopLayer = null; mRegisteredSamplingBounds.setEmpty(); CompositionSamplingListener.unregister(mSamplingListener); } } private void updateMediaLuma(float medianLuma) { mCurrentMedianLuma = medianLuma; // If the difference between the new luma and the current luma is larger than threshold // then apply the current luma, this is to prevent small changes causing colors to flicker if (Math.abs(mCurrentMedianLuma - mLastMedianLuma) > mLuminanceChangeThreshold) { mCallback.onRegionDarknessChanged(medianLuma < mLuminanceThreshold /* isRegionDark */); mLastMedianLuma = medianLuma; } } public void updateSamplingRect() { Rect sampledRegion = mCallback.getSampledRegion(mSampledView); if (!mSamplingRequestBounds.equals(sampledRegion)) { mSamplingRequestBounds.set(sampledRegion); updateSamplingListener(); } } public interface SamplingCallback { /** * Called when the darkness of the sampled region changes * @param isRegionDark true if the sampled luminance is below the luminance threshold */ void onRegionDarknessChanged(boolean isRegionDark); /** * Get the sampled region of interest from the sampled view * @param sampledView The view that this helper is attached to for convenience * @return the region to be sampled in sceen coordinates. Return {@code null} to avoid * sampling in this frame */ Rect getSampledRegion(View sampledView); /** * @return if sampling should be enabled in the current configuration */ default boolean isSamplingEnabled() { return true; } } } Loading
packages/SystemUI/res/values/dimens.xml +8 −2 Original line number Diff line number Diff line Loading @@ -41,10 +41,16 @@ <!-- Size of the nav bar edge panels, should be greater to the edge sensitivity + the drag threshold --> <dimen name="navigation_edge_panel_width">52dp</dimen> <dimen name="navigation_edge_panel_height">52dp</dimen> <dimen name="navigation_edge_panel_width">76dp</dimen> <!-- Padding at the end of the navigation panel to allow the arrow not to be clipped off --> <dimen name="navigation_edge_panel_padding">24dp</dimen> <dimen name="navigation_edge_panel_height">84dp</dimen> <!-- The threshold to drag to trigger the edge action --> <dimen name="navigation_edge_action_drag_threshold">16dp</dimen> <!-- The minimum display position of the arrow on the screen --> <dimen name="navigation_edge_arrow_min_y">64dp</dimen> <!-- The amount by which the arrow is shifted to avoid the finger--> <dimen name="navigation_edge_finger_offset">48dp</dimen> <!-- Luminance threshold to determine black/white contrast for the navigation affordances --> <item name="navigation_luminance_threshold" type="dimen" format="float">0.5</item> Loading
packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java +56 −11 Original line number Diff line number Diff line Loading @@ -48,6 +48,7 @@ import android.view.InputMonitor; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; import android.view.WindowManagerGlobal; Loading Loading @@ -126,6 +127,12 @@ public class EdgeBackGestureHandler implements DisplayListener { private final float mTouchSlop; // Minimum distance to move so that is can be considerd as a back swipe private final float mSwipeThreshold; // The threshold where the touch needs to be at most, such that the arrow is displayed above the // finger, otherwise it will be below private final int mMinArrowPosition; // The amount by which the arrow is shifted to avoid the finger private final int mFingerOffset; private final int mNavBarHeight; Loading @@ -147,6 +154,8 @@ public class EdgeBackGestureHandler implements DisplayListener { private NavigationBarEdgePanel mEdgePanel; private WindowManager.LayoutParams mEdgePanelLp; private final Rect mSamplingRect = new Rect(); private RegionSamplingHelper mRegionSamplingHelper; public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService) { final Resources res = context.getResources(); Loading @@ -163,6 +172,9 @@ public class EdgeBackGestureHandler implements DisplayListener { mSwipeThreshold = res.getDimension(R.dimen.navigation_edge_action_drag_threshold); mNavBarHeight = res.getDimensionPixelSize(R.dimen.navigation_bar_frame_height); mMinArrowPosition = res.getDimensionPixelSize( R.dimen.navigation_edge_arrow_min_y); mFingerOffset = res.getDimensionPixelSize(R.dimen.navigation_edge_finger_offset); } /** Loading Loading @@ -208,6 +220,8 @@ public class EdgeBackGestureHandler implements DisplayListener { if (mEdgePanel != null) { mWm.removeView(mEdgePanel); mEdgePanel = null; mRegionSamplingHelper.stop(); mRegionSamplingHelper = null; } if (!mIsEnabled) { Loading Loading @@ -261,6 +275,18 @@ public class EdgeBackGestureHandler implements DisplayListener { mEdgePanelLp.windowAnimations = 0; mEdgePanel.setLayoutParams(mEdgePanelLp); mWm.addView(mEdgePanel, mEdgePanelLp); mRegionSamplingHelper = new RegionSamplingHelper(mEdgePanel, new RegionSamplingHelper.SamplingCallback() { @Override public void onRegionDarknessChanged(boolean isRegionDark) { mEdgePanel.setIsDark(!isRegionDark, true /* animate */); } @Override public Rect getSampledRegion(View sampledView) { return mSamplingRect; } }); } } Loading Loading @@ -291,7 +317,7 @@ public class EdgeBackGestureHandler implements DisplayListener { // Verify if this is in within the touch region and we aren't in immersive mode, and // either the bouncer is showing or the notification panel is hidden int stateFlags = mOverviewProxyService.getSystemUiStateFlags(); mIsOnLeftEdge = ev.getX() < mEdgeWidth; mIsOnLeftEdge = ev.getX() <= mEdgeWidth; mAllowGesture = (stateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0 && ((stateFlags & SYSUI_STATE_BOUNCER_SHOWING) == SYSUI_STATE_BOUNCER_SHOWING || (stateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0) Loading @@ -301,14 +327,13 @@ public class EdgeBackGestureHandler implements DisplayListener { ? (Gravity.LEFT | Gravity.TOP) : (Gravity.RIGHT | Gravity.TOP); mEdgePanel.setIsLeftPanel(mIsOnLeftEdge); mEdgePanelLp.y = MathUtils.constrain( (int) (ev.getY() - mEdgePanelLp.height / 2), 0, mDisplaySize.y); mEdgePanel.handleTouch(ev); updateEdgePanelPosition(ev.getY()); mWm.updateViewLayout(mEdgePanel, mEdgePanelLp); mRegionSamplingHelper.start(mSamplingRect); mDownPoint.set(ev.getX(), ev.getY()); mThresholdCrossed = false; mEdgePanel.handleTouch(ev); } } else if (mAllowGesture) { if (!mThresholdCrossed && ev.getAction() == MotionEvent.ACTION_MOVE) { Loading @@ -333,12 +358,9 @@ public class EdgeBackGestureHandler implements DisplayListener { // forward touch mEdgePanel.handleTouch(ev); if (ev.getAction() == MotionEvent.ACTION_UP) { float xDiff = ev.getX() - mDownPoint.x; boolean exceedsThreshold = mIsOnLeftEdge ? (xDiff > mSwipeThreshold) : (-xDiff > mSwipeThreshold); boolean performAction = exceedsThreshold && Math.abs(xDiff) > Math.abs(ev.getY() - mDownPoint.y); boolean isUp = ev.getAction() == MotionEvent.ACTION_UP; if (isUp) { boolean performAction = mEdgePanel.shouldTriggerBack(); if (performAction) { // Perform back sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); Loading @@ -347,7 +369,30 @@ public class EdgeBackGestureHandler implements DisplayListener { mOverviewProxyService.notifyBackAction(performAction, (int) mDownPoint.x, (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge); } if (isUp || ev.getAction() == MotionEvent.ACTION_CANCEL) { mRegionSamplingHelper.stop(); } else { updateSamplingRect(); mRegionSamplingHelper.updateSamplingRect(); } } } private void updateEdgePanelPosition(float touchY) { float position = touchY - mFingerOffset; position = Math.max(position, mMinArrowPosition); position = (position - mEdgePanelLp.height / 2.0f); mEdgePanelLp.y = MathUtils.constrain((int) position, 0, mDisplaySize.y); updateSamplingRect(); } private void updateSamplingRect() { int top = mEdgePanelLp.y; int left = mIsOnLeftEdge ? 0 : mDisplaySize.x - mEdgePanelLp.width; int right = left + mEdgePanelLp.width; int bottom = top + mEdgePanelLp.height; mSamplingRect.set(left, top, right, bottom); mEdgePanel.adjustRectToBoundingBox(mSamplingRect); } @Override Loading
packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java +602 −146 File changed.Preview size limit exceeded, changes collapsed. Show changes
packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java 0 → 100644 +240 −0 Original line number Diff line number Diff line /* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.statusbar.phone; import static android.view.Display.DEFAULT_DISPLAY; import android.annotation.Nullable; import android.content.res.Resources; import android.graphics.Rect; import android.os.Handler; import android.os.IBinder; import android.provider.Settings; import android.view.CompositionSamplingListener; import android.view.SurfaceControl; import android.view.View; import android.view.ViewTreeObserver; import com.android.systemui.R; /** * A helper class to sample regions on the screen and inspect its luminosity. */ public class RegionSamplingHelper implements View.OnAttachStateChangeListener, View.OnLayoutChangeListener { private final Handler mHandler = new Handler(); private final View mSampledView; private final CompositionSamplingListener mSamplingListener; private final Runnable mUpdateSamplingListener = this::updateSamplingListener; /** * The requested sampling bounds that we want to sample from */ private final Rect mSamplingRequestBounds = new Rect(); /** * The sampling bounds that are currently registered. */ private final Rect mRegisteredSamplingBounds = new Rect(); private final SamplingCallback mCallback; private boolean mSamplingEnabled = false; private boolean mSamplingListenerRegistered = false; private float mLastMedianLuma; private float mCurrentMedianLuma; private boolean mWaitingOnDraw; // Passing the threshold of this luminance value will make the button black otherwise white private final float mLuminanceThreshold; private final float mLuminanceChangeThreshold; private boolean mFirstSamplingAfterStart; private SurfaceControl mRegisteredStopLayer = null; private ViewTreeObserver.OnDrawListener mUpdateOnDraw = new ViewTreeObserver.OnDrawListener() { @Override public void onDraw() { // We need to post the remove runnable, since it's not allowed to remove in onDraw mHandler.post(mRemoveDrawRunnable); RegionSamplingHelper.this.onDraw(); } }; private Runnable mRemoveDrawRunnable = new Runnable() { @Override public void run() { mSampledView.getViewTreeObserver().removeOnDrawListener(mUpdateOnDraw); } }; public RegionSamplingHelper(View sampledView, SamplingCallback samplingCallback) { mSamplingListener = new CompositionSamplingListener( sampledView.getContext().getMainExecutor()) { @Override public void onSampleCollected(float medianLuma) { if (mSamplingEnabled) { updateMediaLuma(medianLuma); } } }; mSampledView = sampledView; mSampledView.addOnAttachStateChangeListener(this); mSampledView.addOnLayoutChangeListener(this); final Resources res = sampledView.getResources(); mLuminanceThreshold = res.getFloat(R.dimen.navigation_luminance_threshold); mLuminanceChangeThreshold = res.getFloat(R.dimen.navigation_luminance_change_threshold); mCallback = samplingCallback; } private void onDraw() { if (mWaitingOnDraw) { mWaitingOnDraw = false; updateSamplingListener(); } } void start(Rect initialSamplingBounds) { if (!mCallback.isSamplingEnabled()) { return; } if (initialSamplingBounds != null) { mSamplingRequestBounds.set(initialSamplingBounds); } mSamplingEnabled = true; // make sure we notify once mLastMedianLuma = -1; mFirstSamplingAfterStart = true; updateSamplingListener(); } void stop() { mSamplingEnabled = false; updateSamplingListener(); } @Override public void onViewAttachedToWindow(View view) { updateSamplingListener(); } @Override public void onViewDetachedFromWindow(View view) { // isAttachedToWindow is only changed after this call to the listeners, so let's post it // instead postUpdateSamplingListener(); } @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { updateSamplingRect(); } private void postUpdateSamplingListener() { mHandler.removeCallbacks(mUpdateSamplingListener); mHandler.post(mUpdateSamplingListener); } private void updateSamplingListener() { boolean isSamplingEnabled = mSamplingEnabled && !mSamplingRequestBounds.isEmpty() && (mSampledView.isAttachedToWindow() || mFirstSamplingAfterStart); if (isSamplingEnabled) { SurfaceControl stopLayerControl = mSampledView.getViewRootImpl().getSurfaceControl(); if (!stopLayerControl.isValid()) { if (!mWaitingOnDraw) { mWaitingOnDraw = true; // The view might be attached but we haven't drawn yet, so wait until the // next draw to update the listener again with the stop layer, such that our // own drawing doesn't affect the sampling. if (mHandler.hasCallbacks(mRemoveDrawRunnable)) { mHandler.removeCallbacks(mRemoveDrawRunnable); } else { mSampledView.getViewTreeObserver().addOnDrawListener(mUpdateOnDraw); } } // If there's no valid surface, let's just sample without a stop layer, so we // don't have to delay stopLayerControl = null; } if (!mSamplingRequestBounds.equals(mRegisteredSamplingBounds) || mRegisteredStopLayer != stopLayerControl) { // We only want to reregister if something actually changed unregisterSamplingListener(); mSamplingListenerRegistered = true; CompositionSamplingListener.register(mSamplingListener, DEFAULT_DISPLAY, stopLayerControl != null ? stopLayerControl.getHandle() : null, mSamplingRequestBounds); mRegisteredSamplingBounds.set(mSamplingRequestBounds); mRegisteredStopLayer = stopLayerControl; } mFirstSamplingAfterStart = false; } else { unregisterSamplingListener(); } } private void unregisterSamplingListener() { if (mSamplingListenerRegistered) { mSamplingListenerRegistered = false; mRegisteredStopLayer = null; mRegisteredSamplingBounds.setEmpty(); CompositionSamplingListener.unregister(mSamplingListener); } } private void updateMediaLuma(float medianLuma) { mCurrentMedianLuma = medianLuma; // If the difference between the new luma and the current luma is larger than threshold // then apply the current luma, this is to prevent small changes causing colors to flicker if (Math.abs(mCurrentMedianLuma - mLastMedianLuma) > mLuminanceChangeThreshold) { mCallback.onRegionDarknessChanged(medianLuma < mLuminanceThreshold /* isRegionDark */); mLastMedianLuma = medianLuma; } } public void updateSamplingRect() { Rect sampledRegion = mCallback.getSampledRegion(mSampledView); if (!mSamplingRequestBounds.equals(sampledRegion)) { mSamplingRequestBounds.set(sampledRegion); updateSamplingListener(); } } public interface SamplingCallback { /** * Called when the darkness of the sampled region changes * @param isRegionDark true if the sampled luminance is below the luminance threshold */ void onRegionDarknessChanged(boolean isRegionDark); /** * Get the sampled region of interest from the sampled view * @param sampledView The view that this helper is attached to for convenience * @return the region to be sampled in sceen coordinates. Return {@code null} to avoid * sampling in this frame */ Rect getSampledRegion(View sampledView); /** * @return if sampling should be enabled in the current configuration */ default boolean isSamplingEnabled() { return true; } } }