Merge "Adding hover state to system icons in status bar and shade header" into udc-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
e74076cf42
@@ -26,6 +26,7 @@ import com.android.systemui.plugins.annotations.DependsOn;
|
|||||||
import com.android.systemui.plugins.annotations.ProvidesInterface;
|
import com.android.systemui.plugins.annotations.ProvidesInterface;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches events to {@link DarkReceiver}s about changes in darkness, tint area and dark
|
* 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
|
* @return the tint to apply to view depending on the desired tint color and
|
||||||
* the screen tintArea in which to apply that tint
|
* 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)) {
|
if (isInAreas(tintAreas, view)) {
|
||||||
return color;
|
return color;
|
||||||
} else {
|
} else {
|
||||||
@@ -90,7 +91,7 @@ public interface DarkIconDispatcher {
|
|||||||
* @return true if more than half of the view area are in any of the given
|
* @return true if more than half of the view area are in any of the given
|
||||||
* areas, false otherwise
|
* areas, false otherwise
|
||||||
*/
|
*/
|
||||||
static boolean isInAreas(ArrayList<Rect> areas, View view) {
|
static boolean isInAreas(Collection<Rect> areas, View view) {
|
||||||
if (areas.isEmpty()) {
|
if (areas.isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
<color name="qs_detail_button_white">#B3FFFFFF</color><!-- 70% white -->
|
<color name="qs_detail_button_white">#B3FFFFFF</color><!-- 70% white -->
|
||||||
<color name="status_bar_clock_color">#FFFFFFFF</color>
|
<color name="status_bar_clock_color">#FFFFFFFF</color>
|
||||||
<color name="qs_tile_disabled_color">#9E9E9E</color> <!-- 38% black -->
|
<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 -->
|
<!-- The color of the background in the separated list of the Global Actions menu -->
|
||||||
<color name="global_actions_separated_background">#F5F5F5</color>
|
<color name="global_actions_separated_background">#F5F5F5</color>
|
||||||
|
|||||||
@@ -475,6 +475,8 @@
|
|||||||
<!-- Margin start of the system icons super container -->
|
<!-- Margin start of the system icons super container -->
|
||||||
<dimen name="system_icons_super_container_margin_start">16dp</dimen>
|
<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 -->
|
<!-- Width for the notification panel and related windows -->
|
||||||
<dimen name="match_parent">-1px</dimen>
|
<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.StatusBarStateControllerImpl;
|
||||||
import com.android.systemui.statusbar.phone.ActivityStarterImpl;
|
import com.android.systemui.statusbar.phone.ActivityStarterImpl;
|
||||||
import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl;
|
import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl;
|
||||||
|
import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher;
|
||||||
import com.android.systemui.volume.VolumeDialogControllerImpl;
|
import com.android.systemui.volume.VolumeDialogControllerImpl;
|
||||||
|
|
||||||
import dagger.Binds;
|
import dagger.Binds;
|
||||||
@@ -49,6 +50,10 @@ public abstract class PluginModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract DarkIconDispatcher provideDarkIconDispatcher(DarkIconDispatcherImpl controllerImpl);
|
abstract DarkIconDispatcher provideDarkIconDispatcher(DarkIconDispatcherImpl controllerImpl);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract SysuiDarkIconDispatcher provideSysuiDarkIconDispatcher(
|
||||||
|
DarkIconDispatcherImpl controllerImpl);
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
@Binds
|
@Binds
|
||||||
abstract FalsingManager provideFalsingManager(FalsingManagerProxy falsingManagerImpl);
|
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.StatusBarIconController
|
||||||
import com.android.systemui.statusbar.phone.StatusBarLocation
|
import com.android.systemui.statusbar.phone.StatusBarLocation
|
||||||
import com.android.systemui.statusbar.phone.StatusIconContainer
|
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.Clock
|
||||||
import com.android.systemui.statusbar.policy.ConfigurationController
|
import com.android.systemui.statusbar.policy.ConfigurationController
|
||||||
import com.android.systemui.statusbar.policy.NextAlarmController
|
import com.android.systemui.statusbar.policy.NextAlarmController
|
||||||
@@ -99,6 +100,7 @@ constructor(
|
|||||||
private val qsBatteryModeController: QsBatteryModeController,
|
private val qsBatteryModeController: QsBatteryModeController,
|
||||||
private val nextAlarmController: NextAlarmController,
|
private val nextAlarmController: NextAlarmController,
|
||||||
private val activityStarter: ActivityStarter,
|
private val activityStarter: ActivityStarter,
|
||||||
|
private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
|
||||||
) : ViewController<View>(header), Dumpable {
|
) : ViewController<View>(header), Dumpable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -326,6 +328,9 @@ constructor(
|
|||||||
demoModeController.addCallback(demoModeReceiver)
|
demoModeController.addCallback(demoModeReceiver)
|
||||||
statusBarIconController.addIconGroup(iconManager)
|
statusBarIconController.addIconGroup(iconManager)
|
||||||
nextAlarmController.addCallback(nextAlarmCallback)
|
nextAlarmController.addCallback(nextAlarmCallback)
|
||||||
|
systemIcons.setOnHoverListener(
|
||||||
|
statusOverlayHoverListenerFactory.createListener(systemIcons)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewDetached() {
|
override fun onViewDetached() {
|
||||||
@@ -336,6 +341,7 @@ constructor(
|
|||||||
demoModeController.removeCallback(demoModeReceiver)
|
demoModeController.removeCallback(demoModeReceiver)
|
||||||
statusBarIconController.removeIconGroup(iconManager)
|
statusBarIconController.removeIconGroup(iconManager)
|
||||||
nextAlarmController.removeCallback(nextAlarmCallback)
|
nextAlarmController.removeCallback(nextAlarmCallback)
|
||||||
|
systemIcons.setOnHoverListener(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disable(state1: Int, state2: Int, animate: Boolean) {
|
fun disable(state1: Int, state2: Int, animate: Boolean) {
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ import java.util.ArrayList;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
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
|
@SysUISingleton
|
||||||
@@ -47,6 +52,9 @@ public class DarkIconDispatcherImpl implements SysuiDarkIconDispatcher,
|
|||||||
private int mDarkModeIconColorSingleTone;
|
private int mDarkModeIconColorSingleTone;
|
||||||
private int mLightModeIconColorSingleTone;
|
private int mLightModeIconColorSingleTone;
|
||||||
|
|
||||||
|
private final MutableStateFlow<DarkChange> mDarkChangeFlow = StateFlowKt.MutableStateFlow(
|
||||||
|
DarkChange.EMPTY);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
@@ -66,6 +74,11 @@ public class DarkIconDispatcherImpl implements SysuiDarkIconDispatcher,
|
|||||||
return mTransitionsController;
|
return mTransitionsController;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StateFlow<DarkChange> darkChangeFlow() {
|
||||||
|
return FlowKt.asStateFlow(mDarkChangeFlow);
|
||||||
|
}
|
||||||
|
|
||||||
public void addDarkReceiver(DarkReceiver receiver) {
|
public void addDarkReceiver(DarkReceiver receiver) {
|
||||||
mReceivers.put(receiver, receiver);
|
mReceivers.put(receiver, receiver);
|
||||||
receiver.onDarkChanged(mTintAreas, mDarkIntensity, mIconTint);
|
receiver.onDarkChanged(mTintAreas, mDarkIntensity, mIconTint);
|
||||||
@@ -122,6 +135,7 @@ public class DarkIconDispatcherImpl implements SysuiDarkIconDispatcher,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void applyIconTint() {
|
private void applyIconTint() {
|
||||||
|
mDarkChangeFlow.setValue(new DarkChange(mTintAreas, mDarkIntensity, mIconTint));
|
||||||
for (int i = 0; i < mReceivers.size(); i++) {
|
for (int i = 0; i < mReceivers.size(); i++) {
|
||||||
mReceivers.valueAt(i).onDarkChanged(mTintAreas, mDarkIntensity, mIconTint);
|
mReceivers.valueAt(i).onDarkChanged(mTintAreas, mDarkIntensity, mIconTint);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import android.view.DisplayCutout;
|
|||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewTreeObserver;
|
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
@@ -43,11 +42,11 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import com.android.app.animation.Interpolators;
|
|
||||||
import com.android.settingslib.Utils;
|
import com.android.settingslib.Utils;
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.battery.BatteryMeterView;
|
import com.android.systemui.battery.BatteryMeterView;
|
||||||
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
|
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.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
|
||||||
import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder;
|
import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder;
|
||||||
import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
|
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.io.PrintWriter;
|
||||||
import java.util.ArrayList;
|
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.
|
* The header group on Keyguard.
|
||||||
*/
|
*/
|
||||||
@@ -83,6 +87,8 @@ public class KeyguardStatusBarView extends RelativeLayout {
|
|||||||
private int mStatusBarPaddingEnd;
|
private int mStatusBarPaddingEnd;
|
||||||
private int mMinDotWidth;
|
private int mMinDotWidth;
|
||||||
private View mSystemIconsContainer;
|
private View mSystemIconsContainer;
|
||||||
|
private final MutableStateFlow<DarkChange> mDarkChange = StateFlowKt.MutableStateFlow(
|
||||||
|
DarkChange.EMPTY);
|
||||||
|
|
||||||
private View mCutoutSpace;
|
private View mCutoutSpace;
|
||||||
private ViewGroup mStatusIconArea;
|
private ViewGroup mStatusIconArea;
|
||||||
@@ -374,49 +380,6 @@ public class KeyguardStatusBarView extends RelativeLayout {
|
|||||||
return mKeyguardUserAvatarEnabled;
|
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
|
@Override
|
||||||
public void setVisibility(int visibility) {
|
public void setVisibility(int visibility) {
|
||||||
super.setVisibility(visibility);
|
super.setVisibility(visibility);
|
||||||
@@ -474,6 +437,7 @@ public class KeyguardStatusBarView extends RelativeLayout {
|
|||||||
iconManager.setTint(iconColor);
|
iconManager.setTint(iconColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mDarkChange.setValue(new DarkChange(mEmptyTintRect, intensity, iconColor));
|
||||||
applyDarkness(R.id.battery, mEmptyTintRect, intensity, iconColor);
|
applyDarkness(R.id.battery, mEmptyTintRect, intensity, iconColor);
|
||||||
applyDarkness(R.id.clock, mEmptyTintRect, intensity, iconColor);
|
applyDarkness(R.id.clock, mEmptyTintRect, intensity, iconColor);
|
||||||
}
|
}
|
||||||
@@ -536,4 +500,8 @@ public class KeyguardStatusBarView extends RelativeLayout {
|
|||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
Trace.endSection();
|
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.ViewController;
|
||||||
import com.android.systemui.util.settings.SecureSettings;
|
import com.android.systemui.util.settings.SecureSettings;
|
||||||
|
|
||||||
import kotlin.Unit;
|
|
||||||
|
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -78,6 +76,8 @@ import java.util.concurrent.Executor;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import kotlin.Unit;
|
||||||
|
|
||||||
/** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */
|
/** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */
|
||||||
public class KeyguardStatusBarViewController extends ViewController<KeyguardStatusBarView> {
|
public class KeyguardStatusBarViewController extends ViewController<KeyguardStatusBarView> {
|
||||||
private static final String TAG = "KeyguardStatusBarViewController";
|
private static final String TAG = "KeyguardStatusBarViewController";
|
||||||
@@ -119,6 +119,9 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
|
|||||||
private final Object mLock = new Object();
|
private final Object mLock = new Object();
|
||||||
private final KeyguardLogger mLogger;
|
private final KeyguardLogger mLogger;
|
||||||
|
|
||||||
|
private View mSystemIconsContainer;
|
||||||
|
private final StatusOverlayHoverListenerFactory mStatusOverlayHoverListenerFactory;
|
||||||
|
|
||||||
// TODO(b/273443374): remove
|
// TODO(b/273443374): remove
|
||||||
private NotificationMediaManager mNotificationMediaManager;
|
private NotificationMediaManager mNotificationMediaManager;
|
||||||
|
|
||||||
@@ -286,7 +289,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
|
|||||||
CommandQueue commandQueue,
|
CommandQueue commandQueue,
|
||||||
@Main Executor mainExecutor,
|
@Main Executor mainExecutor,
|
||||||
KeyguardLogger logger,
|
KeyguardLogger logger,
|
||||||
NotificationMediaManager notificationMediaManager
|
NotificationMediaManager notificationMediaManager,
|
||||||
|
StatusOverlayHoverListenerFactory statusOverlayHoverListenerFactory
|
||||||
) {
|
) {
|
||||||
super(view);
|
super(view);
|
||||||
mCarrierTextController = carrierTextController;
|
mCarrierTextController = carrierTextController;
|
||||||
@@ -339,6 +343,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
|
|||||||
this::updateViewState
|
this::updateViewState
|
||||||
);
|
);
|
||||||
mNotificationMediaManager = notificationMediaManager;
|
mNotificationMediaManager = notificationMediaManager;
|
||||||
|
mStatusOverlayHoverListenerFactory = statusOverlayHoverListenerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -363,6 +368,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
|
|||||||
mTintedIconManager.setBlockList(getBlockedIcons());
|
mTintedIconManager.setBlockList(getBlockedIcons());
|
||||||
mStatusBarIconController.addIconGroup(mTintedIconManager);
|
mStatusBarIconController.addIconGroup(mTintedIconManager);
|
||||||
}
|
}
|
||||||
|
mSystemIconsContainer = mView.findViewById(R.id.system_icons);
|
||||||
|
StatusOverlayHoverListener hoverListener = mStatusOverlayHoverListenerFactory
|
||||||
|
.createDarkAwareListener(mSystemIconsContainer, mView.darkChangeFlow());
|
||||||
|
mSystemIconsContainer.setOnHoverListener(hoverListener);
|
||||||
mView.setOnApplyWindowInsetsListener(
|
mView.setOnApplyWindowInsetsListener(
|
||||||
(view, windowInsets) -> mView.updateWindowInsets(windowInsets, mInsetsProvider));
|
(view, windowInsets) -> mView.updateWindowInsets(windowInsets, mInsetsProvider));
|
||||||
mSecureSettings.registerContentObserverForUser(
|
mSecureSettings.registerContentObserverForUser(
|
||||||
@@ -376,6 +385,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onViewDetached() {
|
protected void onViewDetached() {
|
||||||
|
mSystemIconsContainer.setOnHoverListener(null);
|
||||||
mConfigurationController.removeCallback(mConfigurationListener);
|
mConfigurationController.removeCallback(mConfigurationListener);
|
||||||
mAnimationScheduler.removeCallback(mAnimationCallback);
|
mAnimationScheduler.removeCallback(mAnimationCallback);
|
||||||
mUserInfoController.removeCallback(mOnUserInfoChangedListener);
|
mUserInfoController.removeCallback(mOnUserInfoChangedListener);
|
||||||
|
|||||||
@@ -63,9 +63,12 @@ class PhoneStatusBarViewController private constructor(
|
|||||||
private val userChipViewModel: StatusBarUserChipViewModel,
|
private val userChipViewModel: StatusBarUserChipViewModel,
|
||||||
private val viewUtil: ViewUtil,
|
private val viewUtil: ViewUtil,
|
||||||
private val featureFlags: FeatureFlags,
|
private val featureFlags: FeatureFlags,
|
||||||
private val configurationController: ConfigurationController
|
private val configurationController: ConfigurationController,
|
||||||
|
private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
|
||||||
) : ViewController<PhoneStatusBarView>(view) {
|
) : ViewController<PhoneStatusBarView>(view) {
|
||||||
|
|
||||||
|
private lateinit var statusContainer: View
|
||||||
|
|
||||||
private val configurationListener = object : ConfigurationController.ConfigurationListener {
|
private val configurationListener = object : ConfigurationController.ConfigurationListener {
|
||||||
override fun onConfigChanged(newConfig: Configuration?) {
|
override fun onConfigChanged(newConfig: Configuration?) {
|
||||||
mView.updateResources()
|
mView.updateResources()
|
||||||
@@ -73,6 +76,9 @@ class PhoneStatusBarViewController private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewAttached() {
|
override fun onViewAttached() {
|
||||||
|
statusContainer = mView.findViewById(R.id.system_icons)
|
||||||
|
statusContainer.setOnHoverListener(
|
||||||
|
statusOverlayHoverListenerFactory.createDarkAwareListener(statusContainer))
|
||||||
if (moveFromCenterAnimationController == null) return
|
if (moveFromCenterAnimationController == null) return
|
||||||
|
|
||||||
val statusBarLeftSide: View = mView.findViewById(R.id.status_bar_start_side_except_heads_up)
|
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() {
|
override fun onViewDetached() {
|
||||||
|
statusContainer.setOnHoverListener(null)
|
||||||
progressProvider?.setReadyToHandleTransition(false)
|
progressProvider?.setReadyToHandleTransition(false)
|
||||||
moveFromCenterAnimationController?.onViewDetached()
|
moveFromCenterAnimationController?.onViewDetached()
|
||||||
configurationController.removeCallback(configurationListener)
|
configurationController.removeCallback(configurationListener)
|
||||||
@@ -245,6 +252,7 @@ class PhoneStatusBarViewController private constructor(
|
|||||||
private val shadeLogger: ShadeLogger,
|
private val shadeLogger: ShadeLogger,
|
||||||
private val viewUtil: ViewUtil,
|
private val viewUtil: ViewUtil,
|
||||||
private val configurationController: ConfigurationController,
|
private val configurationController: ConfigurationController,
|
||||||
|
private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
|
||||||
) {
|
) {
|
||||||
fun create(
|
fun create(
|
||||||
view: PhoneStatusBarView
|
view: PhoneStatusBarView
|
||||||
@@ -268,7 +276,8 @@ class PhoneStatusBarViewController private constructor(
|
|||||||
userChipViewModel,
|
userChipViewModel,
|
||||||
viewUtil,
|
viewUtil,
|
||||||
featureFlags,
|
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;
|
package com.android.systemui.statusbar.phone;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
|
||||||
import com.android.systemui.Dumpable;
|
import com.android.systemui.Dumpable;
|
||||||
import com.android.systemui.plugins.DarkIconDispatcher;
|
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
|
* Dispatches events to {@link DarkReceiver}s about changes in darkness, tint area
|
||||||
* and dark intensity.
|
* and dark intensity.
|
||||||
@@ -29,4 +36,26 @@ public interface SysuiDarkIconDispatcher extends DarkIconDispatcher, Dumpable {
|
|||||||
* @return LightBarTransitionsController
|
* @return LightBarTransitionsController
|
||||||
*/
|
*/
|
||||||
LightBarTransitionsController getTransitionsController();
|
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.StatusBarContentInsetsProvider
|
||||||
import com.android.systemui.statusbar.phone.StatusBarIconController
|
import com.android.systemui.statusbar.phone.StatusBarIconController
|
||||||
import com.android.systemui.statusbar.phone.StatusIconContainer
|
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.Clock
|
||||||
import com.android.systemui.statusbar.policy.FakeConfigurationController
|
import com.android.systemui.statusbar.policy.FakeConfigurationController
|
||||||
import com.android.systemui.statusbar.policy.NextAlarmController
|
import com.android.systemui.statusbar.policy.NextAlarmController
|
||||||
@@ -123,6 +124,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() {
|
|||||||
@Mock private lateinit var qsBatteryModeController: QsBatteryModeController
|
@Mock private lateinit var qsBatteryModeController: QsBatteryModeController
|
||||||
@Mock private lateinit var nextAlarmController: NextAlarmController
|
@Mock private lateinit var nextAlarmController: NextAlarmController
|
||||||
@Mock private lateinit var activityStarter: ActivityStarter
|
@Mock private lateinit var activityStarter: ActivityStarter
|
||||||
|
@Mock private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory
|
||||||
|
|
||||||
@JvmField @Rule val mockitoRule = MockitoJUnit.rule()
|
@JvmField @Rule val mockitoRule = MockitoJUnit.rule()
|
||||||
var viewVisibility = View.GONE
|
var viewVisibility = View.GONE
|
||||||
@@ -194,6 +196,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() {
|
|||||||
qsBatteryModeController,
|
qsBatteryModeController,
|
||||||
nextAlarmController,
|
nextAlarmController,
|
||||||
activityStarter,
|
activityStarter,
|
||||||
|
mStatusOverlayHoverListenerFactory
|
||||||
)
|
)
|
||||||
whenever(view.isAttachedToWindow).thenReturn(true)
|
whenever(view.isAttachedToWindow).thenReturn(true)
|
||||||
shadeHeaderController.init()
|
shadeHeaderController.init()
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase {
|
|||||||
@Mock private KeyguardLogger mLogger;
|
@Mock private KeyguardLogger mLogger;
|
||||||
|
|
||||||
@Mock private NotificationMediaManager mNotificationMediaManager;
|
@Mock private NotificationMediaManager mNotificationMediaManager;
|
||||||
|
@Mock private StatusOverlayHoverListenerFactory mStatusOverlayHoverListenerFactory;
|
||||||
|
|
||||||
private TestShadeViewStateProvider mShadeViewStateProvider;
|
private TestShadeViewStateProvider mShadeViewStateProvider;
|
||||||
private KeyguardStatusBarView mKeyguardStatusBarView;
|
private KeyguardStatusBarView mKeyguardStatusBarView;
|
||||||
@@ -171,7 +172,8 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase {
|
|||||||
mCommandQueue,
|
mCommandQueue,
|
||||||
mFakeExecutor,
|
mFakeExecutor,
|
||||||
mLogger,
|
mLogger,
|
||||||
mNotificationMediaManager
|
mNotificationMediaManager,
|
||||||
|
mStatusOverlayHoverListenerFactory
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
|
|||||||
@Mock
|
@Mock
|
||||||
private lateinit var configurationController: ConfigurationController
|
private lateinit var configurationController: ConfigurationController
|
||||||
@Mock
|
@Mock
|
||||||
|
private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory
|
||||||
|
@Mock
|
||||||
private lateinit var userChipViewModel: StatusBarUserChipViewModel
|
private lateinit var userChipViewModel: StatusBarUserChipViewModel
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var centralSurfacesImpl: CentralSurfacesImpl
|
private lateinit var centralSurfacesImpl: CentralSurfacesImpl
|
||||||
@@ -204,7 +206,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
|
|||||||
sceneInteractor,
|
sceneInteractor,
|
||||||
shadeLogger,
|
shadeLogger,
|
||||||
viewUtil,
|
viewUtil,
|
||||||
configurationController
|
configurationController,
|
||||||
|
mStatusOverlayHoverListenerFactory
|
||||||
).create(view).also {
|
).create(view).also {
|
||||||
it.init()
|
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