Bubbles原理解析

官方文档

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

气泡使用户可以轻松查看和参与对话。
气泡内置于通知系统中。 它们漂浮在其他应用程序内容之上,无论用户走到哪里都会跟随他们。 气泡可以展开以显示应用程序功能和信息,并且可以在不使用时折叠。
当设备被锁定或始终显示处于活动状态时,气泡会像通常的通知一样出现。
气泡是一种选择退出功能。 当应用程序显示第一个气泡时,会显示一个权限对话框,其中提供两种选择:

  • 阻止您应用中的所有气泡 - 通知不会被阻止,但它们永远不会显示为气泡
  • 允许您应用中的所有气泡 - 所有使用 BubbleMetaData 发送的通知都将显示为气泡

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

气泡API

气泡是通过 Notification API 创建的,因此您可以照常发送通知。如果您希望让通知显示为气泡,则需要为其附加一些额外的数据。
气泡的展开视图是根据您选择的 Activity 创建的。此 Activity 需要经过配置才能正确显示为气泡。此 Activity 必须可以调整大小且是嵌入式的。只要 Activity 不满足其中任何一项要求,都会显示为通知。
以下代码演示了如何实现简单的气泡:

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

如果您的应用需要显示多个相同类型的气泡(例如与不同联系人的多个聊天对话),此 Activity 必须能够启动多个实例。在搭载 Android 10 的设备上,除非您将 documentLaunchMode 明确设置为 “always”,否则通知不会显示为气泡。从 Android 11 开始,您无需明确设置此值,因为系统会自动将所有对话的 documentLaunchMode 设置为 “always”。
如需发送气泡,请按照以下步骤操作:

  • 按照常规方式创建通知。
  • 调用 BubbleMetadata.Builder(PendingIntent, Icon) 或 BubbleMetadata.Builder(String) 以创建 BubbleMetadata 对象。
  • 使用 setBubbleMetadata() 将元数据添加到通知中。
  • 如果以 Android 11 或更高版本为目标平台,对话泡元数据或通知必须引用共享快捷方式。

注意:第一次发送通知以显示气泡时,它必须位于 IMPORTANCE_MIN 或更高级别的通知渠道中。

如果在发送气泡时您的应用程序在前台,重要性将被忽略并且您的气泡将始终显示(除非用户已阻止来自您的应用程序的气泡或通知)。

// 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);

创建展开的气泡

您可以将气泡配置为自动以展开状态显示。我们建议您仅在用户执行会导致显示气泡的操作(例如点按按钮以开始新的聊天)时才使用此功能。在这种情况下,还有必要禁止显示在创建气泡时发送的初始通知。
您可以使用以下方法设置启用这些行为的标志:setAutoExpandBubble() 和setSuppressNotification()。

Notification.BubbleMetadata bubbleData =
    new Notification.BubbleMetadata.Builder()
        .setDesiredHeight(600)
        .setIntent(bubbleIntent)
        .setAutoExpandBubble(true)
        .setSuppressNotification(true)
        .build();
气泡内容生命周期

如果展开气泡,内容 Activity 会完成常规进程生命周期,这会使应用成为前台进程(如果应用尚未在前台运行)。
如果收起或关闭气泡,系统会销毁此 Activity。这可能导致系统缓存此进程,然后将其终止,具体取决于应用是否有任何其他前台组件正在运行。

何时显示气泡

为减少对用户的干扰,气泡仅在特定情况下显示。
如果应用以 Android 11 或更高版本为目标平台,那么除非通知符合对话要求,否则将不会显示为气泡。如果应用以 Android 10 为目标平台,那么仅在满足以下一个或多个条件时,通知才会显示为气泡:

  • 通知使用 MessagingStyle,并添加了 Person。
  • 通知来自对 Service.startForeground 的调用,类别为 CATEGORY_CALL,并添加了 Person。
  • 发送通知时,应用在前台运行。

如果上述条件均不满足,系统就会显示通知而不显示气泡。

最佳做法

  • 气泡会占用屏幕空间并遮盖其他应用内容。仅当非常需要显示气泡(例如对于进行中的通信)或用户明确要求为某些内容显示气泡时,才将通知发送为气泡。
  • 请注意,用户可以停用气泡。在这种情况下,气泡通知会显示为一般通知。您应该始终确保您的气泡通知也可以作为一般通知使用。
  • 从气泡启动的进程(例如 activity 和对话框)会显示在气泡容器中。这意味着气泡可以有任务堆栈。如果您的气泡中有很多功能或导航,情况就会变得很复杂。建议您尽量让功能保持具体且简明。
  • 确保在气泡 Activity 中替换 onBackPressed 时调用 super.onBackPressed;否则,气泡可能会无法正常运行。
  • 当气泡在收起后收到更新的消息时,气泡会显示一个标志图标,表示有未读消息。当用户在关联的应用中打开消息时,请按以下步骤操作:
    • 更新 BubbleMetadata 以抑制通知的显示。调用 BubbleMetadata.Builder.setSupressNotification()。这会移除标志图标以表示用户处理过信息。
    • 将 Notification.Builder.setOnlyAlertOnce() 设置为 true 可禁止涉及 BubbleMetadata 更新的声音或振动。

