文章目录
- 对话
- Conversation Space
- Bubbles
- 通知中心的Bubble
- 如何弹出Bubble(app端相关)
- 系统是如何弹出Bubble的(源码相关)
Android R 通知新特性—人与对话(气泡窗)
对话
google在之前的版本就提出了一个“people and conversations”的概念,目的是在手机界面中增加交流类UI的比重,因为毕竟用户使用手机,有很大一部分行为都是基于社交去进行,社交是人与社会沟通的基础。所以google在Android 11中增加了很大一部分的改动用于支持这一行为。且将很多之前版本的新增改动串联起来
Conversation Space
在Android 11上,下拉状态栏提供了一个专属的区域用来显示这些具有会话交互类的通知
可以看到左图:正常的通知是显示在通知区域的,其中飞书的通知也是显示在,可是在我们的认知中,飞书的消息通知也是属于对话类的通知,可是为何未显示在通知栏的对话区域,是因为显示在该区域还需要如下设置:
- 通知使用
MessagingStyle
- (只有在应用以 Android 11 或更高版本为目标平台时适用)通知与Shortcut关联。通知可以通过调用
setShortcutId
或setShortcutInfo
来设置此关联。如果应用以 Android 10 或更低版本为目标平台,通知就不必与Shortcut关联,但UI则会恢复为老版本。
而在右图中:可以看到,在通知区域之上,还有一块名为“对话”的区域,用于显示专属的对话通知。
此空间内的通知在外观和行为上不同于手机上的非对话通知:
- 首先在UI样式上就存在很多不同,对话通知以大图标头像、Person名称、message内容为显示重点,可以看一下右图的Title区域:Parrot • People • 现在 。其中应用的名称并不是Parrot,而是People,Parrot为当前会话的对象名称,也就是Person名称
- 除了一些之前版本就存在的交互逻辑之外,还在右下角有多出一个按钮,用于将当前对话通知以Bubble气泡的形式去悬浮显示,这个我们在下文做详细解释
- 点按通知即可在应用中打开对话(如果对话此前以Bubble形式显示,则会以Bubble的形式中打开),点按文字插入点即可将通知栏中的新消息展开到完整篇幅。
- 提供了对话专用的操作(某些操作通过长按来执行): (1)将此对话标记为优先
(2)将此对话提升为对话泡(仅当应用支持对话泡时才会显示)
(3)将此对话的通知设为静音
(4)为此对话设置自定义提示音或振动
Bubbles
在Android 10上 google就发布了这个叫Bubbles的新特性,一个Bubble(气泡)可以悬浮在其它应用内容之上,可以展开为一个Activity(实际上为一个AvtivityView),也可以收起后随意拖动到屏幕的任意位置。
在Android 10上,电话就已经使用了Bubbles的方式去实现,当用户在拨打电话时切换到其它应用,就会缩小成一个小巧的气泡悬浮在屏幕边缘。
而Android11 上由于ActivityView 功能的增加,bubble的功能也加强了。
通知中心的Bubble
在Android 11上,可通过点击通知中心的对话区域的对话通知,启动对应的对话气泡(Bubble),当然这也需要一定的条件:只有当前对话通知关联了相关的Shortcut快捷方式,才可启动相应的对话气泡。且如果对话在通知栏中标记为“优先”或触发了对话泡显示方式,系统将自动以对话泡形式显示这些对话。
气泡会悬浮并磁吸于屏幕边缘,层级位于状态栏之下,应用之上,用户可以随意拖动吸附并删除
点击则会弹出bubble对应的Activity
注:Bubble相关代码位于frameworks/base/packages/SystemUI/src/com/android/systemui/bubbles
包内
BubbleController.java:
/** Adds the BubbleStackView to the 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) {
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
// themselves.
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_TRUSTED_APPLICATION_OVERLAY,
// Start not focusable - we'll become focusable when expanded so the ActivityView
// can use the IME.
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
PixelFormat.TRANSLUCENT);
mWmLayoutParams.setFitInsetsTypes(0);
mWmLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
mWmLayoutParams.token = new Binder();
mWmLayoutParams.setTitle("Bubbles!");
mWmLayoutParams.packageName = mContext.getPackageName();
mWmLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
try {
mAddedToWindowManager = true;
mWindowManager.addView(mStackView, mWmLayoutParams);
} catch (IllegalStateException e) {
// This means the stack has already been added. This shouldn't happen, since we keep
// track of that, but just in case, update the previously added view's layout params.
e.printStackTrace();
updateWmFlags();
}
}
add的mStackView为BubbleStackView类
/**
* BubbleStackView is lazily created by this method the first time a Bubble is added. This
* method initializes the stack view and adds it to the StatusBar just above the scrim.
*/
private void ensureStackViewCreated() {
if (mStackView == null) {
mStackView = new BubbleStackView(
mContext, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator,
mSysUiState, this::onAllBubblesAnimatedOut, this::onImeVisibilityChanged,
this::hideCurrentInputMethod);
mStackView.addView(mBubbleScrim);
if (mExpandListener != null) {
mStackView.setExpandListener(mExpandListener);
}
mStackView.setUnbubbleConversationCallback(key -> {
final NotificationEntry entry =
mNotificationEntryManager.getPendingOrActiveNotif(key);
if (entry != null) {
onUserChangedBubble(entry, false /* shouldBubble */);
}
});
}
addToWindowManagerMaybe();
}
/**
* Renders bubbles in a stack and handles animating expanded and collapsed states.
*/
public class BubbleStackView extends FrameLayout
implements ViewTreeObserver.OnComputeInternalInsetsListener
可以看到 BubbleStackView继承自FrameLayout,且实现了ViewTreeObserver.OnComputeInternalInsetsListener接口,此接口是在window布局完成时用于计算并设置当前window的TouchRegion属性的接口
/**
* Interface definition for a callback to be invoked when layout has
* completed and the client can compute its interior insets.
*
* We are not yet ready to commit to this API and support it, so
* @hide
*/
public interface OnComputeInternalInsetsListener {
/**
* Callback method to be invoked when layout has completed and the
* client can compute its interior insets.
*
* @param inoutInfo Should be filled in by the implementation with
* the information about the insets of the window. This is called
* with whatever values the previous OnComputeInternalInsetsListener
* returned, if there are multiple such listeners in the window.
*/
public void onComputeInternalInsets(InternalInsetsInfo inoutInfo);
}
我们回到BubbleStackView类中的具体实现:onComputeInternalInsets
@Override
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
mTempRect.setEmpty();
getTouchableRegion(mTempRect);
inoutInfo.touchableRegion.set(mTempRect);
}
可以看到的确在回调的时候去计算region并设置,具体计算我们就不进去细看,这也是为什么只有气泡位置可以响应点击事件,但可以全屏拖动的原因。
如何弹出Bubble(app端相关)
Bubble(气泡)是通过 Notification API 创建的,因此可以照常发送通知。Bubble是通知系统的一部分,如果用户锁屏或者打开了息屏显示(always-on-display),bubble会以普通的通知展示。如果希望让通知显示为气泡,则需要为其附加一些额外的数据。
在语法上,创建bubble和创建通知几乎一模一样。
气泡的展开视图是根据选择的 Activity 创建的。此 Activity 需要经过配置才能正确显示为气泡。此 Activity 必须可以 resizeable 且是 embedded 的。只要 Activity 不满足其中任何一项要求,都会显示为通知。
这些属性需要在AndroidManifest文件中进行配置:
<activity android:name=".bubbles.BubbleActivity"
android:theme="@style/AppTheme.NoActionBar"
android:label="@string/title_activity_bubble"
android:allowEmbedded="true"
android:resizeableActivity="true"/>
如需发送气泡,请按照以下步骤操作:
按照常规方式创建通知。
调用 BubbleMetadata.Builder(PendingIntent, Icon)
或 BubbleMetadata.Builder(String)
以创建 BubbleMetadata 对象。
使用 setBubbleMetadata
将元数据添加到通知中。
// Create bubble intent
Intent target = new Intent(mContext, BubbleActivity.class);
PendingIntent bubbleIntent =
PendingIntent.getActivity(mContext, 0, target, 0 /* flags */);
// Create bubble metadata
Notification.BubbleMetadata bubbleData =
new Notification.BubbleMetadata.Builder(bubbleIntent,
Icon.createWithResource(context, R.drawable.icon))
.setDesiredHeight(600)
.build();
// Create notification
Person chatPartner = new Person.Builder()
.setName("Chat partner")
.setImportant(true)
.build();
Notification.Builder builder =
new Notification.Builder(mContext, CHANNEL_ID)
.setContentIntent(contentIntent)
.setSmallIcon(smallIcon)
.setBubbleMetadata(bubbleData)
.addPerson(chatPartner);
其中的Person对象是和Android10上显示bubble相关
更多的气泡通知相关的配置可查阅google官方文档
系统是如何弹出Bubble的(源码相关)
那么我们是如何在接收到Notification的时候去弹出Bubble气泡呢
我们首先dump一下Bubble的window相关信息
可以看到,window宽高为屏幕宽高[1080,2220] (测试机宽高为1080*2220)
层级为2042
public static final int TYPE_TRUSTED_APPLICATION_OVERLAY = FIRST_SYSTEM_WINDOW + 42;
我们通过dump信息中的Window Title -> "Bubbles!"去查找相关代码,发现window的add位置位于BubbleController中:
BubbleController.java:
/** Adds the BubbleStackView to the 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) {
return;
}
mWmLayoutParams = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_TRUSTED_APPLICATION_OVERLAY,
// Start not focusable - we'll become focusable when expanded so the ActivityView
// can use the IME.
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
PixelFormat.TRANSLUCENT);
mWmLayoutParams.setFitInsetsTypes(0);
mWmLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
mWmLayoutParams.token = new Binder();
mWmLayoutParams.setTitle("Bubbles!");
mWmLayoutParams.packageName = mContext.getPackageName();
mWmLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
try {
mAddedToWindowManager = true;
mWindowManager.addView(mStackView, mWmLayoutParams);
} catch (IllegalStateException e) {
e.printStackTrace();
updateWmFlags();
}
}
add的mStackView对象为BubbleStackView类,我们查看一下赋值的位置:
/**
* BubbleStackView is lazily created by this method the first time a Bubble is added. This
* method initializes the stack view and adds it to the StatusBar just above the scrim.
*/
private void ensureStackViewCreated() {
if (mStackView == null) {
mStackView = new BubbleStackView(
mContext, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator,
mSysUiState, this::onAllBubblesAnimatedOut, this::onImeVisibilityChanged,
this::hideCurrentInputMethod);
mStackView.addView(mBubbleScrim);
......
}
addToWindowManagerMaybe();
}
BubbleStackView.java
/**
* Renders bubbles in a stack and handles animating expanded and collapsed states.
*/
public class BubbleStackView extends FrameLayout
implements ViewTreeObserver.OnComputeInternalInsetsListener
可以看到 BubbleStackView继承自FrameLayout,且实现了ViewTreeObserver.OnComputeInternalInsetsListener接口,此接口是在window布局完成时用于计算并设置当前window的TouchRegion属性的接口
/**
* Interface definition for a callback to be invoked when layout has
* completed and the client can compute its interior insets.
*
* We are not yet ready to commit to this API and support it, so
* @hide
*/
public interface OnComputeInternalInsetsListener {
/**
* Callback method to be invoked when layout has completed and the
* client can compute its interior insets.
*
* @param inoutInfo Should be filled in by the implementation with
* the information about the insets of the window. This is called
* with whatever values the previous OnComputeInternalInsetsListener
* returned, if there are multiple such listeners in the window.
*/
public void onComputeInternalInsets(InternalInsetsInfo inoutInfo);
}
我们回到BubbleStackView类中的具体实现:onComputeInternalInsets
@Override
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
mTempRect.setEmpty();
getTouchableRegion(mTempRect);
inoutInfo.touchableRegion.set(mTempRect);
}
可以看到的确在回调的时候去计算region并设置,具体计算我们就不进去细看,这也是为什么只有气泡位置可以响应点击事件,但可以全屏拖动的原因。
我们从addWindow的位置继续往上追溯。然后再从上至下分析一下相关的流程:
SystemUI在启动的时候会去初始化BubbleController类,具体位于
SystemUIBinder类中,通过dagger依赖注入的方式去实现
@Module(includes = {RecentsModule.class, StatusBarModule.class, BubbleModule.class,
KeyguardModule.class})
BubbleModule.java:
static BubbleController newBubbleController(......) {
return new BubbleController(......);
}
public BubbleController(......) {
......
mBubbleData = data;
mBubbleData.setListener(mBubbleDataListener);
......
mNotificationEntryManager = entryManager;
mNotificationGroupManager = groupManager;
mNotifPipeline = notifPipeline;
if (!featureFlags.isNewNotifPipelineRenderingEnabled()) {
setupNEM();
} else {
setupNotifPipeline();
}
......
}
主要代码位于上方:
首先将一个BubbleData.Listener设置在BubbleData中。
之后在下方调用了setupNEM() 方法
此方法主要是设置通知的监听:
private void setupNEM() {
mNotificationEntryManager.addNotificationEntryListener(
new NotificationEntryListener() {
@Override
public void onPendingEntryAdded(NotificationEntry entry) {
onEntryAdded(entry);
}
@Override
public void onPreEntryUpdated(NotificationEntry entry) {
onEntryUpdated(entry);
}
@Override
public void onEntryRemoved(
NotificationEntry entry,
@android.annotation.Nullable NotificationVisibility visibility,
boolean removedByUser,
int reason) {
BubbleController.this.onEntryRemoved(entry);
}
@Override
public void onNotificationRankingUpdated(RankingMap rankingMap) {
onRankingUpdated(rankingMap);
}
});
mNotificationEntryManager.addNotificationRemoveInterceptor(
new NotificationRemoveInterceptor() {
@Override
public boolean onNotificationRemoveRequested(
String key,
NotificationEntry entry,
int dismissReason) {
......
}
});
mNotificationGroupManager.addOnGroupChangeListener(
new NotificationGroupManager.OnGroupChangeListener() {
@Override
public void onGroupSuppressionChanged(
NotificationGroupManager.NotificationGroup group,
boolean suppressed) {
......
}
});
......
}
当通知发生改变的时候就会回调这些接口
例如当来了一条新通知,就会先去判定是否需要弹出bubble,如果需要,则调用到onEntryAdded方法
private void onEntryAdded(NotificationEntry entry) {
if (mNotificationInterruptStateProvider.shouldBubbleUp(entry)
&& entry.isBubble()
&& canLaunchInActivityView(mContext, entry)) {
updateBubble(entry);
}
}
void updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade) {
// If this is an interruptive notif, mark that it's interrupted
if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) {
notif.setInterruption();
}
Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */);
inflateAndAdd(bubble, suppressFlyout, showInShade);
}
getOrCreateBubble方法将传入的NotificationEntry转换为Bubble,NotificationEntry中封装了此条通知的相关信息。
之后再调用inflateAndAdd方法,将转化出的bubble对象解析并在界面显示,我们继续看inflateAndAdd方法的具体实现
void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
// Lazy init stack view when a bubble is created
ensureStackViewCreated();
bubble.setInflateSynchronously(mInflateSynchronously);
bubble.inflate(b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade),
mContext, mStackView, mBubbleIconFactory, false /* skipInflation */);
}
首先调用ensureStackViewCreated方法将StackView inflate并显示出来,这就是我们在前面看到的
然后调用Bubble的inflate方法去做Bubble的加载
void inflate(BubbleViewInfoTask.Callback callback,
Context context,
BubbleStackView stackView,
BubbleIconFactory iconFactory,
boolean skipInflation) {
if (isBubbleLoading()) {
mInflationTask.cancel(true /* mayInterruptIfRunning */);
}
mInflationTask = new BubbleViewInfoTask(this,
context,
stackView,
iconFactory,
skipInflation,
callback);
if (mInflateSynchronously) {
mInflationTask.onPostExecute(mInflationTask.doInBackground());
} else {
mInflationTask.execute();
}
}
可以看到在inflate的时候创建了一个BubbleViewInfoTask类,此类继承自AsyncTask类,专门用于实现异步加载Bubble的行为
public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask.BubbleViewInfo>
@Override
protected BubbleViewInfo doInBackground(Void... voids) {
return BubbleViewInfo.populate(mContext.get(), mStackView.get(), mIconFactory, mBubble,
mSkipInflation);
}
@Override
protected void onPostExecute(BubbleViewInfo viewInfo) {
if (viewInfo != null) {
mBubble.setViewInfo(viewInfo);
if (mCallback != null && !isCancelled()) {
mCallback.onBubbleViewsReady(mBubble);
}
}
}
在doBackground中调用BubbleViewInfo的静态方法populate,此方法是用于解析Bubble并生成BubbleViewInfo对象,此对象封装了包括View对象、Bubble数据在内的各种属性,具体内容可以在populate的解析行为中略知一二
在解析完成后,onPostExecute会调用传入callback的onBubbleViewsReady方法,并将解析完成的BubbleViewInfo对象传入,从前面的代码可知,此时会进入BubbleData的notificationEntryUpdated方法中
/**
* When this method is called it is expected that all info in the bubble has completed loading.
* @see Bubble#inflate(BubbleViewInfoTask.Callback, Context,
* BubbleStackView, BubbleIconFactory).
*/
void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
......
if (prevBubble == null) {
// Create a new bubble
bubble.setSuppressFlyout(suppressFlyout);
doAdd(bubble);
trim();
} else {
// Updates an existing bubble
bubble.setSuppressFlyout(suppressFlyout);
doUpdate(bubble);
}
......
dispatchPendingChanges();
}
首先会判定是否为新的bubble,如果是新bubble,则调用doAdd方法,如果是现存的bubble,则调用doUpdate方法去更新此bubble,最后调用dispatchPendingChanges方法去提交此次更新
我们不妨先去看doAdd方法
private void doAdd(Bubble bubble) {
mBubbles.add(0, bubble);
mStateChange.addedBubble = bubble;
......
}
重点为标红位置,将当前需要添加的bubble add到一个List中,然后把此bubble赋值给addedBubble
然后我们查看dispatch方法
private void dispatchPendingChanges() {
if (mListener != null && mStateChange.anythingChanged()) {
mListener.applyUpdate(mStateChange);
}
mStateChange = new Update(mBubbles, mOverflowBubbles);
}
anythingChanged函数会判定当前是否有需要更新的改变,例如是否有展开bubble的操作、是否有添加或更新操作、bubble之间的顺序更改等等
boolean anythingChanged() {
return expandedChanged
|| selectionChanged
|| addedBubble != null
|| updatedBubble != null
|| !removedBubbles.isEmpty()
|| orderChanged;
}
最后调用mListener的applyUpdate方法,将此次更新传入
那这个mBubbleData的私有变量mListener又是从哪赋值的呢?
我们别忘了最开始在BubbleController初始化的时候的一个行为
public BubbleController(......) {
......
mBubbleData = data;
mBubbleData.setListener(mBubbleDataListener);
......
mNotificationEntryManager = entryManager;
mNotificationGroupManager = groupManager;
mNotifPipeline = notifPipeline;
if (!featureFlags.isNewNotifPipelineRenderingEnabled()) {
setupNEM();
} else {
setupNotifPipeline();
}
......
}
mBubbleDataListener的具体实现位于BubbleController类中
private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() {
@Override
public void applyUpdate(BubbleData.Update update) {
ensureStackViewCreated();
......
// Collapsing? Do this first before remaining steps.
if (update.expandedChanged && !update.expanded) {
mStackView.setExpanded(false);
mNotificationShadeWindowController.setForceHasTopUi(mHadTopUi);
}
// Do removals, if any.
for (Pair<Bubble, Integer> removed : removedBubbles) {
......
if (mStackView != null) {
mStackView.removeBubble(bubble);
}
......
if (update.addedBubble != null && mStackView != null) {
mDataRepository.addBubble(mCurrentUserId, update.addedBubble);
mStackView.addBubble(update.addedBubble);
}
if (update.updatedBubble != null && mStackView != null) {
mStackView.updateBubble(update.updatedBubble);
}
// At this point, the correct bubbles are inflated in the stack.
// Make sure the order in bubble data is reflected in bubble row.
if (update.orderChanged && mStackView != null) {
mDataRepository.addBubbles(mCurrentUserId, update.bubbles);
mStackView.updateBubbleOrder(update.bubbles);
}
......
// Expanding? Apply this last.
if (update.expandedChanged && update.expanded) {
if (mStackView != null) {
mStackView.setExpanded(true);
......
}
}
......
}
};
可以看到,在applyUpdate中,会根据不同的判定,对mStackView进行不同的操作,具体的UI相关操作我们就暂时先不细细分析,之后补上。
bubble的展开效果:
上述代码中mStackView.setExpanded(true);会触发bubble的展开,而展开后的View在BubbleExpandedView.java中。很明显的可以看到,该View里包含了一个ActivityView的实例。
void populateExpandedView() {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "populateExpandedView: "
+ "bubble=" + getBubbleKey());
}
if (usingActivityView()) {
mActivityView.setCallback(mStateCallback);
} else {
Log.e(TAG, "Cannot populate expanded view.");
}
}
private ActivityView.StateCallback mStateCallback = new ActivityView.StateCallback() {
@Override
public void onActivityViewReady(ActivityView view) {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "onActivityViewReady: mActivityViewStatus=" + mActivityViewStatus
+ " bubble=" + getBubbleKey());
}
switch (mActivityViewStatus) {
case INITIALIZING:
case INITIALIZED:
// Custom options so there is no activity transition animation
ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
0 /* enterResId */, 0 /* exitResId */);
options.setTaskAlwaysOnTop(true);
options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
// Post to keep the lifecycle normal
post(() -> {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "onActivityViewReady: calling startActivity, "
+ "bubble=" + getBubbleKey());
}
if (mActivityView == null) {
mBubbleController.removeBubble(getBubbleKey(),
BubbleController.DISMISS_INVALID_INTENT);
return;
}
try {
if (!mIsOverflow && mBubble.hasMetadataShortcutId()
&& mBubble.getShortcutInfo() != null) {
options.setApplyActivityFlagsForBubbles(true);
// 1
mActivityView.startShortcutActivity(mBubble.getShortcutInfo(),
options, null /* sourceBounds */);
} else {
Intent fillInIntent = new Intent();
fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
if (mBubble != null) {
mBubble.setIntentActive();
}
// 1
mActivityView.startActivity(mPendingIntent, fillInIntent, options);
}
} catch (RuntimeException e) {
Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
+ ", " + e.getMessage() + "; removing bubble");
mBubbleController.removeBubble(getBubbleKey(),
BubbleController.DISMISS_INVALID_INTENT);
}
});
mActivityViewStatus = ActivityViewStatus.ACTIVITY_STARTED;
break;
case ACTIVITY_STARTED:
// 3
post(() -> mActivityManager.moveTaskToFront(mTaskId, 0));
break;
}
}
@Override
public void onActivityViewDestroyed(ActivityView view) {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "onActivityViewDestroyed: mActivityViewStatus=" + mActivityViewStatus
+ " bubble=" + getBubbleKey());
}
mActivityViewStatus = ActivityViewStatus.RELEASED;
}
@Override
public void onTaskCreated(int taskId, ComponentName componentName) {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "onTaskCreated: taskId=" + taskId
+ " bubble=" + getBubbleKey());
}
// 2
mTaskId = taskId;
}
@Override
public void onTaskRemovalStarted(int taskId) {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId
+ " mActivityViewStatus=" + mActivityViewStatus
+ " bubble=" + getBubbleKey());
}
if (mBubble != null) {
// Must post because this is called from a binder thread.
post(() -> mBubbleController.removeBubble(mBubble.getKey(),
BubbleController.DISMISS_TASK_FINISHED));
}
}
};
上述代码可以看出,BubbleExpadnedView会对ActivityView设置一个回调。根据回调状态的不同来处理
- 当onActivityViewReady回调过来时:
根据配置信息,使用ActivityView startActivity / startShortcutActivity。实际是ActivityView里的TaskEmbedder进行操作。关于TaskEmbedder 可以看这篇更专业的文档梦幻联动。 这里不再讲解。 - onTaskCreated回调会记录创建的taskId。
- onActivityViewReady再次调用时,mActivityViewStatus已经修改为ACTIVITY_STARTED,所以直接将步骤2保存的Task移动到前台。就将应用设置的Activity显示出来了。