Merge changes Ie68f7538,I9e8410c6,I79ab3cbe,Iee05e39f into udc-dev
* changes: Create BubbleViewCallback for the bubble bar and use it Use BubbleBarLayerView & BubbleBarExpandedView Introduce BubbleBarLayerView & animation for BubbleBarExpandedView Introduce BubbleBarExpandedView
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
<?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.BubbleBarExpandedView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/bubble_bar_expanded_view">
|
||||
|
||||
</com.android.wm.shell.bubbles.bar.BubbleBarExpandedView>
|
||||
@@ -228,6 +228,8 @@
|
||||
<dimen name="bubble_user_education_stack_padding">16dp</dimen>
|
||||
<!-- Size of the bubble bar (height), should match transient_taskbar_size in Launcher. -->
|
||||
<dimen name="bubblebar_size">72dp</dimen>
|
||||
<!-- The size of the drag handle / menu shown along with a bubble bar expanded view. -->
|
||||
<dimen name="bubblebar_expanded_view_menu_size">16dp</dimen>
|
||||
|
||||
<!-- Bottom and end margin for compat buttons. -->
|
||||
<dimen name="compat_button_margin">24dp</dimen>
|
||||
|
||||
@@ -47,6 +47,8 @@ import android.util.Log;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.logging.InstanceId;
|
||||
import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
|
||||
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
|
||||
import com.android.wm.shell.common.bubbles.BubbleInfo;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
@@ -87,8 +89,18 @@ public class Bubble implements BubbleViewProvider {
|
||||
private String mAppName;
|
||||
private ShortcutInfo mShortcutInfo;
|
||||
private String mMetadataShortcutId;
|
||||
|
||||
/**
|
||||
* If {@link BubbleController#isShowingAsBubbleBar()} is true, the only view that will be
|
||||
* populated will be {@link #mBubbleBarExpandedView}. If it is false, {@link #mIconView}
|
||||
* and {@link #mExpandedView} will be populated.
|
||||
*/
|
||||
@Nullable
|
||||
private BadgedImageView mIconView;
|
||||
@Nullable
|
||||
private BubbleExpandedView mExpandedView;
|
||||
@Nullable
|
||||
private BubbleBarExpandedView mBubbleBarExpandedView;
|
||||
|
||||
private BubbleViewInfoTask mInflationTask;
|
||||
private boolean mInflateSynchronously;
|
||||
@@ -327,12 +339,18 @@ public class Bubble implements BubbleViewProvider {
|
||||
return mIconView;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
@Override
|
||||
public BubbleExpandedView getExpandedView() {
|
||||
return mExpandedView;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BubbleBarExpandedView getBubbleBarExpandedView() {
|
||||
return mBubbleBarExpandedView;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
@@ -364,6 +382,9 @@ public class Bubble implements BubbleViewProvider {
|
||||
mExpandedView.cleanUpExpandedState();
|
||||
mExpandedView = null;
|
||||
}
|
||||
if (mBubbleBarExpandedView != null) {
|
||||
mBubbleBarExpandedView.cleanUpExpandedState();
|
||||
}
|
||||
if (mIntent != null) {
|
||||
mIntent.unregisterCancelListener(mIntentCancelListener);
|
||||
}
|
||||
@@ -410,14 +431,16 @@ public class Bubble implements BubbleViewProvider {
|
||||
* @param callback the callback to notify one the bubble is ready to be displayed.
|
||||
* @param context the context for the bubble.
|
||||
* @param controller the bubble controller.
|
||||
* @param stackView the stackView the bubble is eventually added to.
|
||||
* @param stackView the view the bubble is added to, iff showing as floating.
|
||||
* @param layerView the layer the bubble is added to, iff showing in the bubble bar.
|
||||
* @param iconFactory the icon factory use to create images for the bubble.
|
||||
* @param badgeIconFactory the icon factory to create app badges for the bubble.
|
||||
*/
|
||||
void inflate(BubbleViewInfoTask.Callback callback,
|
||||
Context context,
|
||||
BubbleController controller,
|
||||
BubbleStackView stackView,
|
||||
@Nullable BubbleStackView stackView,
|
||||
@Nullable BubbleBarLayerView layerView,
|
||||
BubbleIconFactory iconFactory,
|
||||
BubbleBadgeIconFactory badgeIconFactory,
|
||||
boolean skipInflation) {
|
||||
@@ -428,6 +451,7 @@ public class Bubble implements BubbleViewProvider {
|
||||
context,
|
||||
controller,
|
||||
stackView,
|
||||
layerView,
|
||||
iconFactory,
|
||||
badgeIconFactory,
|
||||
skipInflation,
|
||||
@@ -445,7 +469,7 @@ public class Bubble implements BubbleViewProvider {
|
||||
}
|
||||
|
||||
boolean isInflated() {
|
||||
return mIconView != null && mExpandedView != null;
|
||||
return (mIconView != null && mExpandedView != null) || mBubbleBarExpandedView != null;
|
||||
}
|
||||
|
||||
void stopInflation() {
|
||||
@@ -459,6 +483,7 @@ public class Bubble implements BubbleViewProvider {
|
||||
if (!isInflated()) {
|
||||
mIconView = info.imageView;
|
||||
mExpandedView = info.expandedView;
|
||||
mBubbleBarExpandedView = info.bubbleBarExpandedView;
|
||||
}
|
||||
|
||||
mShortcutInfo = info.shortcutInfo;
|
||||
@@ -469,7 +494,7 @@ public class Bubble implements BubbleViewProvider {
|
||||
mFlyoutMessage = info.flyoutMessage;
|
||||
|
||||
mBadgeBitmap = info.badgeBitmap;
|
||||
mRawBadgeBitmap = info.mRawBadgeBitmap;
|
||||
mRawBadgeBitmap = info.rawBadgeBitmap;
|
||||
mBubbleBitmap = info.bubbleBitmap;
|
||||
|
||||
mDotColor = info.dotColor;
|
||||
@@ -478,6 +503,9 @@ public class Bubble implements BubbleViewProvider {
|
||||
if (mExpandedView != null) {
|
||||
mExpandedView.update(this /* bubble */);
|
||||
}
|
||||
if (mBubbleBarExpandedView != null) {
|
||||
mBubbleBarExpandedView.update(this /* bubble */);
|
||||
}
|
||||
if (mIconView != null) {
|
||||
mIconView.setRenderedBubble(this /* bubble */);
|
||||
}
|
||||
@@ -607,6 +635,9 @@ public class Bubble implements BubbleViewProvider {
|
||||
*/
|
||||
@Override
|
||||
public int getTaskId() {
|
||||
if (mBubbleBarExpandedView != null) {
|
||||
return mBubbleBarExpandedView.getTaskId();
|
||||
}
|
||||
return mExpandedView != null ? mExpandedView.getTaskId() : mTaskId;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.statusbar.IStatusBarService;
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.WindowManagerShellWrapper;
|
||||
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.common.ExternalInterfaceBinder;
|
||||
import com.android.wm.shell.common.FloatingContentCoordinator;
|
||||
@@ -201,6 +202,7 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
private BubbleLogger mLogger;
|
||||
private BubbleData mBubbleData;
|
||||
@Nullable private BubbleStackView mStackView;
|
||||
@Nullable private BubbleBarLayerView mLayerView;
|
||||
private BubbleIconFactory mBubbleIconFactory;
|
||||
private BubbleBadgeIconFactory mBubbleBadgeIconFactory;
|
||||
private BubblePositioner mBubblePositioner;
|
||||
@@ -260,6 +262,9 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
/** Used to send bubble events to launcher. */
|
||||
private Bubbles.BubbleStateListener mBubbleStateListener;
|
||||
|
||||
/** Used to send updates to the views from {@link #mBubbleDataListener}. */
|
||||
private BubbleViewCallback mBubbleViewCallback;
|
||||
|
||||
public BubbleController(Context context,
|
||||
ShellInit shellInit,
|
||||
ShellCommandHandler shellCommandHandler,
|
||||
@@ -341,6 +346,9 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
}
|
||||
|
||||
protected void onInit() {
|
||||
mBubbleViewCallback = isShowingAsBubbleBar()
|
||||
? mBubbleBarViewCallback
|
||||
: mBubbleStackViewCallback;
|
||||
mBubbleData.setListener(mBubbleDataListener);
|
||||
mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
|
||||
mDataRepository.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
|
||||
@@ -546,7 +554,7 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
}
|
||||
|
||||
private void openBubbleOverflow() {
|
||||
ensureStackViewCreated();
|
||||
ensureBubbleViewsAndWindowCreated();
|
||||
mBubbleData.setShowingOverflow(true);
|
||||
mBubbleData.setSelectedBubble(mBubbleData.getOverflow());
|
||||
mBubbleData.setExpanded(true);
|
||||
@@ -586,7 +594,7 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock);
|
||||
}
|
||||
|
||||
updateStack();
|
||||
updateBubbleViews();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -682,39 +690,59 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
return mBubblePositioner;
|
||||
}
|
||||
|
||||
Bubbles.SysuiProxy getSysuiProxy() {
|
||||
public Bubbles.SysuiProxy getSysuiProxy() {
|
||||
return mSysuiProxy;
|
||||
}
|
||||
|
||||
/**
|
||||
* BubbleStackView is lazily created by this method the first time a Bubble is added. This
|
||||
* method initializes the stack view and adds it to window manager.
|
||||
* The view and window for bubbles is lazily created by this method the first time a Bubble
|
||||
* is added. Depending on the device state, this method will:
|
||||
* - initialize a {@link BubbleStackView} and add it to window manager OR
|
||||
* - initialize a {@link com.android.wm.shell.bubbles.bar.BubbleBarLayerView} and adds
|
||||
* it to window manager.
|
||||
*/
|
||||
private void ensureStackViewCreated() {
|
||||
if (mStackView == null) {
|
||||
mStackView = new BubbleStackView(
|
||||
mContext, this, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator,
|
||||
mMainExecutor);
|
||||
mStackView.onOrientationChanged();
|
||||
if (mExpandListener != null) {
|
||||
mStackView.setExpandListener(mExpandListener);
|
||||
private void ensureBubbleViewsAndWindowCreated() {
|
||||
mBubblePositioner.setShowingInBubbleBar(isShowingAsBubbleBar());
|
||||
if (isShowingAsBubbleBar()) {
|
||||
// When we're showing in launcher / bubble bar is enabled, we don't have bubble stack
|
||||
// view, instead we just show the expanded bubble view as necessary. We still need a
|
||||
// window to show this in, but we use a separate code path.
|
||||
// TODO(b/273312602): consider foldables where we do need a stack view when folded
|
||||
if (mLayerView == null) {
|
||||
mLayerView = new BubbleBarLayerView(mContext, this);
|
||||
}
|
||||
} else {
|
||||
if (mStackView == null) {
|
||||
mStackView = new BubbleStackView(
|
||||
mContext, this, mBubbleData, mSurfaceSynchronizer,
|
||||
mFloatingContentCoordinator,
|
||||
mMainExecutor);
|
||||
mStackView.onOrientationChanged();
|
||||
if (mExpandListener != null) {
|
||||
mStackView.setExpandListener(mExpandListener);
|
||||
}
|
||||
mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
|
||||
}
|
||||
mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
|
||||
}
|
||||
|
||||
addToWindowManagerMaybe();
|
||||
}
|
||||
|
||||
/** Adds the BubbleStackView to the WindowManager if it's not already there. */
|
||||
/** Adds the appropriate view to WindowManager if it's not already there. */
|
||||
private void addToWindowManagerMaybe() {
|
||||
// If the stack is null, or already added, don't add it.
|
||||
if (mStackView == null || mAddedToWindowManager) {
|
||||
// If already added, don't add it.
|
||||
if (mAddedToWindowManager) {
|
||||
return;
|
||||
}
|
||||
// If the appropriate view is null, don't add it.
|
||||
if (isShowingAsBubbleBar() && mLayerView == null) {
|
||||
return;
|
||||
} else if (!isShowingAsBubbleBar() && mStackView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mWmLayoutParams = new WindowManager.LayoutParams(
|
||||
// Fill the screen so we can use translation animations to position the bubble
|
||||
// stack. We'll use touchable regions to ignore touches that are not on the bubbles
|
||||
// views. We'll use touchable regions to ignore touches that are not on the bubbles
|
||||
// themselves.
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
@@ -737,17 +765,30 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
mAddedToWindowManager = true;
|
||||
registerBroadcastReceiver();
|
||||
mBubbleData.getOverflow().initialize(this);
|
||||
mWindowManager.addView(mStackView, mWmLayoutParams);
|
||||
mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
|
||||
if (!windowInsets.equals(mWindowInsets)) {
|
||||
mWindowInsets = windowInsets;
|
||||
mBubblePositioner.update();
|
||||
mStackView.onDisplaySizeChanged();
|
||||
}
|
||||
return windowInsets;
|
||||
});
|
||||
// (TODO: b/273314541) some duplication in the inset listener
|
||||
if (isShowingAsBubbleBar()) {
|
||||
mWindowManager.addView(mLayerView, mWmLayoutParams);
|
||||
mLayerView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
|
||||
if (!windowInsets.equals(mWindowInsets)) {
|
||||
mWindowInsets = windowInsets;
|
||||
mBubblePositioner.update();
|
||||
mLayerView.onDisplaySizeChanged();
|
||||
}
|
||||
return windowInsets;
|
||||
});
|
||||
} else {
|
||||
mWindowManager.addView(mStackView, mWmLayoutParams);
|
||||
mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
|
||||
if (!windowInsets.equals(mWindowInsets)) {
|
||||
mWindowInsets = windowInsets;
|
||||
mBubblePositioner.update();
|
||||
mStackView.onDisplaySizeChanged();
|
||||
}
|
||||
return windowInsets;
|
||||
});
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
// This means the stack has already been added. This shouldn't happen...
|
||||
// This means the view has already been added. This shouldn't happen...
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
@@ -770,7 +811,7 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes the BubbleStackView from the WindowManager if it's there. */
|
||||
/** Removes any bubble views from the WindowManager that exist. */
|
||||
private void removeFromWindowManagerMaybe() {
|
||||
if (!mAddedToWindowManager) {
|
||||
return;
|
||||
@@ -791,8 +832,10 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
if (mStackView != null) {
|
||||
mWindowManager.removeView(mStackView);
|
||||
mBubbleData.getOverflow().cleanUpExpandedState();
|
||||
} else {
|
||||
Log.w(TAG, "StackView added to WindowManager, but was null when removing!");
|
||||
}
|
||||
if (mLayerView != null) {
|
||||
mWindowManager.removeView(mLayerView);
|
||||
mBubbleData.getOverflow().cleanUpExpandedState();
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// This means the stack has already been removed - it shouldn't happen, but ignore if it
|
||||
@@ -887,12 +930,22 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
|
||||
// Reload each bubble
|
||||
for (Bubble b : mBubbleData.getBubbles()) {
|
||||
b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory,
|
||||
b.inflate(null /* callback */,
|
||||
mContext,
|
||||
this,
|
||||
mStackView,
|
||||
mLayerView,
|
||||
mBubbleIconFactory,
|
||||
mBubbleBadgeIconFactory,
|
||||
false /* skipInflation */);
|
||||
}
|
||||
for (Bubble b : mBubbleData.getOverflowBubbles()) {
|
||||
b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory,
|
||||
b.inflate(null /* callback */,
|
||||
mContext,
|
||||
this,
|
||||
mStackView,
|
||||
mLayerView,
|
||||
mBubbleIconFactory,
|
||||
mBubbleBadgeIconFactory,
|
||||
false /* skipInflation */);
|
||||
}
|
||||
@@ -959,7 +1012,7 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public boolean hasBubbles() {
|
||||
if (mStackView == null) {
|
||||
if (mStackView == null && mLayerView == null) {
|
||||
return false;
|
||||
}
|
||||
return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow();
|
||||
@@ -1166,7 +1219,12 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
}
|
||||
bubble.inflate(
|
||||
(b) -> mBubbleData.overflowBubble(Bubbles.DISMISS_RELOAD_FROM_DISK, bubble),
|
||||
mContext, this, mStackView, mBubbleIconFactory, mBubbleBadgeIconFactory,
|
||||
mContext,
|
||||
this,
|
||||
mStackView,
|
||||
mLayerView,
|
||||
mBubbleIconFactory,
|
||||
mBubbleBadgeIconFactory,
|
||||
true /* skipInflation */);
|
||||
});
|
||||
return null;
|
||||
@@ -1238,10 +1296,11 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
@VisibleForTesting
|
||||
public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
|
||||
// Lazy init stack view when a bubble is created
|
||||
ensureStackViewCreated();
|
||||
ensureBubbleViewsAndWindowCreated();
|
||||
bubble.setInflateSynchronously(mInflateSynchronously);
|
||||
bubble.inflate(b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade),
|
||||
mContext, this, mStackView, mBubbleIconFactory, mBubbleBadgeIconFactory,
|
||||
mContext, this, mStackView, mLayerView,
|
||||
mBubbleIconFactory, mBubbleBadgeIconFactory,
|
||||
false /* skipInflation */);
|
||||
}
|
||||
|
||||
@@ -1406,7 +1465,8 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
});
|
||||
}
|
||||
|
||||
private final BubbleViewCallback mBubbleViewCallback = new BubbleViewCallback() {
|
||||
/** When bubbles are floating, this will be used to notify the floating views. */
|
||||
private final BubbleViewCallback mBubbleStackViewCallback = new BubbleViewCallback() {
|
||||
@Override
|
||||
public void removeBubble(Bubble removedBubble) {
|
||||
if (mStackView != null) {
|
||||
@@ -1458,6 +1518,62 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
}
|
||||
};
|
||||
|
||||
/** When bubbles are in the bubble bar, this will be used to notify bubble bar views. */
|
||||
private final BubbleViewCallback mBubbleBarViewCallback = new BubbleViewCallback() {
|
||||
@Override
|
||||
public void removeBubble(Bubble removedBubble) {
|
||||
if (mLayerView != null) {
|
||||
// TODO: need to check if there's something that needs to happen here, e.g. if
|
||||
// the currently selected & expanded bubble is removed?
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addBubble(Bubble addedBubble) {
|
||||
// Nothing to do for adds, these are handled by launcher / in the bubble bar.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBubble(Bubble updatedBubble) {
|
||||
// Nothing to do for updates, these are handled by launcher / in the bubble bar.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer) {
|
||||
// Nothing to do for order changes, these are handled by launcher / in the bubble bar.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void suppressionChanged(Bubble bubble, boolean isSuppressed) {
|
||||
if (mLayerView != null) {
|
||||
// TODO (b/273316505) handle suppression changes, although might not need to
|
||||
// to do anything on the layerview side for this...
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expansionChanged(boolean isExpanded) {
|
||||
if (mLayerView != null) {
|
||||
if (!isExpanded) {
|
||||
mLayerView.collapse();
|
||||
} else {
|
||||
BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble();
|
||||
if (selectedBubble != null) {
|
||||
mLayerView.showExpandedView(selectedBubble);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectionChanged(BubbleViewProvider selectedBubble) {
|
||||
// Only need to update the layer view if we're currently expanded for selection changes.
|
||||
if (mLayerView != null && isStackExpanded()) {
|
||||
mLayerView.showExpandedView(selectedBubble);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() {
|
||||
|
||||
@@ -1475,7 +1591,7 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
+ " unsuppressed=" + (update.unsuppressedBubble != null));
|
||||
}
|
||||
|
||||
ensureStackViewCreated();
|
||||
ensureBubbleViewsAndWindowCreated();
|
||||
|
||||
// Lazy load overflow bubbles from disk
|
||||
loadOverflowBubblesFromDisk();
|
||||
@@ -1570,7 +1686,7 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
}
|
||||
|
||||
mSysuiProxy.notifyInvalidateNotifications("BubbleData.Listener.applyUpdate");
|
||||
updateStack();
|
||||
updateBubbleViews();
|
||||
|
||||
// Update the cached state for queries from SysUI
|
||||
mImpl.mCachedState.update(update);
|
||||
@@ -1650,28 +1766,42 @@ public class BubbleController implements ConfigurationChangeListener,
|
||||
|
||||
/**
|
||||
* Updates the visibility of the bubbles based on current state.
|
||||
* Does not un-bubble, just hides or un-hides.
|
||||
* Updates stack description for TalkBack focus.
|
||||
* Updates bubbles' icon views clickable states
|
||||
* Does not un-bubble, just hides or un-hides the views themselves.
|
||||
*
|
||||
* Updates view description for TalkBack focus.
|
||||
* Updates bubbles' icon views clickable states (when floating).
|
||||
*/
|
||||
public void updateStack() {
|
||||
if (mStackView == null) {
|
||||
public void updateBubbleViews() {
|
||||
if (mStackView == null && mLayerView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mIsStatusBarShade) {
|
||||
// Bubbles don't appear over the locked shade.
|
||||
mStackView.setVisibility(INVISIBLE);
|
||||
// Bubbles don't appear when the device is locked.
|
||||
if (mStackView != null) {
|
||||
mStackView.setVisibility(INVISIBLE);
|
||||
}
|
||||
if (mLayerView != null) {
|
||||
mLayerView.setVisibility(INVISIBLE);
|
||||
}
|
||||
} else if (hasBubbles()) {
|
||||
// If we're unlocked, show the stack if we have bubbles. If we don't have bubbles, the
|
||||
// stack will be set to INVISIBLE in onAllBubblesAnimatedOut after the bubbles animate
|
||||
// out.
|
||||
mStackView.setVisibility(VISIBLE);
|
||||
if (mStackView != null) {
|
||||
mStackView.setVisibility(VISIBLE);
|
||||
}
|
||||
if (mLayerView != null && isStackExpanded()) {
|
||||
mLayerView.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
mStackView.updateContentDescription();
|
||||
|
||||
mStackView.updateBubblesAcessibillityStates();
|
||||
if (mStackView != null) {
|
||||
mStackView.updateContentDescription();
|
||||
mStackView.updateBubblesAcessibillityStates();
|
||||
} else if (mLayerView != null) {
|
||||
// TODO(b/273313561): handle a11y for BubbleBarLayerView
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
||||
@@ -29,6 +29,7 @@ import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import com.android.wm.shell.R
|
||||
import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView
|
||||
|
||||
class BubbleOverflow(
|
||||
private val context: Context,
|
||||
@@ -136,6 +137,10 @@ class BubbleOverflow(
|
||||
return expandedView
|
||||
}
|
||||
|
||||
override fun getBubbleBarExpandedView(): BubbleBarExpandedView? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getDotColor(): Int {
|
||||
return dotColor
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
|
||||
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
@@ -38,12 +39,12 @@ import android.util.Log;
|
||||
import android.util.PathParser;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.graphics.ColorUtils;
|
||||
import com.android.launcher3.icons.BitmapInfo;
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
|
||||
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
@@ -70,6 +71,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
|
||||
private WeakReference<Context> mContext;
|
||||
private WeakReference<BubbleController> mController;
|
||||
private WeakReference<BubbleStackView> mStackView;
|
||||
private WeakReference<BubbleBarLayerView> mLayerView;
|
||||
private BubbleIconFactory mIconFactory;
|
||||
private BubbleBadgeIconFactory mBadgeIconFactory;
|
||||
private boolean mSkipInflation;
|
||||
@@ -83,7 +85,8 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
|
||||
BubbleViewInfoTask(Bubble b,
|
||||
Context context,
|
||||
BubbleController controller,
|
||||
BubbleStackView stackView,
|
||||
@Nullable BubbleStackView stackView,
|
||||
@Nullable BubbleBarLayerView layerView,
|
||||
BubbleIconFactory factory,
|
||||
BubbleBadgeIconFactory badgeFactory,
|
||||
boolean skipInflation,
|
||||
@@ -93,6 +96,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
|
||||
mContext = new WeakReference<>(context);
|
||||
mController = new WeakReference<>(controller);
|
||||
mStackView = new WeakReference<>(stackView);
|
||||
mLayerView = new WeakReference<>(layerView);
|
||||
mIconFactory = factory;
|
||||
mBadgeIconFactory = badgeFactory;
|
||||
mSkipInflation = skipInflation;
|
||||
@@ -102,8 +106,13 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
|
||||
|
||||
@Override
|
||||
protected BubbleViewInfo doInBackground(Void... voids) {
|
||||
return BubbleViewInfo.populate(mContext.get(), mController.get(), mStackView.get(),
|
||||
mIconFactory, mBadgeIconFactory, mBubble, mSkipInflation);
|
||||
if (mController.get().isShowingAsBubbleBar()) {
|
||||
return BubbleViewInfo.populateForBubbleBar(mContext.get(), mController.get(),
|
||||
mLayerView.get(), mBadgeIconFactory, mBubble, mSkipInflation);
|
||||
} else {
|
||||
return BubbleViewInfo.populate(mContext.get(), mController.get(), mStackView.get(),
|
||||
mIconFactory, mBadgeIconFactory, mBubble, mSkipInflation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -124,16 +133,70 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static class BubbleViewInfo {
|
||||
BadgedImageView imageView;
|
||||
BubbleExpandedView expandedView;
|
||||
// TODO(b/273312602): for foldables it might make sense to populate all of the views
|
||||
|
||||
// Always populated
|
||||
ShortcutInfo shortcutInfo;
|
||||
String appName;
|
||||
Bitmap bubbleBitmap;
|
||||
Bitmap badgeBitmap;
|
||||
Bitmap mRawBadgeBitmap;
|
||||
Bitmap rawBadgeBitmap;
|
||||
|
||||
// Only populated when showing in taskbar
|
||||
BubbleBarExpandedView bubbleBarExpandedView;
|
||||
|
||||
// These are only populated when not showing in taskbar
|
||||
BadgedImageView imageView;
|
||||
BubbleExpandedView expandedView;
|
||||
int dotColor;
|
||||
Path dotPath;
|
||||
Bubble.FlyoutMessage flyoutMessage;
|
||||
Bitmap bubbleBitmap;
|
||||
Bitmap badgeBitmap;
|
||||
|
||||
@Nullable
|
||||
public static BubbleViewInfo populateForBubbleBar(Context c, BubbleController controller,
|
||||
BubbleBarLayerView layerView, BubbleBadgeIconFactory badgeIconFactory, Bubble b,
|
||||
boolean skipInflation) {
|
||||
BubbleViewInfo info = new BubbleViewInfo();
|
||||
|
||||
if (!skipInflation && !b.isInflated()) {
|
||||
LayoutInflater inflater = LayoutInflater.from(c);
|
||||
info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate(
|
||||
R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */);
|
||||
info.bubbleBarExpandedView.initialize(controller);
|
||||
}
|
||||
|
||||
if (b.getShortcutInfo() != null) {
|
||||
info.shortcutInfo = b.getShortcutInfo();
|
||||
}
|
||||
|
||||
// App name & app icon
|
||||
PackageManager pm = BubbleController.getPackageManagerForUser(c,
|
||||
b.getUser().getIdentifier());
|
||||
ApplicationInfo appInfo;
|
||||
Drawable badgedIcon;
|
||||
Drawable appIcon;
|
||||
try {
|
||||
appInfo = pm.getApplicationInfo(
|
||||
b.getPackageName(),
|
||||
PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
| PackageManager.MATCH_DISABLED_COMPONENTS
|
||||
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE
|
||||
| PackageManager.MATCH_DIRECT_BOOT_AWARE);
|
||||
if (appInfo != null) {
|
||||
info.appName = String.valueOf(pm.getApplicationLabel(appInfo));
|
||||
}
|
||||
appIcon = pm.getApplicationIcon(b.getPackageName());
|
||||
badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser());
|
||||
} catch (PackageManager.NameNotFoundException exception) {
|
||||
// If we can't find package... don't think we should show the bubble.
|
||||
Log.w(TAG, "Unable to find package: " + b.getPackageName());
|
||||
return null;
|
||||
}
|
||||
|
||||
info.rawBadgeBitmap = badgeIconFactory.getBadgeBitmap(badgedIcon, false).icon;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Nullable
|
||||
@@ -195,7 +258,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
|
||||
b.isImportantConversation());
|
||||
info.badgeBitmap = badgeBitmapInfo.icon;
|
||||
// Raw badge bitmap never includes the important conversation ring
|
||||
info.mRawBadgeBitmap = b.isImportantConversation()
|
||||
info.rawBadgeBitmap = b.isImportantConversation()
|
||||
? badgeIconFactory.getBadgeBitmap(badgedIcon, false).icon
|
||||
: badgeBitmapInfo.icon;
|
||||
|
||||
|
||||
@@ -22,18 +22,40 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
|
||||
|
||||
/**
|
||||
* Interface to represent actual Bubbles and UI elements that act like bubbles, like BubbleOverflow.
|
||||
*/
|
||||
public interface BubbleViewProvider {
|
||||
@Nullable BubbleExpandedView getExpandedView();
|
||||
|
||||
/**
|
||||
* Returns the icon view used for a bubble (the click target when collapsed). This is populated
|
||||
* when bubbles are floating, i.e. when {@link BubbleController#isShowingAsBubbleBar()} is
|
||||
* false.
|
||||
*/
|
||||
@Nullable
|
||||
View getIconView();
|
||||
|
||||
/**
|
||||
* Returns the expanded view used for a bubble. This is populated when bubbles are floating,
|
||||
* i.e. when {@link BubbleController#isShowingAsBubbleBar()} is false.
|
||||
*/
|
||||
@Nullable
|
||||
BubbleExpandedView getExpandedView();
|
||||
|
||||
/**
|
||||
* Returns the expanded view used for a bubble being show in the bubble bar. This is populated
|
||||
* when {@link BubbleController#isShowingAsBubbleBar()} is true.
|
||||
*/
|
||||
@Nullable
|
||||
BubbleBarExpandedView getBubbleBarExpandedView();
|
||||
|
||||
/**
|
||||
* Sets whether the contents of the bubble's TaskView should be visible.
|
||||
*/
|
||||
void setTaskViewVisibility(boolean visible);
|
||||
|
||||
@Nullable View getIconView();
|
||||
|
||||
String getKey();
|
||||
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* 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 static android.view.View.VISIBLE;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.util.Log;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.android.wm.shell.animation.Interpolators;
|
||||
import com.android.wm.shell.animation.PhysicsAnimator;
|
||||
import com.android.wm.shell.bubbles.BubblePositioner;
|
||||
import com.android.wm.shell.bubbles.BubbleViewProvider;
|
||||
import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix;
|
||||
|
||||
/**
|
||||
* Helper class to animate a {@link BubbleBarExpandedView} on a bubble.
|
||||
*/
|
||||
public class BubbleBarAnimationHelper {
|
||||
|
||||
private static final String TAG = BubbleBarAnimationHelper.class.getSimpleName();
|
||||
|
||||
private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f;
|
||||
private static final float EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT = .75f;
|
||||
private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150;
|
||||
|
||||
/** Spring config for the expanded view scale-in animation. */
|
||||
private final PhysicsAnimator.SpringConfig mScaleInSpringConfig =
|
||||
new PhysicsAnimator.SpringConfig(300f, 0.9f);
|
||||
|
||||
/** Spring config for the expanded view scale-out animation. */
|
||||
private final PhysicsAnimator.SpringConfig mScaleOutSpringConfig =
|
||||
new PhysicsAnimator.SpringConfig(900f, 1f);
|
||||
|
||||
/** Matrix used to scale the expanded view container with a given pivot point. */
|
||||
private final AnimatableScaleMatrix mExpandedViewContainerMatrix = new AnimatableScaleMatrix();
|
||||
|
||||
/** Animator for animating the expanded view's alpha (including the TaskView inside it). */
|
||||
private final ValueAnimator mExpandedViewAlphaAnimator = ValueAnimator.ofFloat(0f, 1f);
|
||||
|
||||
private final Context mContext;
|
||||
private final BubbleBarLayerView mLayerView;
|
||||
private final BubblePositioner mPositioner;
|
||||
|
||||
private BubbleViewProvider mExpandedBubble;
|
||||
private boolean mIsExpanded = false;
|
||||
|
||||
public BubbleBarAnimationHelper(Context context,
|
||||
BubbleBarLayerView bubbleBarLayerView,
|
||||
BubblePositioner positioner) {
|
||||
mContext = context;
|
||||
mLayerView = bubbleBarLayerView;
|
||||
mPositioner = positioner;
|
||||
|
||||
mExpandedViewAlphaAnimator.setDuration(EXPANDED_VIEW_ALPHA_ANIMATION_DURATION);
|
||||
mExpandedViewAlphaAnimator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
|
||||
mExpandedViewAlphaAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
if (mExpandedBubble != null && mExpandedBubble.getBubbleBarExpandedView() != null) {
|
||||
// We need to be Z ordered on top in order for alpha animations to work.
|
||||
mExpandedBubble.getBubbleBarExpandedView().setSurfaceZOrderedOnTop(true);
|
||||
mExpandedBubble.getBubbleBarExpandedView().setAnimating(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (mExpandedBubble != null && mExpandedBubble.getBubbleBarExpandedView() != null) {
|
||||
// The surface needs to be Z ordered on top for alpha values to work on the
|
||||
// TaskView, and if we're temporarily hidden, we are still on the screen
|
||||
// with alpha = 0f until we animate back. Stay Z ordered on top so the alpha
|
||||
// = 0f remains in effect.
|
||||
if (mIsExpanded) {
|
||||
mExpandedBubble.getBubbleBarExpandedView().setSurfaceZOrderedOnTop(false);
|
||||
}
|
||||
|
||||
mExpandedBubble.getBubbleBarExpandedView().setContentVisibility(mIsExpanded);
|
||||
mExpandedBubble.getBubbleBarExpandedView().setAnimating(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> {
|
||||
if (mExpandedBubble != null && mExpandedBubble.getBubbleBarExpandedView() != null) {
|
||||
float alpha = (float) valueAnimator.getAnimatedValue();
|
||||
mExpandedBubble.getBubbleBarExpandedView().setTaskViewAlpha(alpha);
|
||||
mExpandedBubble.getBubbleBarExpandedView().setAlpha(alpha);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates the provided bubble's expanded view to the expanded state.
|
||||
*/
|
||||
public void animateExpansion(BubbleViewProvider expandedBubble) {
|
||||
mExpandedBubble = expandedBubble;
|
||||
if (mExpandedBubble == null) {
|
||||
return;
|
||||
}
|
||||
BubbleBarExpandedView bev = mExpandedBubble.getBubbleBarExpandedView();
|
||||
if (bev == null) {
|
||||
return;
|
||||
}
|
||||
mIsExpanded = true;
|
||||
|
||||
mExpandedViewContainerMatrix.setScaleX(0f);
|
||||
mExpandedViewContainerMatrix.setScaleY(0f);
|
||||
|
||||
updateExpandedView();
|
||||
bev.setAnimating(true);
|
||||
bev.setContentVisibility(false);
|
||||
bev.setAlpha(0f);
|
||||
bev.setTaskViewAlpha(0f);
|
||||
bev.setVisibility(VISIBLE);
|
||||
|
||||
// Set the pivot point for the scale, so the view animates out from the bubble bar.
|
||||
PointF bubbleBarPosition = mPositioner.getBubbleBarPosition();
|
||||
mExpandedViewContainerMatrix.setScale(
|
||||
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
|
||||
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
|
||||
bubbleBarPosition.x,
|
||||
bubbleBarPosition.y);
|
||||
|
||||
bev.setAnimationMatrix(mExpandedViewContainerMatrix);
|
||||
|
||||
mExpandedViewAlphaAnimator.start();
|
||||
|
||||
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
|
||||
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
|
||||
.spring(AnimatableScaleMatrix.SCALE_X,
|
||||
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
|
||||
mScaleInSpringConfig)
|
||||
.spring(AnimatableScaleMatrix.SCALE_Y,
|
||||
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
|
||||
mScaleInSpringConfig)
|
||||
.addUpdateListener((target, values) -> {
|
||||
mExpandedBubble.getBubbleBarExpandedView().setAnimationMatrix(
|
||||
mExpandedViewContainerMatrix);
|
||||
})
|
||||
.withEndActions(() -> {
|
||||
bev.setAnimationMatrix(null);
|
||||
updateExpandedView();
|
||||
bev.setSurfaceZOrderedOnTop(false);
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapses the currently expanded bubble.
|
||||
*
|
||||
* @param endRunnable a runnable to run at the end of the animation.
|
||||
*/
|
||||
public void animateCollapse(Runnable endRunnable) {
|
||||
mIsExpanded = false;
|
||||
if (mExpandedBubble == null || mExpandedBubble.getBubbleBarExpandedView() == null) {
|
||||
Log.w(TAG, "Trying to animate collapse without a bubble");
|
||||
return;
|
||||
}
|
||||
|
||||
mExpandedViewContainerMatrix.setScaleX(1f);
|
||||
mExpandedViewContainerMatrix.setScaleY(1f);
|
||||
|
||||
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
|
||||
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
|
||||
.spring(AnimatableScaleMatrix.SCALE_X,
|
||||
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(
|
||||
EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT),
|
||||
mScaleOutSpringConfig)
|
||||
.spring(AnimatableScaleMatrix.SCALE_Y,
|
||||
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(
|
||||
EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT),
|
||||
mScaleOutSpringConfig)
|
||||
.addUpdateListener((target, values) -> {
|
||||
if (mExpandedBubble != null
|
||||
&& mExpandedBubble.getBubbleBarExpandedView() != null) {
|
||||
mExpandedBubble.getBubbleBarExpandedView().setAnimationMatrix(
|
||||
mExpandedViewContainerMatrix);
|
||||
}
|
||||
})
|
||||
.withEndActions(() -> {
|
||||
if (mExpandedBubble != null
|
||||
&& mExpandedBubble.getBubbleBarExpandedView() != null) {
|
||||
mExpandedBubble.getBubbleBarExpandedView().setAnimationMatrix(null);
|
||||
}
|
||||
if (endRunnable != null) {
|
||||
endRunnable.run();
|
||||
}
|
||||
})
|
||||
.start();
|
||||
mExpandedViewAlphaAnimator.reverse();
|
||||
}
|
||||
|
||||
private void updateExpandedView() {
|
||||
if (mExpandedBubble == null || mExpandedBubble.getBubbleBarExpandedView() == null) {
|
||||
Log.w(TAG, "Trying to update the expanded view without a bubble");
|
||||
return;
|
||||
}
|
||||
BubbleBarExpandedView bbev = mExpandedBubble.getBubbleBarExpandedView();
|
||||
|
||||
final int padding = mPositioner.getBubbleBarExpandedViewPadding();
|
||||
final int width = mPositioner.getExpandedViewWidthForBubbleBar();
|
||||
final int height = mPositioner.getExpandedViewHeightForBubbleBar();
|
||||
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) bbev.getLayoutParams();
|
||||
lp.width = width;
|
||||
lp.height = height;
|
||||
bbev.setLayoutParams(lp);
|
||||
if (mLayerView.isOnLeft()) {
|
||||
bbev.setX(mPositioner.getInsets().left + padding);
|
||||
} else {
|
||||
bbev.setX(mPositioner.getAvailableRect().width() - width - padding);
|
||||
}
|
||||
bbev.setY(mPositioner.getInsets().top + padding);
|
||||
bbev.updateLocation();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* 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.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Outline;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.android.internal.policy.ScreenDecorationsUtils;
|
||||
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.taskview.TaskView;
|
||||
|
||||
/**
|
||||
* Expanded view of a bubble when it's part of the bubble bar.
|
||||
*
|
||||
* {@link BubbleController#isShowingAsBubbleBar()}
|
||||
*/
|
||||
public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskViewHelper.Listener {
|
||||
|
||||
private static final String TAG = BubbleBarExpandedView.class.getSimpleName();
|
||||
private static final int INVALID_TASK_ID = -1;
|
||||
|
||||
private BubbleController mController;
|
||||
private BubbleTaskViewHelper mBubbleTaskViewHelper;
|
||||
|
||||
private HandleView mMenuView;
|
||||
private TaskView mTaskView;
|
||||
|
||||
private int mMenuHeight;
|
||||
private int mBackgroundColor;
|
||||
private float mCornerRadius = 0f;
|
||||
|
||||
/**
|
||||
* Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If
|
||||
* {@link #mIsAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha
|
||||
* value until the animation ends.
|
||||
*/
|
||||
private boolean mIsContentVisible = false;
|
||||
private boolean mIsAnimating;
|
||||
|
||||
public BubbleBarExpandedView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public BubbleBarExpandedView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
Context context = getContext();
|
||||
setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation));
|
||||
mMenuHeight = context.getResources().getDimensionPixelSize(
|
||||
R.dimen.bubblebar_expanded_view_menu_size);
|
||||
mMenuView = new HandleView(context);
|
||||
addView(mMenuView);
|
||||
|
||||
applyThemeAttrs();
|
||||
setClipToOutline(true);
|
||||
setOutlineProvider(new ViewOutlineProvider() {
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline) {
|
||||
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Set the BubbleController on the view, must be called before doing anything else. */
|
||||
public void initialize(BubbleController controller) {
|
||||
mController = controller;
|
||||
mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, mController,
|
||||
/* listener= */ this,
|
||||
/* viewParent= */ this);
|
||||
mTaskView = mBubbleTaskViewHelper.getTaskView();
|
||||
addView(mTaskView);
|
||||
mTaskView.setEnableSurfaceClipping(true);
|
||||
mTaskView.setCornerRadius(mCornerRadius);
|
||||
}
|
||||
|
||||
// TODO (b/275087636): call this when theme/config changes
|
||||
void applyThemeAttrs() {
|
||||
boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
|
||||
mContext.getResources());
|
||||
final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
|
||||
android.R.attr.dialogCornerRadius,
|
||||
android.R.attr.colorBackgroundFloating});
|
||||
mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0;
|
||||
mCornerRadius = mCornerRadius / 2f;
|
||||
mBackgroundColor = ta.getColor(1, Color.WHITE);
|
||||
|
||||
ta.recycle();
|
||||
|
||||
mMenuView.setCornerRadius(mCornerRadius);
|
||||
mMenuHeight = getResources().getDimensionPixelSize(
|
||||
R.dimen.bubblebar_expanded_view_menu_size);
|
||||
|
||||
if (mTaskView != null) {
|
||||
mTaskView.setCornerRadius(mCornerRadius);
|
||||
mTaskView.setElevation(150);
|
||||
updateMenuColor();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
|
||||
// Add corner radius here so that the menu extends behind the rounded corners of TaskView.
|
||||
int menuViewHeight = Math.min((int) (mMenuHeight + mCornerRadius), height);
|
||||
measureChild(mMenuView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight,
|
||||
MeasureSpec.getMode(heightMeasureSpec)));
|
||||
|
||||
if (mTaskView != null) {
|
||||
int taskViewHeight = height - menuViewHeight;
|
||||
measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(taskViewHeight,
|
||||
MeasureSpec.getMode(heightMeasureSpec)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
// Drag handle above
|
||||
final int dragHandleBottom = t + mMenuView.getMeasuredHeight();
|
||||
mMenuView.layout(l, t, r, dragHandleBottom);
|
||||
if (mTaskView != null) {
|
||||
// Subtract radius so that the menu extends behind the rounded corners of TaskView.
|
||||
mTaskView.layout(l, (int) (dragHandleBottom - mCornerRadius), r,
|
||||
dragHandleBottom + mTaskView.getMeasuredHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskCreated() {
|
||||
setContentVisibility(true);
|
||||
updateMenuColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentVisibilityChanged(boolean visible) {
|
||||
setContentVisibility(visible);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
mController.collapseStack();
|
||||
}
|
||||
|
||||
/** Cleans up task view, should be called when the bubble is no longer active. */
|
||||
public void cleanUpExpandedState() {
|
||||
if (mBubbleTaskViewHelper != null) {
|
||||
if (mTaskView != null) {
|
||||
removeView(mTaskView);
|
||||
}
|
||||
mBubbleTaskViewHelper.cleanUpTaskView();
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the bubble shown in this task view. */
|
||||
public void update(Bubble bubble) {
|
||||
mBubbleTaskViewHelper.update(bubble);
|
||||
}
|
||||
|
||||
/** The task id of the activity shown in the task view, if it exists. */
|
||||
public int getTaskId() {
|
||||
return mBubbleTaskViewHelper != null ? mBubbleTaskViewHelper.getTaskId() : INVALID_TASK_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when the location or size of the view has changed to update TaskView.
|
||||
*/
|
||||
public void updateLocation() {
|
||||
if (mTaskView == null) return;
|
||||
mTaskView.onLocationChanged();
|
||||
}
|
||||
|
||||
/** Sets the alpha of the task view. */
|
||||
public void setContentVisibility(boolean visible) {
|
||||
mIsContentVisible = visible;
|
||||
|
||||
if (mTaskView == null) return;
|
||||
|
||||
if (!mIsAnimating) {
|
||||
mTaskView.setAlpha(visible ? 1f : 0f);
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the menu bar to be the status bar color specified by the app. */
|
||||
private void updateMenuColor() {
|
||||
if (mTaskView == null) return;
|
||||
ActivityManager.RunningTaskInfo info = mTaskView.getTaskInfo();
|
||||
final int taskBgColor = info.taskDescription.getStatusBarColor();
|
||||
final int color = Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb();
|
||||
if (color != -1) {
|
||||
mMenuView.setBackgroundColor(color);
|
||||
} else {
|
||||
mMenuView.setBackgroundColor(mBackgroundColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the alpha of both this view and the task view.
|
||||
*/
|
||||
public void setTaskViewAlpha(float alpha) {
|
||||
if (mTaskView != null) {
|
||||
mTaskView.setAlpha(alpha);
|
||||
}
|
||||
setAlpha(alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the surface displaying app content should sit on top. This is useful for
|
||||
* ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble
|
||||
* being dragged out, the manage menu) this is set to false, otherwise it should be true.
|
||||
*/
|
||||
public void setSurfaceZOrderedOnTop(boolean onTop) {
|
||||
if (mTaskView == null) {
|
||||
return;
|
||||
}
|
||||
mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the view is animating, in this case we won't change the content visibility
|
||||
* until the animation is done.
|
||||
*/
|
||||
public void setAnimating(boolean animating) {
|
||||
mIsAnimating = animating;
|
||||
// If we're done animating, apply the correct visibility.
|
||||
if (!animating) {
|
||||
setContentVisibility(mIsContentVisible);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* 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 static com.android.wm.shell.animation.Interpolators.ALPHA_IN;
|
||||
import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.android.wm.shell.bubbles.BubbleController;
|
||||
import com.android.wm.shell.bubbles.BubblePositioner;
|
||||
import com.android.wm.shell.bubbles.BubbleViewProvider;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* launcher in the bubble bar. This view does not show a stack of bubbles that can be moved around
|
||||
* on screen and instead shows & animates the expanded bubble for the bubble bar.
|
||||
*/
|
||||
public class BubbleBarLayerView extends FrameLayout
|
||||
implements ViewTreeObserver.OnComputeInternalInsetsListener {
|
||||
|
||||
private static final String TAG = BubbleBarLayerView.class.getSimpleName();
|
||||
|
||||
private static final float SCRIM_ALPHA = 0.2f;
|
||||
|
||||
private final BubbleController mBubbleController;
|
||||
private final BubblePositioner mPositioner;
|
||||
private final BubbleBarAnimationHelper mAnimationHelper;
|
||||
private final View mScrimView;
|
||||
|
||||
@Nullable
|
||||
private BubbleViewProvider mExpandedBubble;
|
||||
private BubbleBarExpandedView mExpandedView;
|
||||
|
||||
// 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. */
|
||||
private boolean mOnLeft = false;
|
||||
|
||||
/** Whether a bubble is expanded. */
|
||||
private boolean mIsExpanded = false;
|
||||
|
||||
private final Region mTouchableRegion = new Region();
|
||||
private final Rect mTempRect = new Rect();
|
||||
|
||||
public BubbleBarLayerView(Context context, BubbleController controller) {
|
||||
super(context);
|
||||
mBubbleController = controller;
|
||||
mPositioner = mBubbleController.getPositioner();
|
||||
|
||||
mAnimationHelper = new BubbleBarAnimationHelper(context,
|
||||
this, mPositioner);
|
||||
|
||||
mScrimView = new View(getContext());
|
||||
mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
mScrimView.setBackgroundDrawable(new ColorDrawable(
|
||||
getResources().getColor(android.R.color.system_neutral1_1000)));
|
||||
addView(mScrimView);
|
||||
mScrimView.setAlpha(0f);
|
||||
mScrimView.setBackgroundDrawable(new ColorDrawable(
|
||||
getResources().getColor(android.R.color.system_neutral1_1000)));
|
||||
|
||||
setOnClickListener(view -> {
|
||||
mBubbleController.collapseStack();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
mPositioner.update();
|
||||
getViewTreeObserver().addOnComputeInternalInsetsListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
|
||||
|
||||
if (mExpandedView != null) {
|
||||
removeView(mExpandedView);
|
||||
mExpandedView = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
|
||||
inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
|
||||
mTouchableRegion.setEmpty();
|
||||
getTouchableRegion(mTouchableRegion);
|
||||
inoutInfo.touchableRegion.set(mTouchableRegion);
|
||||
}
|
||||
|
||||
/** Updates the sizes of any displaying expanded view. */
|
||||
public void onDisplaySizeChanged() {
|
||||
if (mIsExpanded && mExpandedView != null) {
|
||||
updateExpandedView();
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the stack of bubbles is expanded or not. */
|
||||
public boolean isExpanded() {
|
||||
return mIsExpanded;
|
||||
}
|
||||
|
||||
// (TODO: b/273310265): BubblePositioner should be source of truth when this work is done.
|
||||
/** Whether the expanded view is positioned on the left or right side of the screen. */
|
||||
public boolean isOnLeft() {
|
||||
return mOnLeft;
|
||||
}
|
||||
|
||||
/** Shows the expanded view of the provided bubble. */
|
||||
public void showExpandedView(BubbleViewProvider b) {
|
||||
BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView();
|
||||
if (expandedView == null) {
|
||||
return;
|
||||
}
|
||||
if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) {
|
||||
removeView(mExpandedView);
|
||||
mExpandedView = null;
|
||||
}
|
||||
if (mExpandedView == null) {
|
||||
mExpandedBubble = b;
|
||||
mExpandedView = expandedView;
|
||||
final int width = mPositioner.getExpandedViewWidthForBubbleBar();
|
||||
final int height = mPositioner.getExpandedViewHeightForBubbleBar();
|
||||
mExpandedView.setVisibility(GONE);
|
||||
addView(mExpandedView, new FrameLayout.LayoutParams(width, height));
|
||||
}
|
||||
|
||||
mIsExpanded = true;
|
||||
mBubbleController.getSysuiProxy().onStackExpandChanged(true);
|
||||
mAnimationHelper.animateExpansion(mExpandedBubble);
|
||||
showScrim(true);
|
||||
}
|
||||
|
||||
/** Collapses any showing expanded view */
|
||||
public void collapse() {
|
||||
mIsExpanded = false;
|
||||
final BubbleBarExpandedView viewToRemove = mExpandedView;
|
||||
mAnimationHelper.animateCollapse(() -> removeView(viewToRemove));
|
||||
mBubbleController.getSysuiProxy().onStackExpandChanged(false);
|
||||
mExpandedView = null;
|
||||
showScrim(false);
|
||||
}
|
||||
|
||||
/** Updates the expanded view size and position. */
|
||||
private void updateExpandedView() {
|
||||
if (mExpandedView == null) return;
|
||||
final int padding = mPositioner.getBubbleBarExpandedViewPadding();
|
||||
final int width = mPositioner.getExpandedViewWidthForBubbleBar();
|
||||
final int height = mPositioner.getExpandedViewHeightForBubbleBar();
|
||||
FrameLayout.LayoutParams lp = (LayoutParams) mExpandedView.getLayoutParams();
|
||||
lp.width = width;
|
||||
lp.height = height;
|
||||
mExpandedView.setLayoutParams(lp);
|
||||
if (mOnLeft) {
|
||||
mExpandedView.setX(mPositioner.getInsets().left + padding);
|
||||
} else {
|
||||
mExpandedView.setX(mPositioner.getAvailableRect().width() - width - padding);
|
||||
}
|
||||
mExpandedView.setY(mPositioner.getInsets().top + padding);
|
||||
mExpandedView.updateLocation();
|
||||
}
|
||||
|
||||
private void showScrim(boolean show) {
|
||||
if (show) {
|
||||
mScrimView.animate()
|
||||
.setInterpolator(ALPHA_IN)
|
||||
.alpha(SCRIM_ALPHA)
|
||||
.start();
|
||||
} else {
|
||||
mScrimView.animate()
|
||||
.alpha(0f)
|
||||
.setInterpolator(ALPHA_OUT)
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills in the touchable region for expanded view. This is used by window manager to
|
||||
* decide which touch events go to the expanded view.
|
||||
*/
|
||||
private void getTouchableRegion(Region outRegion) {
|
||||
mTempRect.setEmpty();
|
||||
if (mIsExpanded) {
|
||||
getBoundsOnScreen(mTempRect);
|
||||
outRegion.op(mTempRect, Region.Op.UNION);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.content.Context;
|
||||
import android.view.Gravity;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
/**
|
||||
* Handle / menu view to show at the top of a bubble bar expanded view.
|
||||
*/
|
||||
public class HandleView extends LinearLayout {
|
||||
|
||||
// TODO(b/273307221): implement the manage menu in this view.
|
||||
public HandleView(Context context) {
|
||||
super(context);
|
||||
setOrientation(LinearLayout.HORIZONTAL);
|
||||
setGravity(Gravity.CENTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* The menu extends past the top of the TaskView because of the rounded corners. This means
|
||||
* to center content in the menu we must subtract the radius (i.e. the amount of space covered
|
||||
* by TaskView).
|
||||
*/
|
||||
public void setCornerRadius(float radius) {
|
||||
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (int) radius);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user