Initial support for camera cutout in CollapsedStatusBar

- Display a space view exactly covering where the display cutout is

- Custom layout for system_icons because this view needs to now layout
right-to-left, and hide icons that don't fit. Similar to notification
icon container but in the other direction. Still needs dots and to limit
the # of icons

- When in landscape/seascape, the cutout space disappears and instead
the status bar insets itself by the same amount that the window is
letterboxed

- Moved battery percent back to the right of the battery because the
time is no longer on that side

Test: adb shell cmd overlay enable
com.android.internal.display.cutout.emulation && adb shell stop && adb
shell start # to start emulation
Bug: 63772836

Change-Id: I8071bfb4983a9d9306df1487cdac956494e80c28
This commit is contained in:
Evan Laird
2018-01-12 14:26:10 -05:00
parent 588a06f5a2
commit 058c8aee39
9 changed files with 344 additions and 26 deletions

View File

@@ -25,6 +25,6 @@
android:textAppearance="@style/TextAppearance.StatusBar.Expanded.Clock"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center_vertical|start"
android:paddingEnd="@dimen/battery_level_padding_start"
android:paddingStart="@dimen/battery_level_padding_start"
android:importantForAccessibility="no"
/>

View File

@@ -54,30 +54,47 @@
android:layout_height="match_parent"
android:layout="@layout/operator_name" />
<com.android.systemui.statusbar.policy.Clock
android:id="@+id/clock"
android:textAppearance="@style/TextAppearance.StatusBar.Clock"
android:layout_width="wrap_content"
<LinearLayout
android:layout_height="match_parent"
android:singleLine="true"
android:paddingStart="@dimen/status_bar_left_clock_starting_padding"
android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
android:gravity="center_vertical|start"
android:layout_width="0dp"
android:layout_weight="1"
>
<com.android.systemui.statusbar.policy.Clock
android:id="@+id/clock"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textAppearance="@style/TextAppearance.StatusBar.Clock"
android:singleLine="true"
android:paddingStart="@dimen/status_bar_left_clock_starting_padding"
android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
android:gravity="center_vertical|start"
/>
<!-- The alpha of this area is controlled from both PhoneStatusBarTransitions and
PhoneStatusBar (DISABLE_NOTIFICATION_ICONS). -->
<com.android.systemui.statusbar.AlphaOptimizedFrameLayout
android:id="@+id/notification_icon_area"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="horizontal" />
</LinearLayout>
<!-- Space should cover the notch (if it exists) and let other views lay out around it -->
<android.widget.Space
android:id="@+id/cutout_space_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="center_horizontal|center_vertical"
/>
<!-- The alpha of this area is controlled from both PhoneStatusBarTransitions and
PhoneStatusBar (DISABLE_NOTIFICATION_ICONS). -->
<com.android.systemui.statusbar.AlphaOptimizedFrameLayout
android:id="@+id/notification_icon_area"
android:layout_width="0dip"
<com.android.keyguard.AlphaOptimizedLinearLayout android:id="@+id/system_icon_area"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="horizontal" />
<com.android.keyguard.AlphaOptimizedLinearLayout android:id="@+id/system_icon_area"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical|end"
>
<include layout="@layout/system_icons" />

View File

@@ -16,12 +16,13 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/system_icons"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical">
<com.android.keyguard.AlphaOptimizedLinearLayout android:id="@+id/statusIcons"
android:layout_width="wrap_content"
<com.android.systemui.statusbar.phone.StatusIconContainer android:id="@+id/statusIcons"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal"/>

View File

@@ -93,5 +93,8 @@
<item type="id" name="action_snooze_long"/>
<item type="id" name="action_snooze_longer"/>
<item type="id" name="action_snooze_assistant_suggestion_1"/>
<!-- For StatusBarIconContainer to tag its icon views -->
<item type="id" name="status_bar_view_state_tag" />
</resources>

View File

@@ -224,7 +224,6 @@ public class BatteryMeterView extends LinearLayout implements
if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor);
updatePercentText();
addView(mBatteryPercentView,
0,
new ViewGroup.LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT));

View File

