Analysis of Bubbles principle

official document

https://developer.android.com/develop/ui/views/notifications/bubbles#the_bubble_api

Bubbles make it easy for users to view and participate in conversations.
Bubbles are built into the notification system. They float above other app content and follow users wherever they go. Bubbles can be expanded to reveal app functions and information, and can be collapsed when not in use.
Bubbles appear like usual notifications when the device is locked or when the always-display is active.
Bubbles is an opt-out feature. When the app shows the first bubble, it displays a permission dialog with two choices:

  • Block all bubbles in your app - notifications will not be blocked, but they will never appear as bubbles
  • Allow all bubbles in your app - All notifications sent using BubbleMetaData will be displayed as bubbles

https://developer.android.com/static/images/guide/topics/ui/bubbles-demo.mp4

Bubble API

Bubbles are created via the Notification API, so you can send notifications as usual. If you want your notification to appear as a bubble, you need to attach some additional data to it.
An expanded view of the bubble is created based on the Activity you selected. This Activity needs to be configured to display properly as a bubble. The Activity must be resizable and embedded . Whenever the Activity does not meet any of these requirements, it will be displayed as a notification.
The following code demonstrates how to implement a simple bubble:

<activity
  android:name=".bubbles.BubbleActivity"
  android:theme="@style/AppTheme.NoActionBar"
  android:label="@string/title_activity_bubble"
  android:allowEmbedded="true"
  android:resizeableActivity="true"
  />

If your application needs to display multiple bubbles of the same type (such as multiple chat conversations with different contacts), this activity must be able to start multiple instances. On devices running Android 10, notifications don't appear as bubbles unless you explicitly set documentLaunchMode to "always". Starting with Android 11, you don't need to explicitly set this value, as the system automatically sets documentLaunchMode to "always" for all conversations.
To send bubbles, follow these steps:

Note: The first time a notification is sent to show a bubble, it must be in a notification channel of IMPORTANCE_MIN or higher.

If your app is in the foreground when the bubble is sent, the importance will be ignored and your bubble will always be shown (unless the user has blocked bubbles or notifications from your app).

// Create bubble intent
Intent target = new Intent(mContext, BubbleActivity.class);
PendingIntent bubbleIntent =
    PendingIntent.getActivity(mContext, 0, target, 0 /* flags */);

private val CATEGORY_TEXT_SHARE_TARGET =
    "com.example.category.IMG_SHARE_TARGET"

Person chatPartner = new Person.Builder()
        .setName("Chat partner")
        .setImportant(true)
        .build();

// 创建共享快捷方式
private String shortcutId = generateShortcutId();
ShortcutInfo shortcut =
    new ShortcutInfo.Builder(mContext, shortcutId)
    .setCategories(Collections.singleton(CATEGORY_TEXT_SHARE_TARGET))
    .setIntent(Intent(Intent.ACTION_DEFAULT))
    .setLongLived(true)
    .setShortLabel(chatPartner.getName())
    .build();

// Create bubble metadata
Notification.BubbleMetadata bubbleData =
    new Notification.BubbleMetadata.Builder(bubbleIntent,
                                            Icon.createWithResource(context, R.drawable.icon))
    .setDesiredHeight(600)
    .build();

// Create notification, referencing the sharing shortcut
Notification.Builder builder =
    new Notification.Builder(mContext, CHANNEL_ID)
    .setContentIntent(contentIntent)
    .setSmallIcon(smallIcon)
    .setBubbleMetadata(bubbleData)
    .setShortcutId(shortcutId)
    .addPerson(chatPartner);

Create an expanded bubble

You can configure bubbles to automatically appear in the expanded state. We recommend that you only use this feature when the user takes an action that causes the bubble to appear, such as tapping a button to start a new chat. In this case, it is also necessary to suppress the initial notification sent when the bubble is created.
You can set flags that enable these behaviors with: setAutoExpandBubble() and setSuppressNotification() .

Notification.BubbleMetadata bubbleData =
    new Notification.BubbleMetadata.Builder()
        .setDesiredHeight(600)
        .setIntent(bubbleIntent)
        .setAutoExpandBubble(true)
        .setSuppressNotification(true)
        .build();

Bubble Content Lifecycle

If the bubble is expanded, the content activity completes the normal process lifecycle , which brings the app into the foreground process (if the app is not already running in the foreground).
If the bubble is collapsed or closed, the system destroys the activity. This may cause the system to cache the process and then terminate it, depending on whether the app has any other foreground components running.

