Add manage menu to bubble bar expanded view

* Added bubble bar menu view asnd menu view item
* Added bubble bar menu view controller to setup and manage menu presentation
* Added show/hide menu animations
* Added manage menu actions:
	* Don't bubble conversation
	* Open App Settings
	* Close bubble

Test: manual, TBD
Flag: WM_BUBBLE_BAR
Bug: 273307221
Change-Id: I1c858dfdcc06d0e9f656a24c6994dadba9c834bd
This commit is contained in:
Ivan Tkachenko
2023-05-22 19:32:01 +01:00
parent 6bcaace5d2
commit e93184d7b5
15 changed files with 748 additions and 15 deletions

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2020 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<ripple android:color="#99999999">
<item android:drawable="@drawable/bubble_manage_menu_bg" />
</ripple>
</item>
<item android:drawable="@drawable/bubble_manage_menu_bg" />
</selector>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18.59,16.41L20,15L12,7L4,15L5.41,16.41L12,9.83"
android:fillColor="#5F6368"/>
</vector>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
-->
<com.android.wm.shell.bubbles.bar.BubbleBarMenuItemView
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="@dimen/bubble_bar_manage_menu_item_height"
android:gravity="center_vertical"
android:paddingStart="@dimen/bubble_menu_padding"
android:paddingEnd="@dimen/bubble_menu_padding"
android:background="@drawable/bubble_manage_menu_row">
<ImageView
android:id="@+id/bubble_bar_menu_item_icon"
android:layout_width="@dimen/bubble_bar_manage_menu_item_icon_size"
android:layout_height="@dimen/bubble_bar_manage_menu_item_icon_size"
android:contentDescription="@null"/>
<TextView
android:id="@+id/bubble_bar_menu_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textColor="?android:attr/textColorPrimary"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault" />
</com.android.wm.shell.bubbles.bar.BubbleBarMenuItemView>

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
-->
<com.android.wm.shell.bubbles.bar.BubbleBarMenuView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:minWidth="@dimen/bubble_bar_manage_menu_min_width"
android:orientation="vertical"
android:elevation="@dimen/bubble_manage_menu_elevation"
android:paddingTop="@dimen/bubble_bar_manage_menu_padding_top"
android:paddingHorizontal="@dimen/bubble_bar_manage_menu_padding"
android:paddingBottom="@dimen/bubble_bar_manage_menu_padding"
android:clipToPadding="false">
<LinearLayout
android:id="@+id/bubble_bar_manage_menu_bubble_section"
android:layout_width="match_parent"
android:layout_height="@dimen/bubble_bar_manage_menu_item_height"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="14dp"
android:paddingEnd="12dp"
android:background="@drawable/bubble_manage_menu_section"
android:elevation="@dimen/bubble_manage_menu_elevation">
<ImageView
android:id="@+id/bubble_bar_manage_menu_bubble_icon"
android:layout_width="@dimen/bubble_menu_icon_size"
android:layout_height="@dimen/bubble_menu_icon_size"
android:contentDescription="@null" />
<TextView
android:id="@+id/bubble_bar_manage_menu_bubble_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:textColor="?android:attr/textColorPrimary"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault" />
<ImageView
android:id="@+id/bubble_bar_manage_menu_dismiss_icon"
android:layout_width="@dimen/bubble_bar_manage_menu_dismiss_icon_size"
android:layout_height="@dimen/bubble_bar_manage_menu_dismiss_icon_size"
android:layout_marginStart="8dp"
android:contentDescription="@null"
android:src="@drawable/ic_expand_less"
app:tint="?android:attr/textColorPrimary" />
</LinearLayout>
<LinearLayout
android:id="@+id/bubble_bar_manage_menu_actions_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="@dimen/bubble_bar_manage_menu_section_spacing"
android:background="@drawable/bubble_manage_menu_bg"
android:elevation="@dimen/bubble_manage_menu_elevation" />
</com.android.wm.shell.bubbles.bar.BubbleBarMenuView>

View File

@@ -32,6 +32,7 @@
<color name="bubbles_icon_tint">@color/GM2_grey_700</color>
<color name="bubble_bar_expanded_view_handle_light">#EBffffff</color>
<color name="bubble_bar_expanded_view_handle_dark">#99000000</color>
<color name="bubble_bar_expanded_view_menu_close">#DC362E</color>
<!-- PiP -->
<color name="pip_custom_close_bg">#D93025</color>