@@ -16,26 +16,34 @@
package com.android.systemui.statusbar.phone;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.EventLog;
import android.view.DisplayCutout;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import com.android.systemui.BatteryMeterView;
import com.android.systemui.DejankUtils;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import com.android.systemui.Dependency;
import com.android.systemui.EventLogTags;
import com.android.systemui.R;
import com.android.systemui.statusbar.policy.DarkIconDispatcher;
import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
import com.android.systemui.util.leak.RotationUtils;
public class PhoneStatusBarView extends PanelBar {
private static final String TAG = "PhoneStatusBarView";
private static final boolean DEBUG = StatusBar.DEBUG;
private static final boolean DEBUG_GESTURES = false;
private static final int NO_VALUE = Integer.MIN_VALUE;
StatusBar mBar;
@@ -53,6 +61,10 @@ public class PhoneStatusBarView extends PanelBar {
}
};
private DarkReceiver mBattery;
private int mLastOrientation;
private View mCutoutSpace;
@Nullable
private DisplayCutout mDisplayCutout;
public PhoneStatusBarView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -76,6 +88,7 @@ public class PhoneStatusBarView extends PanelBar {
public void onFinishInflate() {
mBarTransitions.init();
mBattery = findViewById(R.id.battery);
mCutoutSpace = findViewById(R.id.cutout_space_view);
}
@Override
@@ -83,12 +96,51 @@ public class PhoneStatusBarView extends PanelBar {
super.onAttachedToWindow();
// Always have Battery meters in the status bar observe the dark/light modes.
Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mBattery);
if (updateOrientationAndCutout(getResources().getConfiguration().orientation)) {
postUpdateLayoutForCutout();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mBattery);
mDisplayCutout = null;
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// May trigger cutout space layout-ing
if (updateOrientationAndCutout(newConfig.orientation)) {
postUpdateLayoutForCutout();
}
}
/**
*
* @param newOrientation may pass NO_VALUE for no change
* @return boolean indicating if we need to update the cutout location / margins
*/
private boolean updateOrientationAndCutout(int newOrientation) {
boolean changed = false;
if (newOrientation != NO_VALUE) {
if (mLastOrientation != newOrientation) {
changed = true;
mLastOrientation = newOrientation;
}
}
if (mDisplayCutout == null) {
DisplayCutout cutout = getRootWindowInsets().getDisplayCutout();
if (cutout != null) {
changed = true;
mDisplayCutout = cutout;
}
}
return changed;
}
@Override
@@ -214,4 +266,75 @@ public class PhoneStatusBarView extends PanelBar {
R.dimen.status_bar_height);
setLayoutParams(layoutParams);
}
private void updateLayoutForCutout() {
updateCutoutLocation();
updateSafeInsets();
}
private void postUpdateLayoutForCutout() {
Runnable r = new Runnable() {
@Override
public void run() {
updateLayoutForCutout();
}
};
// Let the cutout emulation draw first
postDelayed(r, 0);
}
private void updateCutoutLocation() {
if (mDisplayCutout == null || mDisplayCutout.isEmpty()
|| mLastOrientation != ORIENTATION_PORTRAIT) {
mCutoutSpace.setVisibility(View.GONE);
return;
}
mCutoutSpace.setVisibility(View.VISIBLE);
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mCutoutSpace.getLayoutParams();
lp.width = mDisplayCutout.getBoundingRect().width();
lp.height = mDisplayCutout.getBoundingRect().height();
}
private void updateSafeInsets() {
// Depending on our rotation, we may have to work around a cutout in the middle of the view,
// or letterboxing from the right or left sides.
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
if (mDisplayCutout == null || mDisplayCutout.isEmpty()) {
lp.leftMargin = 0;
lp.rightMargin = 0;
return;
}
int leftMargin = 0;
int rightMargin = 0;
switch (RotationUtils.getRotation(getContext())) {
/*
* Landscape: <-|
* Seascape: |->
*/
case RotationUtils.ROTATION_LANDSCAPE:
leftMargin = getDisplayCutoutHeight();
break;
case RotationUtils.ROTATION_SEASCAPE:
rightMargin = getDisplayCutoutHeight();
break;
default:
break;
}
lp.leftMargin = leftMargin;
lp.rightMargin = rightMargin;
}
//TODO: Find a better way
private int getDisplayCutoutHeight() {
if (mDisplayCutout == null || mDisplayCutout.isEmpty()) {
return 0;
}
Rect r = mDisplayCutout.getBoundingRect();
return r.bottom - r.top;
}
}

View File