When to Show Bubbles

Bubbles are only shown in certain situations to reduce disruption to users.
If your app targets Android 11 or higher, notifications won't appear as bubbles unless they meet the dialog requirements . If your app targets Android 10, notifications appear as bubbles only if one or more of the following conditions are met:

If none of the above conditions are met, the notification will be shown without a bubble.

best practice

  • Bubbles take up screen real estate and obscure other app content. Send notifications as bubbles only when there is a strong need to display bubbles (such as for ongoing communication) or when the user explicitly requests that bubbles be displayed for something.
  • Note that bubbles can be disabled by the user. In this case, bubble notifications appear as generic notifications. You should always make sure that your bubble notifications also work as general notifications.
  • Processes started from bubbles, such as activities and dialogs, are displayed in bubble containers. This means bubbles can have task stacks. Things can get complicated if you have a lot of functionality or navigation in your bubble. We recommend that you keep your features as specific and concise as possible.
  • Make sure to call super.onBackPressed when overriding onBackPressed in your Bubbles Activity ; otherwise, Bubbles may not function properly.
  • When a bubble receives an updated message after it's collapsed, the bubble displays a flag icon to indicate that it has unread messages. When a user opens a message in the associated app, follow these steps:

sample application

The People sample app is a simple conversational app using speech bubbles. For demonstration purposes, this app uses a chatbot. In a real application, bubbles should only be used for messages sent by humans, not messages sent by chatbots.

Bubbles are a special type of content that can "float" above other applications or system UI.
Bubbles can be expanded to reveal more content.
The controller manages the addition, removal, and visibility of bubbles on the screen.

layout analysis

insert image description here
insert image description here