View File

@@ -234,6 +234,20 @@
<dimen name="bubble_bar_expanded_view_handle_width">128dp</dimen>
<!-- The height of the drag handle shown along with a bubble bar expanded view. -->
<dimen name="bubble_bar_expanded_view_handle_height">4dp</dimen>
<!-- Minimum width of the bubble bar manage menu. -->
<dimen name="bubble_bar_manage_menu_min_width">200dp</dimen>
<!-- Size of the dismiss icon in the bubble bar manage menu. -->
<dimen name="bubble_bar_manage_menu_dismiss_icon_size">16dp</dimen>
<!-- Padding of the bubble bar manage menu, provides space for menu shadows -->
<dimen name="bubble_bar_manage_menu_padding">8dp</dimen>
<!-- Top padding of the bubble bar manage menu -->
<dimen name="bubble_bar_manage_menu_padding_top">2dp</dimen>
<!-- Spacing between sections of the bubble bar manage menu -->
<dimen name="bubble_bar_manage_menu_section_spacing">2dp</dimen>
<!-- Height of an item in the bubble bar manage menu. -->
<dimen name="bubble_bar_manage_menu_item_height">52dp</dimen>
<!-- Size of the icons in the bubble bar manage menu. -->
<dimen name="bubble_bar_manage_menu_item_icon_size">20dp</dimen>
<!-- Bottom and end margin for compat buttons. -->
<dimen name="compat_button_margin">24dp</dimen>

View File

@@ -855,7 +855,8 @@ public class Bubble implements BubbleViewProvider {
return mIsAppBubble;
}
Intent getSettingsIntent(final Context context) {
/** Creates open app settings intent */
public Intent getSettingsIntent(final Context context) {
final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
final int uid = getUid(context);

View File

@@ -720,6 +720,7 @@ public class BubbleController implements ConfigurationChangeListener,
// TODO(b/273312602): consider foldables where we do need a stack view when folded
if (mLayerView == null) {
mLayerView = new BubbleBarLayerView(mContext, this);
mLayerView.setUnBubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
}
} else {
if (mStackView == null) {
@@ -1220,6 +1221,13 @@ public class BubbleController implements ConfigurationChangeListener,
}
}
/**
* Dismiss bubble if it exists and remove it from the stack
*/
public void dismissBubble(Bubble bubble, @Bubbles.DismissReason int reason) {
mBubbleData.dismissBubbleWithKey(bubble.getKey(), reason);
}
/**
* Performs a screenshot that may exclude the bubble layer, if one is present. The screenshot
* can be access via the supplied {@link ScreenshotSync#get()} asynchronously.

View File

@@ -192,6 +192,11 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
}
info.rawBadgeBitmap = iconFactory.getBadgeBitmap(badgedIcon, false).icon;
float[] bubbleBitmapScale = new float[1];
info.bubbleBitmap = iconFactory.getBubbleBitmap(
iconFactory.getBubbleDrawable(c, info.shortcutInfo,
b.getIcon()), bubbleBitmapScale);
return info;
}

View File

@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Outline;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
@@ -33,8 +34,12 @@ import com.android.wm.shell.R;
import com.android.wm.shell.bubbles.Bubble;
import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.BubbleTaskViewHelper;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.taskview.TaskView;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* Expanded view of a bubble when it's part of the bubble bar.
*
@@ -47,6 +52,9 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
private BubbleController mController;
private BubbleTaskViewHelper mBubbleTaskViewHelper;
private BubbleBarMenuViewController mMenuViewController;
private @Nullable Supplier<Rect> mLayerBoundsSupplier;
private @Nullable Consumer<String> mUnBubbleConversationCallback;
private BubbleBarHandleView mHandleView = new BubbleBarHandleView(getContext());
private @Nullable TaskView mTaskView;
@@ -98,6 +106,13 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
});
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// Hide manage menu when view disappears
mMenuViewController.hideMenu(false /* animated */);
}
/** Set the BubbleController on the view, must be called before doing anything else. */
public void initialize(BubbleController controller) {
mController = controller;
@@ -108,6 +123,36 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
addView(mTaskView);
mTaskView.setEnableSurfaceClipping(true);
mTaskView.setCornerRadius(mCornerRadius);
mMenuViewController = new BubbleBarMenuViewController(mContext, this);
mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() {
@Override
public void onMenuVisibilityChanged(boolean visible) {
if (mTaskView == null || mLayerBoundsSupplier == null) return;
// Updates the obscured touchable region for the task surface.
mTaskView.setObscuredTouchRect(visible ? mLayerBoundsSupplier.get() : null);
}
@Override
public void onUnBubbleConversation(Bubble bubble) {
if (mUnBubbleConversationCallback != null) {
mUnBubbleConversationCallback.accept(bubble.getKey());
}
}
@Override
public void onOpenAppSettings(Bubble bubble) {
mController.collapseStack();
mContext.startActivityAsUser(bubble.getSettingsIntent(mContext), bubble.getUser());
}
@Override
public void onDismissBubble(Bubble bubble) {
mController.dismissBubble(bubble, Bubbles.DISMISS_USER_REMOVED);
}
});
mHandleView.setOnClickListener(view -> {
mMenuViewController.showMenu(true /* animated */);
});
}
// TODO (b/275087636): call this when theme/config changes
@@ -183,11 +228,13 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
}
mBubbleTaskViewHelper.cleanUpTaskView();
}
mMenuViewController.hideMenu(false /* animated */);
}
/** Updates the bubble shown in this task view. */
/** Updates the bubble shown in the expanded view. */
public void update(Bubble bubble) {
mBubbleTaskViewHelper.update(bubble);
mMenuViewController.updateMenu(bubble);
}
/** The task id of the activity shown in the task view, if it exists. */
@@ -195,6 +242,17 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
return mBubbleTaskViewHelper != null ? mBubbleTaskViewHelper.getTaskId() : INVALID_TASK_ID;
}
/** Sets layer bounds supplier used for obscured touchable region of task view */
void setLayerBoundsSupplier(@Nullable Supplier<Rect> supplier) {
mLayerBoundsSupplier = supplier;
}
/** Sets the function to call to un-bubble the given conversation. */
public void setUnBubbleConversationCallback(
@Nullable Consumer<String> unBubbleConversationCallback) {
mUnBubbleConversationCallback = unBubbleConversationCallback;
}
/**
* Call when the location or size of the view has changed to update TaskView.
*/

