diff --git a/packages/SystemUI/res/drawable/volume_background_bottom.xml b/packages/SystemUI/res/drawable/volume_background_bottom.xml
new file mode 100644
index 0000000000000..ae3d15991f65f
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_background_bottom.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/volume_background.xml b/packages/SystemUI/res/drawable/volume_background_top.xml
similarity index 85%
rename from packages/SystemUI/res/drawable/volume_background.xml
rename to packages/SystemUI/res/drawable/volume_background_top.xml
index 66f1d0da49905..3cd87fc320615 100644
--- a/packages/SystemUI/res/drawable/volume_background.xml
+++ b/packages/SystemUI/res/drawable/volume_background_top.xml
@@ -20,7 +20,8 @@
-
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/volume_background_top_rounded.xml b/packages/SystemUI/res/drawable/volume_background_top_rounded.xml
new file mode 100644
index 0000000000000..77382e14631f8
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_background_top_rounded.xml
@@ -0,0 +1,29 @@
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/volume_row_rounded_background.xml b/packages/SystemUI/res/drawable/volume_row_rounded_background.xml
new file mode 100644
index 0000000000000..95ba73df61c44
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_row_rounded_background.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog.xml b/packages/SystemUI/res/layout-land-television/volume_dialog.xml
index 8adc6eb753b53..6b5629facd418 100644
--- a/packages/SystemUI/res/layout-land-television/volume_dialog.xml
+++ b/packages/SystemUI/res/layout-land-television/volume_dialog.xml
@@ -32,7 +32,7 @@
android:clipToPadding="false">
+ android:gravity="right">
@@ -80,7 +79,7 @@
+ android:orientation="horizontal">
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/volume_background_bottom"
+ android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
+ android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
+ android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
+ android:background="@drawable/volume_row_rounded_background">
-
+ android:orientation="vertical"
+ android:gravity="right">
@@ -80,7 +78,7 @@
+ android:orientation="horizontal">
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/volume_background_bottom"
+ android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
+ android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
+ android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
+ android:background="@drawable/volume_row_rounded_background">
diff --git a/packages/SystemUI/res/layout/volume_ringer_drawer.xml b/packages/SystemUI/res/layout/volume_ringer_drawer.xml
index 9d14ac46e4062..9b6c92c71e5d0 100644
--- a/packages/SystemUI/res/layout/volume_ringer_drawer.xml
+++ b/packages/SystemUI/res/layout/volume_ringer_drawer.xml
@@ -18,9 +18,16 @@
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 24eac1593195d..b2ab5f782d710 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -472,6 +472,8 @@
20dp
+ 7dp
+
12dp
56dp
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 98d8b29dc1646..50b885815e099 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -28,6 +28,7 @@ import static android.media.AudioManager.STREAM_VOICE_CALL;
import static android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
+import static android.view.View.LAYOUT_DIRECTION_RTL;
import static android.view.View.VISIBLE;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
@@ -52,6 +53,7 @@ import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
+import android.graphics.Outline;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.Region;
@@ -74,11 +76,13 @@ import android.util.Log;
import android.util.Slog;
import android.util.SparseBooleanArray;
import android.view.ContextThemeWrapper;
+import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.AccessibilityDelegate;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
import android.view.ViewPropertyAnimator;
import android.view.ViewStub;
import android.view.ViewTreeObserver;
@@ -147,7 +151,10 @@ public class VolumeDialogImpl implements VolumeDialog,
private final int mDialogShowAnimationDurationMs;
private final int mDialogHideAnimationDurationMs;
+ private final int mDialogWidth;
+ private final int mDialogCornerRadius;
private final int mRingerDrawerItemSize;
+ private final int mRingerRowsPadding;
private final boolean mShowVibrate;
private final int mRingerCount;
private final boolean mShowLowMediaVolumeIcon;
@@ -162,11 +169,25 @@ public class VolumeDialogImpl implements VolumeDialog,
private Window mWindow;
private CustomDialog mDialog;
private ViewGroup mDialogView;
+ private ViewGroup mDialogRowsViewContainer;
private ViewGroup mDialogRowsView;
private ViewGroup mRinger;
- @Nullable private View mRingerAndRowsContainer;
- @Nullable private Drawable mRingerAndRowsContainerBackground;
+ /**
+ * Container for the top part of the dialog, which contains the ringer, the ringer drawer, the
+ * volume rows, and the ellipsis button. This does not include the live caption button.
+ */
+ @Nullable private View mTopContainer;
+
+ /** Container for the ringer icon, and for the (initially hidden) ringer drawer view. */
+ @Nullable private View mRingerAndDrawerContainer;
+
+ /**
+ * Background drawable for the ringer and drawer container. The background's top bound is
+ * initially inset by the height of the (hidden) ringer drawer. When the drawer is animated in,
+ * this top bound is animated to accommodate it.
+ */
+ @Nullable private Drawable mRingerAndDrawerContainerBackground;
private ViewGroup mSelectedRingerContainer;
private ImageView mSelectedRingerIcon;
@@ -196,6 +217,7 @@ public class VolumeDialogImpl implements VolumeDialog,
private final ValueAnimator mAnimateUpBackgroundToMatchDrawer = ValueAnimator.ofFloat(1f, 0f);
private boolean mIsRingerDrawerOpen = false;
+ private float mRingerDrawerClosedAmount = 1f;
private ImageButton mRingerIcon;
private ViewGroup mODICaptionsView;
@@ -266,8 +288,14 @@ public class VolumeDialogImpl implements VolumeDialog,
mDialogRowsView.invalidate();
};
}
+ mDialogWidth = mContext.getResources().getDimensionPixelSize(
+ R.dimen.volume_dialog_panel_width);
+ mDialogCornerRadius = mContext.getResources().getDimensionPixelSize(
+ R.dimen.volume_dialog_panel_width_half);
mRingerDrawerItemSize = mContext.getResources().getDimensionPixelSize(
R.dimen.volume_ringer_drawer_item_size);
+ mRingerRowsPadding = mContext.getResources().getDimensionPixelSize(
+ R.dimen.volume_dialog_ringer_rows_padding);
mShowVibrate = mController.hasVibrator();
// Normal, mute, and possibly vibrate.
@@ -321,7 +349,7 @@ public class VolumeDialogImpl implements VolumeDialog,
// The ringer and rows container has extra height at the top to fit the expanded ringer
// drawer. This area should not be touchable unless the ringer drawer is open.
- if (view == mRingerAndRowsContainer && !mIsRingerDrawerOpen) {
+ if (view == mTopContainer && !mIsRingerDrawerOpen) {
if (!isLandscape()) {
y += getRingerDrawerOpenExtraSize();
} else {
@@ -431,6 +459,46 @@ public class VolumeDialogImpl implements VolumeDialog,
});
}
+ mDialogRowsViewContainer = mDialogView.findViewById(R.id.volume_dialog_rows_container);
+ mTopContainer = mDialogView.findViewById(R.id.volume_dialog_top_container);
+ mRingerAndDrawerContainer = mDialogView.findViewById(
+ R.id.volume_ringer_and_drawer_container);
+
+ if (mRingerAndDrawerContainer != null) {
+ if (isLandscape()) {
+ // In landscape, we need to add padding to the bottom of the ringer drawer so that
+ // when it expands to the left, it doesn't overlap any additional volume rows.
+ mRingerAndDrawerContainer.setPadding(
+ mRingerAndDrawerContainer.getPaddingLeft(),
+ mRingerAndDrawerContainer.getPaddingTop(),
+ mRingerAndDrawerContainer.getPaddingRight(),
+ mRingerRowsPadding);
+
+ // Since the ringer drawer is expanding to the left, outside of the background of
+ // the dialog, it needs its own rounded background drawable. We also need that
+ // background to be rounded on all sides. We'll use a background rounded on all four
+ // corners, and then extend the container's background later to fill in the bottom
+ // corners when the drawer is closed.
+ mRingerAndDrawerContainer.setBackgroundDrawable(
+ mContext.getDrawable(R.drawable.volume_background_top_rounded));
+ }
+
+ // Post to wait for layout so that the background bounds are set.
+ mRingerAndDrawerContainer.post(() -> {
+ final LayerDrawable ringerAndDrawerBg =
+ (LayerDrawable) mRingerAndDrawerContainer.getBackground();
+
+ // Retrieve the ShapeDrawable from within the background - this is what we will
+ // animate up and down when the drawer is opened/closed.
+ if (ringerAndDrawerBg != null && ringerAndDrawerBg.getNumberOfLayers() > 0) {
+ mRingerAndDrawerContainerBackground = ringerAndDrawerBg.getDrawable(0);
+
+ updateBackgroundForDrawerClosedAmount();
+ setTopContainerBackgroundDrawable();
+ }
+ });
+ }
+
mRinger = mDialog.findViewById(R.id.ringer);
if (mRinger != null) {
mRingerIcon = mRinger.findViewById(R.id.ringer_icon);
@@ -510,6 +578,11 @@ public class VolumeDialogImpl implements VolumeDialog,
Configuration.ORIENTATION_LANDSCAPE;
}
+ private boolean isRtl() {
+ return mContext.getResources().getConfiguration().getLayoutDirection()
+ == LAYOUT_DIRECTION_RTL;
+ }
+
public void setStreamImportant(int stream, boolean important) {
mHandler.obtainMessage(H.SET_STREAM_IMPORTANT, stream, important ? 1 : 0).sendToTarget();
}
@@ -612,23 +685,6 @@ public class VolumeDialogImpl implements VolumeDialog,
row.anim = null;
- mRingerAndRowsContainer = mDialogView.findViewById(
- R.id.volume_dialog_ringer_and_rows_container);
-
- if (mRingerAndRowsContainer != null) {
- // Wait for layout so the background bounds are set, then set the background top to the
- // ringer drawer closed position.
- mRingerAndRowsContainer.post(() -> {
- final LayerDrawable bgWrapper =
- ((LayerDrawable) mRingerAndRowsContainer.getBackground());
-
- if (bgWrapper != null) {
- mRingerAndRowsContainerBackground = bgWrapper.getDrawable(0);
- setRingerAndRowsBackgroundTop(1f /* closedAmount */);
- }
- });
- }
-
final LayerDrawable seekbarDrawable =
(LayerDrawable) mContext.getDrawable(R.drawable.volume_row_seekbar);
@@ -764,8 +820,10 @@ public class VolumeDialogImpl implements VolumeDialog,
});
mRingerDrawerIconColorAnimator.setDuration(DRAWER_ANIMATION_DURATION_SHORT);
- mAnimateUpBackgroundToMatchDrawer.addUpdateListener(valueAnimator ->
- setRingerAndRowsBackgroundTop((float) valueAnimator.getAnimatedValue()));
+ mAnimateUpBackgroundToMatchDrawer.addUpdateListener(valueAnimator -> {
+ mRingerDrawerClosedAmount = (float) valueAnimator.getAnimatedValue();
+ updateBackgroundForDrawerClosedAmount();
+ });
}
private ImageView getDrawerIconViewForMode(int mode) {
@@ -792,6 +850,10 @@ public class VolumeDialogImpl implements VolumeDialog,
/** Animates in the ringer drawer. */
private void showRingerDrawer() {
+ if (mIsRingerDrawerOpen) {
+ return;
+ }
+
// Show all ringer icons except the currently selected one, since we're going to animate the
// ringer button to that position.
mRingerDrawerVibrateIcon.setVisibility(
@@ -874,6 +936,10 @@ public class VolumeDialogImpl implements VolumeDialog,
return;
}
+ if (!mIsRingerDrawerOpen) {
+ return;
+ }
+
// Hide the drawer icon for the selected ringer - it's visible in the ringer button and we
// don't want to be able to see it while it animates away.
getDrawerIconViewForMode(mState.ringerModeInternal).setVisibility(INVISIBLE);
@@ -1143,6 +1209,7 @@ public class VolumeDialogImpl implements VolumeDialog,
mController.notifyVisible(true);
mController.getCaptionsComponentState(false);
checkODICaptionsTooltip(false);
+ updateBackgroundForDrawerClosedAmount();
}
protected void rescheduleTimeoutH() {
@@ -1258,15 +1325,65 @@ public class VolumeDialogImpl implements VolumeDialog,
if (!mShowing) {
trimObsoleteH();
}
+
+ // Index of the last row that is actually visible.
+ int rightmostVisibleRowIndex = !isRtl() ? -1 : Short.MAX_VALUE;
+
// apply changes to all rows
for (final VolumeRow row : mRows) {
final boolean isActive = row == activeRow;
final boolean shouldBeVisible = shouldBeVisibleH(row, activeRow);
Util.setVisOrGone(row.view, shouldBeVisible);
+
+ if (shouldBeVisible && mRingerAndDrawerContainerBackground != null) {
+ // For RTL, the rightmost row has the lowest index since child views are laid out
+ // from right to left.
+ rightmostVisibleRowIndex =
+ !isRtl()
+ ? Math.max(rightmostVisibleRowIndex,
+ mDialogRowsView.indexOfChild(row.view))
+ : Math.min(rightmostVisibleRowIndex,
+ mDialogRowsView.indexOfChild(row.view));
+
+ // Add spacing between each of the visible rows - we'll remove the spacing from the
+ // last row after the loop.
+ final ViewGroup.LayoutParams layoutParams = row.view.getLayoutParams();
+ if (layoutParams instanceof LinearLayout.LayoutParams) {
+ final LinearLayout.LayoutParams linearLayoutParams =
+ ((LinearLayout.LayoutParams) layoutParams);
+ if (!isRtl()) {
+ linearLayoutParams.setMarginEnd(mRingerRowsPadding);
+ } else {
+ linearLayoutParams.setMarginStart(mRingerRowsPadding);
+ }
+ }
+
+ // Set the background on each of the rows. We'll remove this from the last row after
+ // the loop, since the last row's background is drawn by the main volume container.
+ row.view.setBackgroundDrawable(
+ mContext.getDrawable(R.drawable.volume_row_rounded_background));
+ }
+
if (row.view.isShown()) {
updateVolumeRowTintH(row, isActive);
}
}
+
+ if (rightmostVisibleRowIndex > -1 && rightmostVisibleRowIndex < Short.MAX_VALUE) {
+ final View lastVisibleChild = mDialogRowsView.getChildAt(rightmostVisibleRowIndex);
+ final ViewGroup.LayoutParams layoutParams = lastVisibleChild.getLayoutParams();
+ // Remove the spacing on the last row, and remove its background since the container is
+ // drawing a background for this row.
+ if (layoutParams instanceof LinearLayout.LayoutParams) {
+ final LinearLayout.LayoutParams linearLayoutParams =
+ ((LinearLayout.LayoutParams) layoutParams);
+ linearLayoutParams.setMarginStart(0);
+ linearLayoutParams.setMarginEnd(0);
+ lastVisibleChild.setBackgroundColor(Color.TRANSPARENT);
+ }
+ }
+
+ updateBackgroundForDrawerClosedAmount();
}
protected void updateRingerH() {
@@ -1742,32 +1859,74 @@ public class VolumeDialogImpl implements VolumeDialog,
return (mRingerCount - 1) * mRingerDrawerItemSize;
}
- /**
- * Sets the top of the background drawable behind the container view for the ringer icon and the
- * volume rows, depending on whether the ringer drawer is open or closed.
- */
- private void setRingerAndRowsBackgroundTop(float drawerClosedAmount) {
- if (mRingerAndRowsContainerBackground == null) {
+ private void updateBackgroundForDrawerClosedAmount() {
+ if (mRingerAndDrawerContainerBackground == null) {
return;
}
- final Rect bounds = mRingerAndRowsContainerBackground.copyBounds();
-
+ final Rect bounds = mRingerAndDrawerContainerBackground.copyBounds();
if (!isLandscape()) {
- // In portrait, the background should fill the full width, but only go up to the ringer
- // icon's top. We'll extend it all the way to the top of the container when the ringer
- // drawer opens.
- bounds.left = 0;
- bounds.top = (int) (drawerClosedAmount * getRingerDrawerOpenExtraSize());
+ bounds.top = (int) (mRingerDrawerClosedAmount * getRingerDrawerOpenExtraSize());
} else {
- // In landscape, the background should be inset by the size of the open drawer, since it
- // opens sideways. It should extend to the top of the container since we haven't left
- // space for the drawer to open upward.
- bounds.left = getRingerDrawerOpenExtraSize();
- bounds.top = 0;
+ bounds.left = (int) (mRingerDrawerClosedAmount * getRingerDrawerOpenExtraSize());
+ }
+ mRingerAndDrawerContainerBackground.setBounds(bounds);
+ }
+
+ /*
+ * The top container is responsible for drawing the solid color background behind the rightmost
+ * (primary) volume row. This is because the volume drawer animates in from below, initially
+ * overlapping the primary row. We need the drawer to draw below the row's SeekBar, since it
+ * looks strange to overlap it, but above the row's background color, since otherwise it will be
+ * clipped.
+ *
+ * Since we can't be both above and below the volume row view, we'll be below it, and render the
+ * background color in the container since they're both above that.
+ */
+ private void setTopContainerBackgroundDrawable() {
+ if (mTopContainer == null) {
+ return;
}
- mRingerAndRowsContainerBackground.setBounds(bounds);
+ final ColorDrawable solidDrawable = new ColorDrawable(
+ Utils.getColorAttrDefaultColor(mContext, com.android.internal.R.attr.colorSurface));
+
+ final LayerDrawable background = new LayerDrawable(new Drawable[] { solidDrawable });
+
+ // Size the solid color to match the primary volume row. In landscape, extend it upwards
+ // slightly so that it fills in the bottom corners of the ringer icon, whose background is
+ // rounded on all sides so that it can expand to the left, outside the dialog's background.
+ background.setLayerSize(0, mDialogWidth,
+ !isLandscape()
+ ? mDialogRowsView.getHeight()
+ : mDialogRowsView.getHeight() + mDialogCornerRadius);
+ // Inset the top so that the color only renders below the ringer drawer, which has its own
+ // background. In landscape, reduce the inset slightly since we are using the background to
+ // fill in the corners of the closed ringer drawer.
+ background.setLayerInsetTop(0,
+ !isLandscape()
+ ? mDialogRowsViewContainer.getTop()
+ : mDialogRowsViewContainer.getTop() - mDialogCornerRadius);
+
+ // Set gravity to top-right, since additional rows will be added on the left.
+ background.setLayerGravity(0, Gravity.TOP | Gravity.RIGHT);
+
+ // In landscape, the ringer drawer animates out to the left (instead of down). Since the
+ // drawer comes from the right (beyond the bounds of the dialog), we should clip it so it
+ // doesn't draw outside the dialog background. This isn't an issue in portrait, since the
+ // drawer animates downward, below the volume row.
+ if (isLandscape()) {
+ mRingerAndDrawerContainer.setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRoundRect(
+ 0, 0, view.getWidth(), view.getHeight(), mDialogCornerRadius);
+ }
+ });
+ mRingerAndDrawerContainer.setClipToOutline(true);
+ }
+
+ mTopContainer.setBackground(background);
}
private final VolumeDialogController.Callbacks mControllerCallbackH