When the root layout BubbleStackView is added, first create and add BubbleExpandedView and initialize and add BubbleOverflowContainerView

     // 在这里初始化{@link BubbleController}和{@link BubbleStackView},
	// 这个方法必须在view inflate之后调用。
    void initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow) {
    
    
        mController = controller;
        mStackView = stackView;
        mIsOverflow = isOverflow;
        mPositioner = mController.getPositioner();
    	// 首次先添加右侧的BubbleOverflowContainerView相关的
        if (mIsOverflow) {
    
    
            mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate(
                    R.layout.bubble_overflow_container, null /* root */);
            mOverflowView.setBubbleController(mController);
            FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
            mExpandedViewContainer.addView(mOverflowView, lp);
            mExpandedViewContainer.setLayoutParams(
                    new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
            bringChildToFront(mOverflowView);
            mManageButton.setVisibility(GONE);
        } ......
    }

The two BadgedImageViews on the left page are loaded and the BubbleExpandedView is added

        @VisibleForTesting
        @Nullable
        public static BubbleViewInfo populate(Context c, BubbleController controller,
                BubbleStackView stackView, BubbleIconFactory iconFactory,
                BubbleBadgeIconFactory badgeIconFactory, Bubble b,
                boolean skipInflation) {
    
    
            BubbleViewInfo info = new BubbleViewInfo();

            // View inflation: only should do this once per bubble
            if (!skipInflation && !b.isInflated()) {
    
    
                LayoutInflater inflater = LayoutInflater.from(c);
                info.imageView = (BadgedImageView) inflater.inflate(
                        R.layout.bubble_view, stackView, false /* attachToRoot */);
                info.imageView.initialize(controller.getPositioner());

                info.expandedView = (BubbleExpandedView) inflater.inflate(
                        R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
                info.expandedView.initialize(controller, stackView, false /* isOverflow */);
            }

Add TaskView

     // 在这里初始化{@link BubbleController}和{@link BubbleStackView},
	// 这个方法必须在view inflate之后调用。
    void initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow) {
    
    
        mController = controller;
        mStackView = stackView;
        mIsOverflow = isOverflow;
        mPositioner = mController.getPositioner();
    	// 首次先添加右侧的BubbleOverflowContainerView相关的
        if (mIsOverflow) {
    
    
        	......
        } else {
    
    
            // 添加TaskView
            mTaskView = new TaskView(mContext, mController.getTaskOrganizer(),
                    mController.getTaskViewTransitions(), mController.getSyncTransactionQueue());
            mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener);
            mExpandedViewContainer.addView(mTaskView);
            bringChildToFront(mTaskView);
        }
    }

code analysis

Window type: Application overlay windows appear above all active windows (types between {@link #FIRST_APPLICATION_WINDOW} and {@link #LAST_APPLICATION_WINDOW}) but below key system windows like the status bar or IME.
The system can change the position, size, or visibility of these windows at any time to reduce visual clutter for the user and manage resources.
Requires {@link android.Manifest.permission#SYSTEM_ALERT_WINDOW} permission.
The system will adjust the importance of processes with this window type to reduce the chances of the low memory killer killing them.
On a multi-user system, only displayed on the owning user's screen.

public static final int TYPE_APPLICATION_OVERLAY = FIRST_SYSTEM_WINDOW + 38;

Window flag: This window never gets key input focus, so the user cannot send key or other button events to it. Those will go to any focusable windows behind it. This flag will also enable {@link #FLAG_NOT_TOUCH_MODAL}, whether explicitly set or not.
Setting this flag also means that the window does not need to interact with the soft input method, so it will be Z-ordered and positioned independently of any active IME (usually this means it gets Z-ordered above the IME, so it can use fullscreen content and override the input method if needed. You can modify this behavior with {@link #FLAG_ALT_FOCUSABLE_IM}.

public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;

Window flag: Allows any pointer events outside the window to be sent to the window behind it, even if this window is focusable (its {@link #FLAG_NOT_FOCUSABLE} is not set). Otherwise it consumes all pointer events by itself, whether they are inside the window or not.

public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;

Specifies that the window should be considered a trusted system overlay. Trusted system overrides are ignored when considering whether windows are obscured during input scheduling. Requires {@link android.Manifest.permission#INTERNAL_SYSTEM_WINDOW} permission.
{@see android.view.MotionEvent#FLAG_WINDOW_IS_OBSCURED}
{@see android.view.MotionEvent#FLAG_WINDOW_IS_PARTIALLY_OBSCURED}

public void setTrustedOverlay() {
    
    
    privateFlags |= PRIVATE_FLAG_TRUSTED_OVERLAY;
}

Specifies which inset types this window should avoid overlapping during layout.
@param Type{@link WindowInsets.Type} The insets this window should avoid.
The initial value of this object includes all system bars.

public void setFitInsetsTypes(@InsetsType int types) {
    
    
    mFitInsetsTypes = types;
    privateFlags |= PRIVATE_FLAG_FIT_INSETS_CONTROLLED;
}

Resize option for {@link #softInputMode}: Set to allow the window to be resized when an input method is shown so that its content is not covered by the input method. This cannot be used in conjunction with {@link #SOFT_INPUT_ADJUST_PAN}; if neither of these is set, then the system will try to choose one or the other depending on the contents of the window. If the window's layout parameter flags include {@link #FLAG_FULLSCREEN}, this value for {@link #softInputMode} will be ignored; the window will not be resized, but will remain fullscreen.
@deprecated Call {@link Window#setDecorFitsSystemWindows(boolean)} with {@code false} and install {@link OnApplyWindowInsetsListener} on root content view for insets of fit type {@link Type#ime()}.

@Deprecated
public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;

Always allow the window to extend to the {@link DisplayCutout} area of ​​all edges of the screen.
The window must ensure that no important content overlaps the {@link DisplayCutout}.
In this mode, the window extends under the cutout on all edges in portrait and landscape orientation, whether or not the window hides the system bars.

public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS = 3;

Add root layout BubbleStackView

Add a full-screen bubble window to the screen, mStackView is a FrameLayout.

The BubbleStackView is lazily created by this method when the Bubble is first added. This method initializes the stack view and adds it to the window manager.

    private void ensureStackViewCreated() {
    
    
        if (mStackView == null) {
    
    
            // 创建bubble根布局
            mStackView = new BubbleStackView(
                    mContext, this, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator,
                    mMainExecutor);
            mStackView.onOrientationChanged();
            if (mExpandListener != null) {
    
    
                mStackView.setExpandListener(mExpandListener);
            }
            mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
        }
        addToWindowManagerMaybe();
    }
    /** 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_APPLICATION_OVERLAY,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                        | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
                PixelFormat.TRANSLUCENT);

        mWmLayoutParams.setTrustedOverlay();
        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;
        mWmLayoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;

        try {
    
    
            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;
            });
        } catch (IllegalStateException e) {
    
    
            // This means the stack has already been added. This shouldn't happen...
            e.printStackTrace();
        }
    }

Add and display Task

View that can display a task.

  1. The initialization has been completed, directly hang the surfacecontrol corresponding to the Task under the surfacecontrol of the TaskView and display it
  2. Otherwise, first start the corresponding activity to complete the initialization, wait for the Task to be created and complete the operation 1 in the onTaskAppeared method
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
    
    
        mSurfaceCreated = true;
        if (mListener != null && !mIsInitialized) {
    
    
            mIsInitialized = true;
            // 未初始化,先启动activity
            mListenerExecutor.execute(() -> {
    
    
                mListener.onInitialized();
            });
        }
        mShellExecutor.execute(() -> {
    
    
            if (mTaskToken == null) {
    
    
                // Nothing to update, task is not yet available
                return;
            }
            if (isUsingShellTransitions()) {
    
    
                mTaskViewTransitions.setTaskViewVisible(this, true /* visible */);
                return;
            }
            // 将Task的surfacecontrol从先有层级剥离,挂在TaskView(SurfaceView的子类)下面
            // Reparent the task when this surface is created
            mTransaction.reparent(mTaskLeash, getSurfaceControl())
                    .show(mTaskLeash)
                    .apply();
            updateTaskVisibility();
        });
    }

The container for the expanded bubble view, which handles rendering the caret and settings icons.

 @Override
public void onInitialized() {
    
    

    if (mDestroyed || mInitialized) {
    
    
        return;
    }

    // 自定义选项,因此没有activity过渡动画
    ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),0 /* enterResId */, 0 /* exitResId */);

    Rect launchBounds = new Rect();
    mTaskView.getBoundsOnScreen(launchBounds);

    // TODO: I notice inconsistencies in lifecycle
    // Post to keep the lifecycle normal
    post(() -> {
    
    
        try {
    
    
            options.setTaskAlwaysOnTop(true);
            options.setLaunchedFromBubble(true);
            if (!mIsOverflow && mBubble.hasMetadataShortcutId()) {
    
    
                options.setApplyActivityFlagsForBubbles(true);
                mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
                        options, launchBounds);
            } else {
    
    
                Intent fillInIntent = new Intent();
                // Apply flags to make behaviour match documentLaunchMode=always.
                fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
                fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                if (mBubble != null) {
    
    
                    mBubble.setIntentActive();
                }
                mTaskView.startActivity(mPendingIntent, fillInIntent, options,
                        launchBounds);
            }
        } catch (RuntimeException e) {
    
    
            Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
                    + ", " + e.getMessage() + "; removing bubble");
            mController.removeBubble(getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT);
        }
    });
    mInitialized = true;
}

insert image description here

    @Override
    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo,
            SurfaceControl leash) {
    
    
        if (isUsingShellTransitions()) {
    
    
            // Everything else handled by enter transition.
            return;
        }
        mTaskInfo = taskInfo;
        mTaskToken = taskInfo.token;
        mTaskLeash = leash;

        if (mSurfaceCreated) {
    
    
            // Surface is ready, so just reparent the task to this surface control
            mTransaction.reparent(mTaskLeash, getSurfaceControl())
                    .show(mTaskLeash)
                    .apply();
        } else {
    
    
            // The surface has already been destroyed before the task has appeared,
            // so go ahead and hide the task entirely
            updateTaskVisibility();
        }

Sequence diagram of onTaskAppeared callback:
insert image description here
insert image description here

Remove Task

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    
    
        mSurfaceCreated = false;
        mShellExecutor.execute(() -> {
    
    
            if (mTaskToken == null) {
    
    
                // Nothing to update, task is not yet available
                return;
            }

            if (isUsingShellTransitions()) {
    
    
                mTaskViewTransitions.setTaskViewVisible(this, false /* visible */);
                return;
            }

            // Unparent the task when this surface is destroyed
            mTransaction.reparent(mTaskLeash, null).apply();
            updateTaskVisibility();
        });
    }
    @Override
    public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
    
    
    	// 与出现时不同,我们还不能保证消失会在我们知道的转换中发生——所以即使启用了 shell 转换,也请将清理留在这里。
        if (mTaskToken == null || !mTaskToken.equals(taskInfo.token)) return;

        if (mListener != null) {
    
    
            final int taskId = taskInfo.taskId;
            mListenerExecutor.execute(() -> {
    
    
                mListener.onTaskRemovalStarted(taskId);
            });
        }
        mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, false);

        // Unparent the task when this surface is destroyed
        mTransaction.reparent(mTaskLeash, null).apply();
        resetTaskInfo();
    }

Guess you like

Origin blog.csdn.net/xiaoyantan/article/details/128675924