View File

@@ -20,7 +20,6 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Outline;
import android.graphics.Rect;
import android.util.AttributeSet;
@@ -38,35 +37,35 @@ import com.android.wm.shell.R;
public class BubbleBarHandleView extends View {
private static final long COLOR_CHANGE_DURATION = 120;
private final int mHandleWidth;
private final int mHandleHeight;
private final @ColorInt int mHandleLightColor;
private final @ColorInt int mHandleDarkColor;
private int mHandleWidth;
private int mHandleHeight;
private @ColorInt int mHandleLightColor;
private @ColorInt int mHandleDarkColor;
private @Nullable ObjectAnimator mColorChangeAnim;
public BubbleBarHandleView(Context context) {
this(context, null);
this(context, null /* attrs */);
}
public BubbleBarHandleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
this(context, attrs, 0 /* defStyleAttr */);
}
public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
}
public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
Resources resources = context.getResources();
mHandleWidth = resources.getDimensionPixelSize(
mHandleWidth = getResources().getDimensionPixelSize(
R.dimen.bubble_bar_expanded_view_handle_width);
mHandleHeight = resources.getDimensionPixelSize(
mHandleHeight = getResources().getDimensionPixelSize(
R.dimen.bubble_bar_expanded_view_handle_height);
mHandleLightColor = ContextCompat.getColor(context,
mHandleLightColor = ContextCompat.getColor(getContext(),
R.color.bubble_bar_expanded_view_handle_light);
mHandleDarkColor = ContextCompat.getColor(context,
mHandleDarkColor = ContextCompat.getColor(getContext(),
R.color.bubble_bar_expanded_view_handle_dark);
setClipToOutline(true);

View File

@@ -32,6 +32,8 @@ import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.BubbleViewProvider;
import java.util.function.Consumer;
/**
* Similar to {@link com.android.wm.shell.bubbles.BubbleStackView}, this view is added to window
* manager to display bubbles. However, it is only used when bubbles are being displayed in
@@ -53,6 +55,7 @@ public class BubbleBarLayerView extends FrameLayout
@Nullable
private BubbleViewProvider mExpandedBubble;
private BubbleBarExpandedView mExpandedView;
private @Nullable Consumer<String> mUnBubbleConversationCallback;
// TODO(b/273310265) - currently the view is always on the right, need to update for RTL.
/** Whether the expanded view is displaying on the left of the screen or not. */
@@ -146,6 +149,13 @@ public class BubbleBarLayerView extends FrameLayout
final int width = mPositioner.getExpandedViewWidthForBubbleBar();
final int height = mPositioner.getExpandedViewHeightForBubbleBar();
mExpandedView.setVisibility(GONE);
mExpandedView.setUnBubbleConversationCallback(mUnBubbleConversationCallback);
mExpandedView.setLayerBoundsSupplier(() -> new Rect(0, 0, getWidth(), getHeight()));
mExpandedView.setUnBubbleConversationCallback(bubbleKey -> {
if (mUnBubbleConversationCallback != null) {
mUnBubbleConversationCallback.accept(bubbleKey);
}
});
addView(mExpandedView, new FrameLayout.LayoutParams(width, height));
}
@@ -165,6 +175,12 @@ public class BubbleBarLayerView extends FrameLayout
showScrim(false);
}
/** Sets the function to call to un-bubble the given conversation. */
public void setUnBubbleConversationCallback(
@Nullable Consumer<String> unBubbleConversationCallback) {
mUnBubbleConversationCallback = unBubbleConversationCallback;
}
/** Updates the expanded view size and position. */
private void updateExpandedView() {
if (mExpandedView == null) return;

View File

@@ -0,0 +1,77 @@
/*
* 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.wm.shell.bubbles.bar;
import android.annotation.ColorInt;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Icon;
import android.util.AttributeSet;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.wm.shell.R;
/**
* Bubble bar expanded view menu item view to display menu action details
*/
public class BubbleBarMenuItemView extends LinearLayout {
private ImageView mImageView;
private TextView mTextView;
public BubbleBarMenuItemView(Context context) {
this(context, null /* attrs */);
}
public BubbleBarMenuItemView(Context context, AttributeSet attrs) {
this(context, attrs, 0 /* defStyleAttr */);
}
public BubbleBarMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
}
public BubbleBarMenuItemView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mImageView = findViewById(R.id.bubble_bar_menu_item_icon);
mTextView = findViewById(R.id.bubble_bar_menu_item_title);
}
/**
* Update menu item with the details and tint color
*/
void update(Icon icon, String title, @ColorInt int tint) {
if (tint == Color.TRANSPARENT) {
final TypedArray typedArray = getContext().obtainStyledAttributes(
new int[]{android.R.attr.textColorPrimary});
mTextView.setTextColor(typedArray.getColor(0, Color.BLACK));
} else {
icon.setTint(tint);
mTextView.setTextColor(tint);
}
mImageView.setImageIcon(icon);
mTextView.setText(title);
}
}

View File

@@ -0,0 +1,145 @@
/*
* 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.wm.shell.bubbles.bar;
import android.annotation.ColorInt;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Icon;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.wm.shell.R;
import com.android.wm.shell.bubbles.Bubble;
import java.util.ArrayList;
/**
* Bubble bar expanded view menu
*/
public class BubbleBarMenuView extends LinearLayout {
private ViewGroup mBubbleSectionView;
private ViewGroup mActionsSectionView;
private ImageView mBubbleIconView;
private TextView mBubbleTitleView;
public BubbleBarMenuView(Context context) {
this(context, null /* attrs */);
}
public BubbleBarMenuView(Context context, AttributeSet attrs) {
this(context, attrs, 0 /* defStyleAttr */);
}
public BubbleBarMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
}
public BubbleBarMenuView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mBubbleSectionView = findViewById(R.id.bubble_bar_manage_menu_bubble_section);
mActionsSectionView = findViewById(R.id.bubble_bar_manage_menu_actions_section);
mBubbleIconView = findViewById(R.id.bubble_bar_manage_menu_bubble_icon);
mBubbleTitleView = findViewById(R.id.bubble_bar_manage_menu_bubble_title);
}
/** Update menu details with bubble info */
void updateInfo(Bubble bubble) {
if (bubble.getIcon() != null) {
mBubbleIconView.setImageIcon(bubble.getIcon());
} else {
mBubbleIconView.setImageBitmap(bubble.getBubbleIcon());
}
mBubbleTitleView.setText(bubble.getTitle());
}
/**
* Update menu action items views
* @param actions used to populate menu item views
*/
void updateActions(ArrayList<MenuAction> actions) {
mActionsSectionView.removeAllViews();
LayoutInflater inflater = LayoutInflater.from(mContext);
for (MenuAction action : actions) {
BubbleBarMenuItemView itemView = (BubbleBarMenuItemView) inflater.inflate(
R.layout.bubble_bar_menu_item, mActionsSectionView, false);
itemView.update(action.mIcon, action.mTitle, action.mTint);
itemView.setOnClickListener(action.mOnClick);
mActionsSectionView.addView(itemView);
}
}
/** Sets on close menu listener */
void setOnCloseListener(Runnable onClose) {
mBubbleSectionView.setOnClickListener(view -> {
onClose.run();
});
}
/**
* Overridden to proxy to section views alpha.
* @implNote
* If animate alpha on the parent (menu container) view, section view shadows get distorted.
* To prevent distortion and artifacts alpha changes applied directly on the section views.
*/
@Override
public void setAlpha(float alpha) {
mBubbleSectionView.setAlpha(alpha);
mActionsSectionView.setAlpha(alpha);
}
/**
* Overridden to proxy section view alpha value.
* @implNote
* The assumption is that both section views have the same alpha value
*/
@Override
public float getAlpha() {
return mBubbleSectionView.getAlpha();
}
/**
* Menu action details used to create menu items
*/
static class MenuAction {
private Icon mIcon;
private @ColorInt int mTint;
private String mTitle;
private OnClickListener mOnClick;
MenuAction(Icon icon, String title, OnClickListener onClick) {
this(icon, title, Color.TRANSPARENT, onClick);
}
MenuAction(Icon icon, String title, @ColorInt int tint, OnClickListener onClick) {
this.mIcon = icon;
this.mTitle = title;
this.mTint = tint;
this.mOnClick = onClick;
}
}
}