@@ -62,7 +62,7 @@ public interface StatusBarIconController {
}
/**
* Version of ViewGroup that observers state from the DarkIconDispatcher.
* Version of ViewGroup that observes state from the DarkIconDispatcher.
*/
public static class DarkIconManager extends IconManager {
private final DarkIconDispatcher mDarkIconDispatcher;

View File

@@ -0,0 +1,169 @@
/*
* Copyright (C) 2017 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.
*/
/**
* A container for Status bar system icons. Limits the number of system icons and handles overflow
* similar to NotificationIconController. Can be used to layout nested StatusIconContainers
*
* Children are expected to be of type StatusBarIconView.
*/
package com.android.systemui.statusbar.phone;
import android.annotation.Nullable;
import android.content.Context;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.view.View;
import com.android.keyguard.AlphaOptimizedLinearLayout;
import com.android.systemui.R;
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.stack.ViewState;
public class StatusIconContainer extends AlphaOptimizedLinearLayout {
private static final String TAG = "StatusIconContainer";
private static final int MAX_ICONS = 5;
private static final int MAX_DOTS = 3;
public StatusIconContainer(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
float midY = getHeight() / 2.0f;
// Layout all child views so that we can move them around later
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
int top = (int) (midY - height / 2.0f);
child.layout(0, top, width, top + height);
}
resetViewStates();
calculateIconTranslations();
applyIconStates();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int count = getChildCount();
// Measure all children so that they report the correct width
for (int i = 0; i < count; i++) {
measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
}
}
@Override
public void onViewAdded(View child) {
super.onViewAdded(child);
ViewState vs = new ViewState();
child.setTag(R.id.status_bar_view_state_tag, vs);
}
@Override
public void onViewRemoved(View child) {
super.onViewRemoved(child);
child.setTag(R.id.status_bar_view_state_tag, null);
}
/**
* Layout is happening from end -> start
*/
private void calculateIconTranslations() {
float translationX = getWidth();
float contentStart = getPaddingStart();
int childCount = getChildCount();
// Underflow === don't show content until that index
int firstUnderflowIndex = -1;
android.util.Log.d(TAG, "calculateIconTransitions: start=" + translationX);
//TODO: Dots
for (int i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
if (!(child instanceof StatusBarIconView)) {
continue;
}
ViewState childState = getViewStateFromChild(child);
if (childState == null ) {
continue;
}
// Rely on StatusBarIcon for truth about visibility
if (!((StatusBarIconView) child).getStatusBarIcon().visible) {
childState.hidden = true;
continue;
}
childState.xTranslation = translationX - child.getWidth();
if (childState.xTranslation < contentStart) {
if (firstUnderflowIndex == -1) {
firstUnderflowIndex = i;
}
}
translationX -= child.getWidth();
}
if (firstUnderflowIndex != -1) {
for (int i = 0; i <= firstUnderflowIndex; i++) {
View child = getChildAt(i);
ViewState vs = getViewStateFromChild(child);
if (vs != null) {
vs.hidden = true;
}
}
}
}
private void applyIconStates() {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
ViewState vs = getViewStateFromChild(child);
if (vs != null) {
vs.applyToView(child);
}
}
}
private void resetViewStates() {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
ViewState vs = getViewStateFromChild(child);
if (vs == null) {
continue;
}
vs.initFrom(child);
vs.alpha = 1.0f;
if (child instanceof StatusBarIconView) {
vs.hidden = !((StatusBarIconView)child).getStatusBarIcon().visible;
} else {
vs.hidden = false;
}
}
}
private static @Nullable ViewState getViewStateFromChild(View child) {
return (ViewState) child.getTag(R.id.status_bar_view_state_tag);
}
}

View File

@@ -74,6 +74,8 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
assertEquals(View.VISIBLE, mFragment.getView().findViewById(R.id.system_icon_area)
.getVisibility());
assertEquals(View.VISIBLE, mFragment.getView().findViewById(R.id.clock)
.getVisibility());
}
@Test
@@ -87,11 +89,15 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
assertEquals(View.INVISIBLE, mFragment.getView().findViewById(R.id.system_icon_area)
.getVisibility());
assertEquals(View.INVISIBLE, mFragment.getView().findViewById(R.id.clock)
.getVisibility());
fragment.disable(0, 0, false);
assertEquals(View.VISIBLE, mFragment.getView().findViewById(R.id.system_icon_area)
.getVisibility());
assertEquals(View.VISIBLE, mFragment.getView().findViewById(R.id.clock)
.getVisibility());
}
@Test