Merge "Adding hover state to system icons in status bar and shade header" into udc-qpr-dev am: e74076cf42
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/24041603 Change-Id: I46ae8d658f118cc32f07ac40b5c37b204c65b3f4 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -26,6 +26,7 @@ import com.android.systemui.plugins.annotations.DependsOn;
|
||||
import com.android.systemui.plugins.annotations.ProvidesInterface;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Dispatches events to {@link DarkReceiver}s about changes in darkness, tint area and dark
|
||||
@@ -78,7 +79,7 @@ public interface DarkIconDispatcher {
|
||||
* @return the tint to apply to view depending on the desired tint color and
|
||||
* the screen tintArea in which to apply that tint
|
||||
*/
|
||||
static int getTint(ArrayList<Rect> tintAreas, View view, int color) {
|
||||
static int getTint(Collection<Rect> tintAreas, View view, int color) {
|
||||
if (isInAreas(tintAreas, view)) {
|
||||
return color;
|
||||
} else {
|
||||
@@ -90,7 +91,7 @@ public interface DarkIconDispatcher {
|
||||
* @return true if more than half of the view area are in any of the given
|
||||
* areas, false otherwise
|
||||
*/
|
||||
static boolean isInAreas(ArrayList<Rect> areas, View view) {
|
||||
static boolean isInAreas(Collection<Rect> areas, View view) {
|
||||
if (areas.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
<color name="qs_detail_button_white">#B3FFFFFF</color><!-- 70% white -->
|
||||
<color name="status_bar_clock_color">#FFFFFFFF</color>
|
||||
<color name="qs_tile_disabled_color">#9E9E9E</color> <!-- 38% black -->
|
||||
<color name="status_bar_icons_hover_color_light">#38FFFFFF</color> <!-- 22% white -->
|
||||
<color name="status_bar_icons_hover_color_dark">#38000000</color> <!-- 22% black -->
|
||||
|
||||
<!-- The color of the background in the separated list of the Global Actions menu -->
|
||||
<color name="global_actions_separated_background">#F5F5F5</color>
|
||||
|
||||
@@ -475,6 +475,8 @@
|
||||
<!-- Margin start of the system icons super container -->
|
||||
<dimen name="system_icons_super_container_margin_start">16dp</dimen>
|
||||
|
||||
<dimen name="status_icons_hover_state_background_radius">16dp</dimen>
|
||||
|
||||
<!-- Width for the notification panel and related windows -->
|
||||
<dimen name="match_parent">-1px</dimen>
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
||||
import com.android.systemui.statusbar.StatusBarStateControllerImpl;
|
||||
import com.android.systemui.statusbar.phone.ActivityStarterImpl;
|
||||
import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl;
|
||||
import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher;
|
||||
import com.android.systemui.volume.VolumeDialogControllerImpl;
|
||||
|
||||
import dagger.Binds;
|
||||
@@ -49,6 +50,10 @@ public abstract class PluginModule {
|
||||
@Binds
|
||||
abstract DarkIconDispatcher provideDarkIconDispatcher(DarkIconDispatcherImpl controllerImpl);
|
||||
|
||||
@Binds
|
||||
abstract SysuiDarkIconDispatcher provideSysuiDarkIconDispatcher(
|
||||
DarkIconDispatcherImpl controllerImpl);
|
||||
|
||||
/** */
|
||||
@Binds
|
||||
abstract FalsingManager provideFalsingManager(FalsingManagerProxy falsingManagerImpl);
|
||||
|
||||
@@ -61,6 +61,7 @@ import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
|
||||
import com.android.systemui.statusbar.phone.StatusBarIconController
|
||||
import com.android.systemui.statusbar.phone.StatusBarLocation
|
||||
import com.android.systemui.statusbar.phone.StatusIconContainer
|
||||
import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory
|
||||
import com.android.systemui.statusbar.policy.Clock
|
||||
import com.android.systemui.statusbar.policy.ConfigurationController
|
||||
import com.android.systemui.statusbar.policy.NextAlarmController
|
||||
@@ -99,6 +100,7 @@ constructor(
|
||||
private val qsBatteryModeController: QsBatteryModeController,
|
||||
private val nextAlarmController: NextAlarmController,
|
||||
private val activityStarter: ActivityStarter,
|
||||
private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
|
||||
) : ViewController<View>(header), Dumpable {
|
||||
|
||||
companion object {
|
||||
@@ -326,6 +328,9 @@ constructor(
|
||||
demoModeController.addCallback(demoModeReceiver)
|
||||
statusBarIconController.addIconGroup(iconManager)
|
||||
nextAlarmController.addCallback(nextAlarmCallback)
|
||||
systemIcons.setOnHoverListener(
|
||||
statusOverlayHoverListenerFactory.createListener(systemIcons)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewDetached() {
|
||||
@@ -336,6 +341,7 @@ constructor(
|
||||
demoModeController.removeCallback(demoModeReceiver)
|
||||
statusBarIconController.removeIconGroup(iconManager)
|
||||
nextAlarmController.removeCallback(nextAlarmCallback)
|
||||
systemIcons.setOnHoverListener(null)
|
||||
}
|
||||
|
||||
fun disable(state1: Int, state2: Int, animate: Boolean) {
|
||||
|
||||
@@ -32,6 +32,11 @@ import java.util.ArrayList;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import kotlinx.coroutines.flow.FlowKt;
|
||||
import kotlinx.coroutines.flow.MutableStateFlow;
|
||||
import kotlinx.coroutines.flow.StateFlow;
|
||||
import kotlinx.coroutines.flow.StateFlowKt;
|
||||
|
||||
/**
|
||||
*/
|
||||
@SysUISingleton
|
||||
@@ -47,6 +52,9 @@ public class DarkIconDispatcherImpl implements SysuiDarkIconDispatcher,
|
||||
private int mDarkModeIconColorSingleTone;
|
||||
private int mLightModeIconColorSingleTone;
|
||||
|
||||
private final MutableStateFlow<DarkChange> mDarkChangeFlow = StateFlowKt.MutableStateFlow(
|
||||
DarkChange.EMPTY);
|
||||
|
||||
/**
|
||||
*/
|
||||
@Inject
|
||||
@@ -66,6 +74,11 @@ public class DarkIconDispatcherImpl implements SysuiDarkIconDispatcher,
|
||||
return mTransitionsController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StateFlow<DarkChange> darkChangeFlow() {
|
||||
return FlowKt.asStateFlow(mDarkChangeFlow);
|
||||
}
|
||||
|
||||
public void addDarkReceiver(DarkReceiver receiver) {
|
||||
mReceivers.put(receiver, receiver);
|
||||
receiver.onDarkChanged(mTintAreas, mDarkIntensity, mIconTint);
|
||||
@@ -122,6 +135,7 @@ public class DarkIconDispatcherImpl implements SysuiDarkIconDispatcher,
|
||||
}
|
||||
|
||||
private void applyIconTint() {
|
||||
mDarkChangeFlow.setValue(new DarkChange(mTintAreas, mDarkIntensity, mIconTint));
|
||||
for (int i = 0; i < mReceivers.size(); i++) {
|
||||
mReceivers.valueAt(i).onDarkChanged(mTintAreas, mDarkIntensity, mIconTint);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import android.view.DisplayCutout;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
@@ -43,11 +42,11 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.app.animation.Interpolators;
|
||||
import com.android.settingslib.Utils;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.battery.BatteryMeterView;
|
||||
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
|
||||
import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange;
|
||||
import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
|
||||
import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder;
|
||||
import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
|
||||
@@ -55,6 +54,11 @@ import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import kotlinx.coroutines.flow.FlowKt;
|
||||
import kotlinx.coroutines.flow.MutableStateFlow;
|
||||
import kotlinx.coroutines.flow.StateFlow;
|
||||
import kotlinx.coroutines.flow.StateFlowKt;
|
||||
|
||||
/**
|
||||
* The header group on Keyguard.
|
||||
*/
|
||||
@@ -83,6 +87,8 @@ public class KeyguardStatusBarView extends RelativeLayout {
|
||||
private int mStatusBarPaddingEnd;
|
||||
private int mMinDotWidth;
|
||||
private View mSystemIconsContainer;
|
||||
private final MutableStateFlow<DarkChange> mDarkChange = StateFlowKt.MutableStateFlow(
|
||||
DarkChange.EMPTY);
|
||||
|
||||
private View mCutoutSpace;
|
||||
private ViewGroup mStatusIconArea;
|
||||
@@ -374,49 +380,6 @@ public class KeyguardStatusBarView extends RelativeLayout {
|
||||
return mKeyguardUserAvatarEnabled;
|
||||
}
|
||||
|
||||
private void animateNextLayoutChange() {
|
||||
final int systemIconsCurrentX = mSystemIconsContainer.getLeft();
|
||||
final boolean userAvatarVisible = mMultiUserAvatar.getParent() == mStatusIconArea;
|
||||
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
boolean userAvatarHiding = userAvatarVisible
|
||||
&& mMultiUserAvatar.getParent() != mStatusIconArea;
|
||||
mSystemIconsContainer.setX(systemIconsCurrentX);
|
||||
mSystemIconsContainer.animate()
|
||||
.translationX(0)
|
||||
.setDuration(400)
|
||||
.setStartDelay(userAvatarHiding ? 300 : 0)
|
||||
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
|
||||
.start();
|
||||
if (userAvatarHiding) {
|
||||
getOverlay().add(mMultiUserAvatar);
|
||||
mMultiUserAvatar.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(300)
|
||||
.setStartDelay(0)
|
||||
.setInterpolator(Interpolators.ALPHA_OUT)
|
||||
.withEndAction(() -> {
|
||||
mMultiUserAvatar.setAlpha(1f);
|
||||
getOverlay().remove(mMultiUserAvatar);
|
||||
})
|
||||
.start();
|
||||
|
||||
} else {
|
||||
mMultiUserAvatar.setAlpha(0f);
|
||||
mMultiUserAvatar.animate()
|
||||
.alpha(1f)
|
||||
.setDuration(300)
|
||||
.setStartDelay(200)
|
||||
.setInterpolator(Interpolators.ALPHA_IN);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisibility(int visibility) {
|
||||
super.setVisibility(visibility);
|
||||
@@ -474,6 +437,7 @@ public class KeyguardStatusBarView extends RelativeLayout {
|
||||
iconManager.setTint(iconColor);
|
||||
}
|
||||
|
||||
mDarkChange.setValue(new DarkChange(mEmptyTintRect, intensity, iconColor));
|
||||
applyDarkness(R.id.battery, mEmptyTintRect, intensity, iconColor);
|
||||
applyDarkness(R.id.clock, mEmptyTintRect, intensity, iconColor);
|
||||
}
|
||||
@@ -536,4 +500,8 @@ public class KeyguardStatusBarView extends RelativeLayout {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
Trace.endSection();
|
||||
}
|
||||
|
||||
public StateFlow<DarkChange> darkChangeFlow() {
|
||||
return FlowKt.asStateFlow(mDarkChange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,8 +69,6 @@ import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
|
||||
import com.android.systemui.util.ViewController;
|
||||
import com.android.systemui.util.settings.SecureSettings;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -78,6 +76,8 @@ import java.util.concurrent.Executor;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
/** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */
|
||||
public class KeyguardStatusBarViewController extends ViewController<KeyguardStatusBarView> {
|
||||
private static final String TAG = "KeyguardStatusBarViewController";
|
||||
@@ -119,6 +119,9 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
|
||||
private final Object mLock = new Object();
|
||||
private final KeyguardLogger mLogger;
|
||||
|
||||
private View mSystemIconsContainer;
|
||||
private final StatusOverlayHoverListenerFactory mStatusOverlayHoverListenerFactory;
|
||||
|
||||
// TODO(b/273443374): remove
|
||||
private NotificationMediaManager mNotificationMediaManager;
|
||||
|
||||
@@ -286,7 +289,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
|
||||
CommandQueue commandQueue,
|
||||
@Main Executor mainExecutor,
|
||||
KeyguardLogger logger,
|
||||
NotificationMediaManager notificationMediaManager
|
||||
NotificationMediaManager notificationMediaManager,
|
||||
StatusOverlayHoverListenerFactory statusOverlayHoverListenerFactory
|
||||
) {
|
||||
super(view);
|
||||
mCarrierTextController = carrierTextController;
|
||||
@@ -339,6 +343,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
|
||||
this::updateViewState
|
||||
);
|
||||
mNotificationMediaManager = notificationMediaManager;
|
||||
mStatusOverlayHoverListenerFactory = statusOverlayHoverListenerFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -363,6 +368,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
|
||||
mTintedIconManager.setBlockList(getBlockedIcons());
|
||||
mStatusBarIconController.addIconGroup(mTintedIconManager);
|
||||
}
|
||||
mSystemIconsContainer = mView.findViewById(R.id.system_icons);
|
||||
StatusOverlayHoverListener hoverListener = mStatusOverlayHoverListenerFactory
|
||||
.createDarkAwareListener(mSystemIconsContainer, mView.darkChangeFlow());
|
||||
mSystemIconsContainer.setOnHoverListener(hoverListener);
|
||||
mView.setOnApplyWindowInsetsListener(
|
||||
(view, windowInsets) -> mView.updateWindowInsets(windowInsets, mInsetsProvider));
|
||||
mSecureSettings.registerContentObserverForUser(
|
||||
@@ -376,6 +385,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
|
||||
|
||||
@Override
|
||||
protected void onViewDetached() {
|
||||
mSystemIconsContainer.setOnHoverListener(null);
|
||||
mConfigurationController.removeCallback(mConfigurationListener);
|
||||
mAnimationScheduler.removeCallback(mAnimationCallback);
|
||||
mUserInfoController.removeCallback(mOnUserInfoChangedListener);
|
||||
|
||||
@@ -63,9 +63,12 @@ class PhoneStatusBarViewController private constructor(
|
||||
private val userChipViewModel: StatusBarUserChipViewModel,
|
||||
private val viewUtil: ViewUtil,
|
||||
private val featureFlags: FeatureFlags,
|
||||
private val configurationController: ConfigurationController
|
||||
private val configurationController: ConfigurationController,
|
||||
private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
|
||||
) : ViewController<PhoneStatusBarView>(view) {
|
||||
|
||||
private lateinit var statusContainer: View
|
||||
|
||||
private val configurationListener = object : ConfigurationController.ConfigurationListener {
|
||||
override fun onConfigChanged(newConfig: Configuration?) {
|
||||
mView.updateResources()
|
||||
@@ -73,6 +76,9 @@ class PhoneStatusBarViewController private constructor(
|
||||
}
|
||||
|
||||
override fun onViewAttached() {
|
||||
statusContainer = mView.findViewById(R.id.system_icons)
|
||||
statusContainer.setOnHoverListener(
|
||||
statusOverlayHoverListenerFactory.createDarkAwareListener(statusContainer))
|
||||
if (moveFromCenterAnimationController == null) return
|
||||
|
||||
val statusBarLeftSide: View = mView.findViewById(R.id.status_bar_start_side_except_heads_up)
|
||||
@@ -104,6 +110,7 @@ class PhoneStatusBarViewController private constructor(
|
||||
}
|
||||
|
||||
override fun onViewDetached() {
|
||||
statusContainer.setOnHoverListener(null)
|
||||
progressProvider?.setReadyToHandleTransition(false)
|
||||
moveFromCenterAnimationController?.onViewDetached()
|
||||
configurationController.removeCallback(configurationListener)
|
||||
@@ -245,6 +252,7 @@ class PhoneStatusBarViewController private constructor(
|
||||
private val shadeLogger: ShadeLogger,
|
||||
private val viewUtil: ViewUtil,
|
||||
private val configurationController: ConfigurationController,
|
||||
private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
|
||||
) {
|
||||
fun create(
|
||||
view: PhoneStatusBarView
|
||||
@@ -268,7 +276,8 @@ class PhoneStatusBarViewController private constructor(
|
||||
userChipViewModel,
|
||||
viewUtil,
|
||||
featureFlags,
|
||||
configurationController
|
||||
configurationController,
|
||||
statusOverlayHoverListenerFactory,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.PaintDrawable
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.View.OnHoverListener
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
import com.android.systemui.lifecycle.repeatWhenAttached
|
||||
import com.android.systemui.plugins.DarkIconDispatcher
|
||||
import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange
|
||||
import com.android.systemui.statusbar.policy.ConfigurationController
|
||||
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class StatusOverlayHoverListenerFactory
|
||||
@Inject
|
||||
constructor(
|
||||
@Main private val resources: Resources,
|
||||
private val configurationController: ConfigurationController,
|
||||
private val darkIconDispatcher: SysuiDarkIconDispatcher,
|
||||
) {
|
||||
|
||||
/** Creates listener always using the same light color for overlay */
|
||||
fun createListener(view: View) =
|
||||
StatusOverlayHoverListener(
|
||||
view,
|
||||
configurationController,
|
||||
resources,
|
||||
flowOf(HoverTheme.LIGHT),
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates listener using [DarkIconDispatcher] to determine light or dark color of the overlay
|
||||
*/
|
||||
fun createDarkAwareListener(view: View) =
|
||||
createDarkAwareListener(view, darkIconDispatcher.darkChangeFlow())
|
||||
|
||||
/**
|
||||
* Creates listener using provided [DarkChange] producer to determine light or dark color of the
|
||||
* overlay
|
||||
*/
|
||||
fun createDarkAwareListener(view: View, darkFlow: StateFlow<DarkChange>) =
|
||||
StatusOverlayHoverListener(
|
||||
view,
|
||||
configurationController,
|
||||
resources,
|
||||
darkFlow.map { toHoverTheme(view, it) },
|
||||
)
|
||||
|
||||
private fun toHoverTheme(view: View, darkChange: DarkChange): HoverTheme {
|
||||
val calculatedTint = DarkIconDispatcher.getTint(darkChange.areas, view, darkChange.tint)
|
||||
// currently calculated tint is either white or some shade of black.
|
||||
// So checking for Color.WHITE is deterministic compared to checking for Color.BLACK.
|
||||
// In the future checking Color.luminance() might be more appropriate.
|
||||
return if (calculatedTint == Color.WHITE) HoverTheme.LIGHT else HoverTheme.DARK
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* theme of hover drawable - it's different from device theme. This theme depends on view's
|
||||
* background and/or dark value returned from [DarkIconDispatcher]
|
||||
*/
|
||||
enum class HoverTheme {
|
||||
LIGHT,
|
||||
DARK
|
||||
}
|
||||
|
||||
/**
|
||||
* [OnHoverListener] that adds [Drawable] overlay on top of the status icons when cursor/stylus
|
||||
* starts hovering over them and removes overlay when status icons are no longer hovered
|
||||
*/
|
||||
class StatusOverlayHoverListener(
|
||||
view: View,
|
||||
configurationController: ConfigurationController,
|
||||
private val resources: Resources,
|
||||
private val themeFlow: Flow<HoverTheme>,
|
||||
) : OnHoverListener {
|
||||
|
||||
@ColorInt private var darkColor: Int = 0
|
||||
@ColorInt private var lightColor: Int = 0
|
||||
private var cornerRadius = 0f
|
||||
|
||||
private var lastTheme = HoverTheme.LIGHT
|
||||
|
||||
val backgroundColor
|
||||
get() = if (lastTheme == HoverTheme.LIGHT) lightColor else darkColor
|
||||
|
||||
init {
|
||||
view.repeatWhenAttached {
|
||||
lifecycleScope.launch {
|
||||
val configurationListener =
|
||||
object : ConfigurationListener {
|
||||
override fun onConfigChanged(newConfig: Configuration?) {
|
||||
updateResources()
|
||||
}
|
||||
}
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
configurationController.addCallback(configurationListener)
|
||||
}
|
||||
configurationController.removeCallback(configurationListener)
|
||||
}
|
||||
lifecycleScope.launch { themeFlow.collect { lastTheme = it } }
|
||||
}
|
||||
updateResources()
|
||||
}
|
||||
|
||||
override fun onHover(v: View, event: MotionEvent): Boolean {
|
||||
if (event.action == MotionEvent.ACTION_HOVER_ENTER) {
|
||||
val drawable =
|
||||
PaintDrawable(backgroundColor).apply {
|
||||
setCornerRadius(cornerRadius)
|
||||
setBounds(0, 0, v.width, v.height)
|
||||
}
|
||||
v.overlay.add(drawable)
|
||||
} else if (event.action == MotionEvent.ACTION_HOVER_EXIT) {
|
||||
v.overlay.clear()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateResources() {
|
||||
lightColor = resources.getColor(R.color.status_bar_icons_hover_color_light)
|
||||
darkColor = resources.getColor(R.color.status_bar_icons_hover_color_dark)
|
||||
cornerRadius = resources.getDimension(R.dimen.status_icons_hover_state_background_radius)
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,16 @@
|
||||
|
||||
package com.android.systemui.statusbar.phone;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
import com.android.systemui.Dumpable;
|
||||
import com.android.systemui.plugins.DarkIconDispatcher;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow;
|
||||
|
||||
/**
|
||||
* Dispatches events to {@link DarkReceiver}s about changes in darkness, tint area
|
||||
* and dark intensity.
|
||||
@@ -29,4 +36,26 @@ public interface SysuiDarkIconDispatcher extends DarkIconDispatcher, Dumpable {
|
||||
* @return LightBarTransitionsController
|
||||
*/
|
||||
LightBarTransitionsController getTransitionsController();
|
||||
|
||||
/**
|
||||
* Flow equivalent of registering {@link DarkReceiver} using
|
||||
* {@link DarkIconDispatcher#addDarkReceiver(DarkReceiver)}
|
||||
*/
|
||||
StateFlow<DarkChange> darkChangeFlow();
|
||||
|
||||
/** Model for {@link #darkChangeFlow()} */
|
||||
class DarkChange {
|
||||
|
||||
public static final DarkChange EMPTY = new DarkChange(new ArrayList<>(), 0, 0);
|
||||
|
||||
public DarkChange(Collection<Rect> areas, float darkIntensity, int tint) {
|
||||
this.areas = areas;
|
||||
this.darkIntensity = darkIntensity;
|
||||
this.tint = tint;
|
||||
}
|
||||
|
||||
public final Collection<Rect> areas;
|
||||
public final float darkIntensity;
|
||||
public final int tint;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ import com.android.systemui.shade.carrier.ShadeCarrierGroupController
|
||||
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
|
||||
import com.android.systemui.statusbar.phone.StatusBarIconController
|
||||
import com.android.systemui.statusbar.phone.StatusIconContainer
|
||||
import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory
|
||||
import com.android.systemui.statusbar.policy.Clock
|
||||
import com.android.systemui.statusbar.policy.FakeConfigurationController
|
||||
import com.android.systemui.statusbar.policy.NextAlarmController
|
||||
@@ -123,6 +124,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() {
|
||||
@Mock private lateinit var qsBatteryModeController: QsBatteryModeController
|
||||
@Mock private lateinit var nextAlarmController: NextAlarmController
|
||||
@Mock private lateinit var activityStarter: ActivityStarter
|
||||
@Mock private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory
|
||||
|
||||
@JvmField @Rule val mockitoRule = MockitoJUnit.rule()
|
||||
var viewVisibility = View.GONE
|
||||
@@ -194,6 +196,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() {
|
||||
qsBatteryModeController,
|
||||
nextAlarmController,
|
||||
activityStarter,
|
||||
mStatusOverlayHoverListenerFactory
|
||||
)
|
||||
whenever(view.isAttachedToWindow).thenReturn(true)
|
||||
shadeHeaderController.init()
|
||||
|
||||
@@ -122,6 +122,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase {
|
||||
@Mock private KeyguardLogger mLogger;
|
||||
|
||||
@Mock private NotificationMediaManager mNotificationMediaManager;
|
||||
@Mock private StatusOverlayHoverListenerFactory mStatusOverlayHoverListenerFactory;
|
||||
|
||||
private TestShadeViewStateProvider mShadeViewStateProvider;
|
||||
private KeyguardStatusBarView mKeyguardStatusBarView;
|
||||
@@ -171,7 +172,8 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase {
|
||||
mCommandQueue,
|
||||
mFakeExecutor,
|
||||
mLogger,
|
||||
mNotificationMediaManager
|
||||
mNotificationMediaManager,
|
||||
mStatusOverlayHoverListenerFactory
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
|
||||
@Mock
|
||||
private lateinit var configurationController: ConfigurationController
|
||||
@Mock
|
||||
private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory
|
||||
@Mock
|
||||
private lateinit var userChipViewModel: StatusBarUserChipViewModel
|
||||
@Mock
|
||||
private lateinit var centralSurfacesImpl: CentralSurfacesImpl
|
||||
@@ -204,7 +206,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
|
||||
sceneInteractor,
|
||||
shadeLogger,
|
||||
viewUtil,
|
||||
configurationController
|
||||
configurationController,
|
||||
mStatusOverlayHoverListenerFactory
|
||||
).create(view).also {
|
||||
it.init()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.PaintDrawable
|
||||
import android.os.SystemClock
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper
|
||||
import android.testing.TestableLooper.RunWithLooper
|
||||
import android.testing.ViewUtils
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroupOverlay
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange
|
||||
import com.android.systemui.statusbar.policy.FakeConfigurationController
|
||||
import com.android.systemui.util.mockito.argumentCaptor
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.mockito.whenever
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito.verify
|
||||
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@RunWithLooper(setAsMainLooper = true)
|
||||
@SmallTest
|
||||
class StatusOverlayHoverListenerTest : SysuiTestCase() {
|
||||
|
||||
private val viewOverlay = mock<ViewGroupOverlay>()
|
||||
private val overlayCaptor = argumentCaptor<Drawable>()
|
||||
private val darkDispatcher = mock<SysuiDarkIconDispatcher>()
|
||||
private val darkChange: MutableStateFlow<DarkChange> = MutableStateFlow(DarkChange.EMPTY)
|
||||
|
||||
private val factory =
|
||||
StatusOverlayHoverListenerFactory(
|
||||
context.resources,
|
||||
FakeConfigurationController(),
|
||||
darkDispatcher
|
||||
)
|
||||
private val view = TestableStatusContainer(context, viewOverlay)
|
||||
|
||||
private lateinit var looper: TestableLooper
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
looper = TestableLooper.get(this)
|
||||
whenever(darkDispatcher.darkChangeFlow()).thenReturn(darkChange)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onHoverStarted_addsOverlay() {
|
||||
view.setUpHoverListener()
|
||||
|
||||
view.hoverStarted()
|
||||
|
||||
assertThat(overlayDrawable).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onHoverEnded_removesOverlay() {
|
||||
view.setUpHoverListener()
|
||||
|
||||
view.hoverStarted() // stopped callback will be called only if hover has started
|
||||
view.hoverStopped()
|
||||
|
||||
verify(viewOverlay).clear()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onHoverStarted_overlayHasLightColor() {
|
||||
view.setUpHoverListener()
|
||||
|
||||
view.hoverStarted()
|
||||
|
||||
assertThat(overlayColor)
|
||||
.isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_light))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDarkAwareHoverStarted_withBlackIcons_overlayHasDarkColor() {
|
||||
view.setUpDarkAwareHoverListener()
|
||||
setIconsTint(Color.BLACK)
|
||||
|
||||
view.hoverStarted()
|
||||
|
||||
assertThat(overlayColor)
|
||||
.isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_dark))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onHoverStarted_withBlackIcons_overlayHasLightColor() {
|
||||
view.setUpHoverListener()
|
||||
setIconsTint(Color.BLACK)
|
||||
|
||||
view.hoverStarted()
|
||||
|
||||
assertThat(overlayColor)
|
||||
.isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_light))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDarkAwareHoverStarted_withWhiteIcons_overlayHasLightColor() {
|
||||
view.setUpDarkAwareHoverListener()
|
||||
setIconsTint(Color.WHITE)
|
||||
|
||||
view.hoverStarted()
|
||||
|
||||
assertThat(overlayColor)
|
||||
.isEqualTo(context.resources.getColor(R.color.status_bar_icons_hover_color_light))
|
||||
}
|
||||
|
||||
private fun View.setUpHoverListener() {
|
||||
setOnHoverListener(factory.createListener(view))
|
||||
attachView(view)
|
||||
}
|
||||
|
||||
private fun View.setUpDarkAwareHoverListener() {
|
||||
setOnHoverListener(factory.createDarkAwareListener(view))
|
||||
attachView(view)
|
||||
}
|
||||
|
||||
private fun attachView(view: View) {
|
||||
ViewUtils.attachView(view)
|
||||
// attaching is async so processAllMessages is required for view.repeatWhenAttached to run
|
||||
looper.processAllMessages()
|
||||
}
|
||||
|
||||
private val overlayDrawable: Drawable
|
||||
get() {
|
||||
verify(viewOverlay).add(overlayCaptor.capture())
|
||||
return overlayCaptor.value
|
||||
}
|
||||
|
||||
private val overlayColor
|
||||
get() = (overlayDrawable as PaintDrawable).paint.color
|
||||
|
||||
private fun setIconsTint(@ColorInt color: Int) {
|
||||
// passing empty ArrayList is equivalent to just accepting passed color as icons color
|
||||
darkChange.value = DarkChange(/* areas= */ ArrayList(), /* darkIntensity= */ 1f, color)
|
||||
}
|
||||
|
||||
private fun TestableStatusContainer.hoverStarted() {
|
||||
injectHoverEvent(hoverEvent(MotionEvent.ACTION_HOVER_ENTER))
|
||||
}
|
||||
|
||||
private fun TestableStatusContainer.hoverStopped() {
|
||||
injectHoverEvent(hoverEvent(MotionEvent.ACTION_HOVER_EXIT))
|
||||
}
|
||||
|
||||
class TestableStatusContainer(context: Context, private val mockOverlay: ViewGroupOverlay) :
|
||||
LinearLayout(context) {
|
||||
|
||||
fun injectHoverEvent(event: MotionEvent) = dispatchHoverEvent(event)
|
||||
|
||||
override fun getOverlay() = mockOverlay
|
||||
}
|
||||
|
||||
private fun hoverEvent(action: Int): MotionEvent {
|
||||
return MotionEvent.obtain(
|
||||
/* downTime= */ SystemClock.uptimeMillis(),
|
||||
/* eventTime= */ SystemClock.uptimeMillis(),
|
||||
/* action= */ action,
|
||||
/* x= */ 0f,
|
||||
/* y= */ 0f,
|
||||
/* metaState= */ 0
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user