View File

@@ -0,0 +1,242 @@
/*
* 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.wm.shell.bubbles.bar;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Icon;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.core.content.ContextCompat;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.wm.shell.R;
import com.android.wm.shell.animation.PhysicsAnimator;
import com.android.wm.shell.bubbles.Bubble;
import java.util.ArrayList;
/**
* Manages bubble bar expanded view menu presentation and animations
*/
class BubbleBarMenuViewController {
private static final float MENU_INITIAL_SCALE = 0.5f;
private final Context mContext;
private final ViewGroup mRootView;
private @Nullable Listener mListener;
private @Nullable Bubble mBubble;
private @Nullable BubbleBarMenuView mMenuView;
/** A transparent view used to intercept touches to collapse menu when presented */
private @Nullable View mScrimView;
private @Nullable PhysicsAnimator<BubbleBarMenuView> mMenuAnimator;
private PhysicsAnimator.SpringConfig mMenuSpringConfig;
BubbleBarMenuViewController(Context context, ViewGroup rootView) {
mContext = context;
mRootView = rootView;
mMenuSpringConfig = new PhysicsAnimator.SpringConfig(
SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
}
/** Sets menu actions listener */
void setListener(@Nullable Listener listener) {
mListener = listener;
}
/** Update menu with bubble */
void updateMenu(@NonNull Bubble bubble) {
mBubble = bubble;
}
/**
* Show bubble bar expanded view menu
* @param animated if should animate transition
*/
void showMenu(boolean animated) {
if (mMenuView == null || mScrimView == null) {
setupMenu();
}
cancelAnimations();
mMenuView.setVisibility(View.VISIBLE);
mScrimView.setVisibility(View.VISIBLE);
Runnable endActions = () -> {
mMenuView.getChildAt(0).requestAccessibilityFocus();
if (mListener != null) {
mListener.onMenuVisibilityChanged(true /* isShown */);
}
};
if (animated) {
animateTransition(true /* show */, endActions);
} else {
endActions.run();
}
}
/**
* Hide bubble bar expanded view menu
* @param animated if should animate transition
*/
void hideMenu(boolean animated) {
if (mMenuView == null || mScrimView == null) return;
cancelAnimations();
Runnable endActions = () -> {
mMenuView.setVisibility(View.GONE);
mScrimView.setVisibility(View.GONE);
if (mListener != null) {
mListener.onMenuVisibilityChanged(false /* isShown */);
}
};
if (animated) {
animateTransition(false /* show */, endActions);
} else {
endActions.run();
}
}
/**
* Animate show/hide menu transition
* @param show if should show or hide the menu
* @param endActions will be called when animation ends
*/
private void animateTransition(boolean show, Runnable endActions) {
if (mMenuView == null) return;
mMenuAnimator = PhysicsAnimator.getInstance(mMenuView);
mMenuAnimator.setDefaultSpringConfig(mMenuSpringConfig);
mMenuAnimator
.spring(DynamicAnimation.ALPHA, show ? 1f : 0f)
.spring(DynamicAnimation.SCALE_Y, show ? 1f : MENU_INITIAL_SCALE)
.withEndActions(() -> {
mMenuAnimator = null;
endActions.run();
})
.start();
}
/** Cancel running animations */
private void cancelAnimations() {
if (mMenuAnimator != null) {
mMenuAnimator.cancel();
mMenuAnimator = null;
}
}
/** Sets up and inflate menu views */
private void setupMenu() {
// Menu view setup
mMenuView = (BubbleBarMenuView) LayoutInflater.from(mContext).inflate(
R.layout.bubble_bar_menu_view, mRootView, false);
mMenuView.setAlpha(0f);
mMenuView.setPivotY(0f);
mMenuView.setScaleY(MENU_INITIAL_SCALE);
mMenuView.setOnCloseListener(() -> hideMenu(true /* animated */));
if (mBubble != null) {
mMenuView.updateInfo(mBubble);
mMenuView.updateActions(createMenuActions(mBubble));
}
// Scrim view setup
mScrimView = new View(mContext);
mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
mScrimView.setOnClickListener(view -> hideMenu(true /* animated */));
// Attach to root view
mRootView.addView(mScrimView);
mRootView.addView(mMenuView);
}
/**
* Creates menu actions to populate menu view
* @param bubble used to create actions depending on bubble type
*/
private ArrayList<BubbleBarMenuView.MenuAction> createMenuActions(Bubble bubble) {
ArrayList<BubbleBarMenuView.MenuAction> menuActions = new ArrayList<>();
Resources resources = mContext.getResources();
if (bubble.isConversation()) {
// Don't bubble conversation action
menuActions.add(new BubbleBarMenuView.MenuAction(
Icon.createWithResource(mContext, R.drawable.bubble_ic_stop_bubble),
resources.getString(R.string.bubbles_dont_bubble_conversation),
view -> {
hideMenu(true /* animated */);
if (mListener != null) {
mListener.onUnBubbleConversation(bubble);
}
}
));
// Open settings action
Icon appIcon = bubble.getRawAppBadge() != null ? Icon.createWithBitmap(
bubble.getRawAppBadge()) : null;
menuActions.add(new BubbleBarMenuView.MenuAction(
appIcon,
resources.getString(R.string.bubbles_app_settings, bubble.getAppName()),
view -> {
hideMenu(true /* animated */);
if (mListener != null) {
mListener.onOpenAppSettings(bubble);
}
}
));
}
// Dismiss bubble action
menuActions.add(new BubbleBarMenuView.MenuAction(
Icon.createWithResource(resources, R.drawable.ic_remove_no_shadow),
resources.getString(R.string.bubble_dismiss_text),
ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_menu_close),
view -> {
hideMenu(true /* animated */);
if (mListener != null) {
mListener.onDismissBubble(bubble);
}
}
));
return menuActions;
}
/**
* Bubble bar expanded view menu actions listener
*/
interface Listener {
/**
* Called when manage menu is shown/hidden
* If animated will be called when animation ends
*/
void onMenuVisibilityChanged(boolean visible);
/**
* Un-bubbles conversation and removes the bubble from the stack
* This conversation will not be bubbled with new messages
* @see com.android.wm.shell.bubbles.BubbleController
*/
void onUnBubbleConversation(Bubble bubble);
/**
* Launches app notification bubble settings for the bubble with intent created in:
* {@code Bubble.getSettingsIntent}
*/
void onOpenAppSettings(Bubble bubble);
/**
* Dismiss bubble and remove it from the bubble stack
*/
void onDismissBubble(Bubble bubble);
}
}