示例应用

People 示例应用是一个使用气泡的简单对话式应用。出于演示目的,此应用使用聊天机器人。在真实的应用中,气泡应仅用于人类发送的消息,而不用于聊天机器人发送的消息。

气泡是一种特殊类型的内容,可以“漂浮”在其他应用程序或系统 UI 之上。
可以展开气泡以显示更多内容。
控制器管理屏幕上气泡的添加、移除和可见状态。

布局分析

Bubbles原理解析_第1张图片
Bubbles原理解析_第2张图片

Bubbles原理解析_第3张图片

在根布局BubbleStackView添加时先创建添加BubbleExpandedView并初始化添加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);
        } ......
    }

左侧页面的两个BadgedImageView加载和BubbleExpandedView添加

        @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 */);
            }

添加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);
        }
    }

代码分析

窗口类型:应用程序覆盖窗口显示在所有活动窗口上方({@link #FIRST_APPLICATION_WINDOW} 和 {@link #LAST_APPLICATION_WINDOW} 之间的类型)但在状态栏或 IME 等关键系统窗口下方。
系统可以随时更改这些窗口的位置、大小或可见性,以减少用户的视觉混乱并管理资源。
需要 {@link android.Manifest.permission#SYSTEM_ALERT_WINDOW} 权限。
系统将调整具有这种窗口类型的进程的重要性,以减少低内存杀手杀死它们的机会。
在多用户系统中,仅在拥有用户的屏幕上显示。

public static final int TYPE_APPLICATION_OVERLAY = FIRST_SYSTEM_WINDOW + 38;

窗口标志:此窗口永远不会获得按键输入焦点,因此用户无法向其发送按键或其他按钮事件。 那些将转到它后面的任何可聚焦窗口。 此标志还将启用 {@link #FLAG_NOT_TOUCH_MODAL},无论是否明确设置。
设置此标志还意味着窗口不需要与软输入法交互,因此它将独立于任何活动输入法进行 Z 排序和定位(通常这意味着它在输入法之上获得 Z 排序, 因此它可以使用全屏显示内容并在需要时覆盖输入法。您可以使用 {@link #FLAG_ALT_FOCUSABLE_IM} 修改此行为。

public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;

窗口标志:即使此窗口可聚焦(其 {@link #FLAG_NOT_FOCUSABLE} 未设置),也允许将窗口外的任何指针事件发送到其后面的窗口。 否则它将自己消耗所有指针事件,无论它们是否在窗口内。

public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;

指定窗口应被视为受信任的系统覆盖。 在考虑输入调度期间窗口是否被遮挡时,可信系统覆盖将被忽略。 需要 {@link android.Manifest.permission#INTERNAL_SYSTEM_WINDOW} 权限。
{@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;
}

指定此窗口在布局期间应避免重叠的插图类型。
@param 类型{@link WindowInsets.Type} 此窗口应避免的插入。
该对象的初始值包括所有系统栏。

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

{@link #softInputMode} 的调整选项:设置为允许在显示输入法时调整窗口大小,使其内容不被输入法覆盖。 这不能与 {@link #SOFT_INPUT_ADJUST_PAN} 结合使用; 如果这些都没有设置,那么系统将根据窗口的内容尝试选择一个或另一个。 如果窗口的布局参数标志包括 {@link #FLAG_FULLSCREEN},{@link #softInputMode} 的这个值将被忽略; 窗口不会调整大小,但会保持全屏。
@deprecated 使用 {@code false} 调用 {@link Window#setDecorFitsSystemWindows(boolean)} 并在适合类型 {@link Type#ime()} 的插入的根内容视图上安装 {@link OnApplyWindowInsetsListener}。

@Deprecated
public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;

始终允许窗口延伸到屏幕所有边缘的 {@link DisplayCutout} 区域。
窗口必须确保没有重要内容与 {@link DisplayCutout} 重叠。
在此模式下,无论窗口是否隐藏系统栏,窗口都会在纵向和横向显示的所有边缘上的切口下延伸。

public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS = 3;
添加根布局BubbleStackView

添加全屏的气泡窗口到屏幕上,mStackView是一个FrameLayout。

BubbleStackView 是在第一次添加 Bubble 时通过此方法延迟创建的。 此方法初始化堆栈视图并将其添加到窗口管理器。

    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();
        }
    }
添加并显示Task

View that can display a task.

  1. 已完成初始化,直接将Task对应的surfacecontrol挂在TaskView的surfacecontrol下面并显示
  2. 否则,先启动对应的activity完成初始化,等Task创建完成在onTaskAppeared方法中完成1中操作
    @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();
        });
    }

展开气泡视图的容器,处理呈现插入符号和设置图标。

 @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;
}

Bubbles原理解析_第4张图片

    @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();
        }

onTaskAppeared回调的时序图:
Bubbles原理解析_第5张图片
Bubbles原理解析_第6张图片

移除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();
    }

你可能感兴趣的:(WMS,android)