Merge "Creates BubbleBarView & BubbleBarViewController & friends" into udc-dev
diff --git a/quickstep/res/layout/bubble_view.xml b/quickstep/res/layout/bubble_view.xml
new file mode 100644
index 0000000..0b1ed9f
--- /dev/null
+++ b/quickstep/res/layout/bubble_view.xml
@@ -0,0 +1,55 @@
+<?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.
+ -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <ImageView
+ android:id="@+id/icon_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:contentDescription="@null" />
+
+ <!--
+ Icon badge size is defined in Launcher3 BaseIconFactory as 0.444 of icon size.
+ Constraint guide starts from left, which means for a badge positioned on the right,
+ percent has to be 1 - 0.444 to have the same effect.
+ -->
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/app_icon_constraint_horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_percent="0.556" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/app_icon_constraint_vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.556" />
+
+ <ImageView
+ android:id="@+id/app_icon_view"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:contentDescription="@null"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="@id/app_icon_constraint_vertical"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="@id/app_icon_constraint_horizontal" />
+
+</merge>
\ No newline at end of file
diff --git a/quickstep/res/layout/bubblebar_item_view.xml b/quickstep/res/layout/bubblebar_item_view.xml
new file mode 100644
index 0000000..64fc4df
--- /dev/null
+++ b/quickstep/res/layout/bubblebar_item_view.xml
@@ -0,0 +1,21 @@
+<?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.launcher3.taskbar.bubbles.BubbleView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/bubble_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 3df5d57..cdb3b1c 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -344,6 +344,19 @@
<!-- Recents overview -->
<dimen name="recents_filter_icon_size">30dp</dimen>
+ <!-- Bubble bar -->
+ <dimen name="bubblebar_size">72dp</dimen>
+ <dimen name="bubblebar_stashed_handle_width">55dp</dimen>
+ <dimen name="bubblebar_stashed_size">@dimen/transient_taskbar_stashed_height</dimen>
+ <dimen name="bubblebar_stashed_handle_height">@dimen/taskbar_stashed_handle_height</dimen>
+ <dimen name="bubblebar_pointer_size">8dp</dimen>
+
+ <dimen name="bubblebar_icon_size">50dp</dimen>
+ <dimen name="bubblebar_badge_size">24dp</dimen>
+ <dimen name="bubblebar_icon_overlap">12dp</dimen>
+ <dimen name="bubblebar_icon_spacing">3dp</dimen>
+ <dimen name="bubblebar_icon_elevation">1dp</dimen>
+
<!-- Launcher splash screen -->
<!-- Note: keep this value in sync with the WindowManager/Shell dimens.xml -->
<!-- starting_surface_exit_animation_window_shift_length -->
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
new file mode 100644
index 0000000..667c6f5
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.launcher3.taskbar.bubbles
+
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorFilter
+import android.graphics.Paint
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.ShapeDrawable
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.Utilities.mapToRange
+import com.android.launcher3.anim.Interpolators
+import com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.wm.shell.common.TriangleShape
+
+/** Drawable for the background of the bubble bar. */
+class BubbleBarBackground(context: TaskbarActivityContext, private val backgroundHeight: Float) :
+ Drawable() {
+
+ private val DARK_THEME_SHADOW_ALPHA = 51f
+ private val LIGHT_THEME_SHADOW_ALPHA = 25f
+
+ private val paint: Paint = Paint()
+ private val pointerSize: Float
+
+ private val shadowAlpha: Float
+ private var shadowBlur = 0f
+ private var keyShadowDistance = 0f
+
+ private var arrowPositionX: Float = 0f
+ private var showingArrow: Boolean = false
+ private var arrowDrawable: ShapeDrawable
+
+ init {
+ paint.color = context.getColor(R.color.taskbar_background)
+ paint.flags = Paint.ANTI_ALIAS_FLAG
+ paint.style = Paint.Style.FILL
+
+ val res = context.resources
+ shadowBlur = res.getDimension(R.dimen.transient_taskbar_shadow_blur)
+ keyShadowDistance = res.getDimension(R.dimen.transient_taskbar_key_shadow_distance)
+ pointerSize = res.getDimension(R.dimen.bubblebar_pointer_size)
+
+ shadowAlpha =
+ if (Utilities.isDarkTheme(context)) DARK_THEME_SHADOW_ALPHA
+ else LIGHT_THEME_SHADOW_ALPHA
+
+ arrowDrawable =
+ ShapeDrawable(TriangleShape.create(pointerSize, pointerSize, /* pointUp= */ true))
+ arrowDrawable.setBounds(0, 0, pointerSize.toInt(), pointerSize.toInt())
+ arrowDrawable.paint.flags = Paint.ANTI_ALIAS_FLAG
+ arrowDrawable.paint.style = Paint.Style.FILL
+ arrowDrawable.paint.color = context.getColor(R.color.taskbar_background)
+ }
+
+ fun showArrow(show: Boolean) {
+ showingArrow = show
+ }
+
+ fun setArrowPosition(x: Float) {
+ arrowPositionX = x
+ }
+
+ /** Draws the background with the given paint and height, on the provided canvas. */
+ override fun draw(canvas: Canvas) {
+ canvas.save()
+
+ // TODO (b/277359345): Should animate the alpha similar to taskbar (see TaskbarDragLayer)
+ // Draw shadows.
+ val newShadowAlpha =
+ mapToRange(paint.alpha.toFloat(), 0f, 255f, 0f, shadowAlpha, Interpolators.LINEAR)
+ paint.setShadowLayer(
+ shadowBlur,
+ 0f,
+ keyShadowDistance,
+ setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha))
+ )
+ arrowDrawable.paint.setShadowLayer(
+ shadowBlur,
+ 0f,
+ keyShadowDistance,
+ setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha))
+ )
+
+ // Draw background.
+ val radius = backgroundHeight / 2f
+ canvas.drawRoundRect(
+ 0f,
+ 0f,
+ canvas.width.toFloat(),
+ canvas.height.toFloat(),
+ radius,
+ radius,
+ paint
+ )
+
+ if (showingArrow) {
+ // Draw arrow.
+ val transX = arrowPositionX - pointerSize / 2f
+ canvas.translate(transX, -pointerSize)
+ arrowDrawable.draw(canvas)
+ }
+
+ canvas.restore()
+ }
+
+ override fun getOpacity(): Int {
+ return paint.alpha
+ }
+
+ override fun setAlpha(alpha: Int) {
+ paint.alpha = alpha
+ }
+
+ override fun setColorFilter(colorFilter: ColorFilter?) {
+ paint.colorFilter = colorFilter
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubble.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubble.kt
new file mode 100644
index 0000000..b1633e7
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubble.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.launcher3.taskbar.bubbles
+
+import android.graphics.Bitmap
+import android.graphics.Path
+import com.android.wm.shell.common.bubbles.BubbleInfo
+
+/** Contains state info about a bubble in the bubble bar as well as presentation information. */
+data class BubbleBarBubble(
+ val info: BubbleInfo,
+ val view: BubbleView,
+ val badge: Bitmap,
+ val icon: Bitmap,
+ val dotColor: Int,
+ val dotPath: Path,
+ val appName: String
+) {
+
+ fun getKey(): String {
+ return info.key
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
new file mode 100644
index 0000000..07daf06
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -0,0 +1,304 @@
+/*
+ * 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.launcher3.taskbar.bubbles;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.views.ActivityContext;
+
+import java.util.List;
+
+/**
+ * The view that holds all the bubble views. Modifying this view should happen through
+ * {@link BubbleBarViewController}. Updates to the bubbles themselves (adds, removes, updates,
+ * selection) should happen through BubbleBarController which is the source of truth
+ * for state information about the bubbles.
+ * <p>
+ * The bubble bar has a couple of visual states:
+ * - stashed as a handle
+ * - unstashed but collapsed, in this state the bar is showing but the bubbles are stacked within it
+ * - unstashed and expanded, in this state the bar is showing and the bubbles are shown in a row
+ * with one of the bubbles being selected. Additionally, WMShell will display the expanded bubble
+ * view above the bar.
+ * <p>
+ * The bubble bar has some behavior related to taskbar:
+ * - When taskbar is unstashed, bubble bar will also become unstashed (but in its "collapsed"
+ * state)
+ * - When taskbar is stashed, bubble bar will also become stashed (unless bubble bar is in its
+ * "expanded" state)
+ * - When bubble bar is in its "expanded" state, taskbar becomes stashed
+ * <p>
+ * If there are no bubbles, the bubble bar and bubble stashed handle are not shown. Additionally
+ * the bubble bar and stashed handle are not shown on lockscreen.
+ * <p>
+ * When taskbar is in persistent or 3 button nav mode, the bubble bar is not available, and instead
+ * the bubbles are shown fully by WMShell in their floating mode.
+ */
+public class BubbleBarView extends FrameLayout {
+
+ private static final String TAG = BubbleBarView.class.getSimpleName();
+
+ // TODO: (b/273594744) calculate the amount of space we have and base the max on that
+ // if it's smaller than 5.
+ private static final int MAX_BUBBLES = 5;
+
+ private final TaskbarActivityContext mActivityContext;
+ private final BubbleBarBackground mBubbleBarBackground;
+
+ // The current bounds of all the bubble bar.
+ private final Rect mBubbleBarBounds = new Rect();
+ // The amount the bubbles overlap when they are stacked in the bubble bar
+ private final float mIconOverlapAmount;
+ // The spacing between the bubbles when they are expanded in the bubble bar
+ private final float mIconSpacing;
+ // The size of a bubble in the bar
+ private final float mIconSize;
+ // The elevation of the bubbles within the bar
+ private final float mBubbleElevation;
+
+ // Whether the bar is expanded (i.e. the bubble activity is being displayed).
+ private boolean mIsBarExpanded = false;
+ // The currently selected bubble view.
+ private BubbleView mSelectedBubbleView;
+ // The click listener when the bubble bar is collapsed.
+ private View.OnClickListener mOnClickListener;
+
+ private final Rect mTempRect = new Rect();
+
+ // We don't reorder the bubbles when they are expanded as it could be jarring for the user
+ // this runnable will be populated with any reordering of the bubbles that should be applied
+ // once they are collapsed.
+ @Nullable
+ private Runnable mReorderRunnable;
+
+ public BubbleBarView(Context context) {
+ this(context, null);
+ }
+
+ public BubbleBarView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mActivityContext = ActivityContext.lookupContext(context);
+
+ mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap);
+ mIconSpacing = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
+ mIconSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
+ mBubbleElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_elevation);
+ setClipToPadding(false);
+
+ mBubbleBarBackground = new BubbleBarBackground(mActivityContext,
+ getResources().getDimensionPixelSize(R.dimen.bubblebar_size));
+ setBackgroundDrawable(mBubbleBarBackground);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ mBubbleBarBounds.left = left;
+ mBubbleBarBounds.top = top;
+ mBubbleBarBounds.right = right;
+ mBubbleBarBounds.bottom = bottom;
+
+ // The bubble bar handle is aligned to the bottom edge of the screen so scale towards that.
+ setPivotX(getWidth());
+ setPivotY(getHeight());
+
+ // Position the views
+ updateChildrenRenderNodeProperties();
+ }
+
+ /**
+ * Returns the bounds of the bubble bar.
+ */
+ public Rect getBubbleBarBounds() {
+ return mBubbleBarBounds;
+ }
+
+ // TODO: (b/273592694) animate it
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (getChildCount() + 1 > MAX_BUBBLES) {
+ removeViewInLayout(getChildAt(getChildCount() - 1));
+ }
+ super.addView(child, index, params);
+ }
+
+ /**
+ * Updates the z order, positions, and badge visibility of the bubble views in the bar based
+ * on the expanded state.
+ */
+ // TODO: (b/273592694) animate it
+ private void updateChildrenRenderNodeProperties() {
+ int bubbleCount = getChildCount();
+ final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
+ for (int i = 0; i < bubbleCount; i++) {
+ BubbleView bv = (BubbleView) getChildAt(i);
+ bv.setTranslationY(ty);
+ if (mIsBarExpanded) {
+ final float tx = i * (mIconSize + mIconSpacing);
+ bv.setTranslationX(tx);
+ bv.setZ(0);
+ bv.showBadge();
+ } else {
+ bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
+ bv.setTranslationX(i * mIconOverlapAmount);
+ if (i > 0) {
+ bv.hideBadge();
+ } else {
+ bv.showBadge();
+ }
+ }
+ }
+ }
+
+ /**
+ * Reorders the views to match the provided list.
+ */
+ public void reorder(List<BubbleView> viewOrder) {
+ if (isExpanded()) {
+ mReorderRunnable = () -> doReorder(viewOrder);
+ } else {
+ doReorder(viewOrder);
+ }
+ }
+
+ // TODO: (b/273592694) animate it
+ private void doReorder(List<BubbleView> viewOrder) {
+ if (!isExpanded()) {
+ for (int i = 0; i < viewOrder.size(); i++) {
+ View child = viewOrder.get(i);
+ if (child != null) {
+ removeViewInLayout(child);
+ addViewInLayout(child, i, child.getLayoutParams());
+ }
+ }
+ updateChildrenRenderNodeProperties();
+ }
+ }
+
+ /**
+ * Sets which bubble view should be shown as selected.
+ */
+ // TODO: (b/273592694) animate it
+ public void setSelectedBubble(BubbleView view) {
+ mSelectedBubbleView = view;
+ updateArrowForSelected();
+ invalidate();
+ }
+
+ private void updateArrowForSelected() {
+ if (mSelectedBubbleView == null) {
+ Log.w(TAG, "trying to update selection arrow without a selected view!");
+ return;
+ }
+ final int index = indexOfChild(mSelectedBubbleView);
+ // Find the center of the bubble when it's expanded, set the arrow position to it.
+ final float tx = getPaddingStart() + index * (mIconSize + mIconSpacing) + mIconSize / 2f;
+ mBubbleBarBackground.setArrowPosition(tx);
+ }
+
+ @Override
+ public void setOnClickListener(View.OnClickListener listener) {
+ mOnClickListener = listener;
+ setOrUnsetClickListener();
+ }
+
+ /**
+ * The click listener used for the bubble view gets added / removed depending on whether
+ * the bar is expanded or collapsed, this updates whether the listener is set based on state.
+ */
+ private void setOrUnsetClickListener() {
+ super.setOnClickListener(mIsBarExpanded ? null : mOnClickListener);
+ }
+
+ /**
+ * Sets whether the bubble bar is expanded or collapsed.
+ */
+ // TODO: (b/273592694) animate it
+ public void setExpanded(boolean isBarExpanded) {
+ if (mIsBarExpanded != isBarExpanded) {
+ mIsBarExpanded = isBarExpanded;
+ updateArrowForSelected();
+ setOrUnsetClickListener();
+ if (!isBarExpanded && mReorderRunnable != null) {
+ mReorderRunnable.run();
+ mReorderRunnable = null;
+ }
+ mBubbleBarBackground.showArrow(mIsBarExpanded);
+ requestLayout(); // trigger layout to reposition views & update size for expansion
+ }
+ }
+
+ /**
+ * Returns whether the bubble bar is expanded.
+ */
+ public boolean isExpanded() {
+ return mIsBarExpanded;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int childCount = getChildCount();
+ final float iconWidth = mIsBarExpanded
+ ? (childCount * (mIconSize + mIconSpacing))
+ : mIconSize + ((childCount - 1) * mIconOverlapAmount);
+ final int totalWidth = (int) iconWidth + getPaddingStart() + getPaddingEnd();
+ setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec));
+
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ measureChild(child, (int) mIconSize, (int) mIconSize);
+ }
+ }
+
+ /**
+ * Returns whether the given MotionEvent, *in screen coordinates*, is within bubble bar
+ * touch bounds.
+ */
+ public boolean isEventOverAnyItem(MotionEvent ev) {
+ if (getVisibility() == View.VISIBLE) {
+ getBoundsOnScreen(mTempRect);
+ return mTempRect.contains((int) ev.getX(), (int) ev.getY());
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (!mIsBarExpanded) {
+ // When the bar is collapsed, all taps on it should expand it.
+ return true;
+ }
+ return super.onInterceptTouchEvent(ev);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
new file mode 100644
index 0000000..deac42f
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -0,0 +1,276 @@
+/*
+ * 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.launcher3.taskbar.bubbles;
+
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.util.MultiPropertyFactory;
+import com.android.launcher3.util.MultiValueAlpha;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Controller for {@link BubbleBarView}. Manages the visibility of the bubble bar as well as
+ * responding to changes in bubble state provided by BubbleBarController.
+ */
+public class BubbleBarViewController {
+
+ private static final String TAG = BubbleBarViewController.class.getSimpleName();
+
+ private final TaskbarActivityContext mActivity;
+ private final BubbleBarView mBarView;
+ private final int mIconSize;
+
+ // Initialized in init.
+ private View.OnClickListener mBubbleClickListener;
+ private View.OnClickListener mBubbleBarClickListener;
+
+ // These are exposed to BubbleStashController to animate for stashing/un-stashing
+ private final MultiValueAlpha mBubbleBarAlpha;
+ private final AnimatedFloat mBubbleBarScale = new AnimatedFloat(this::updateScale);
+ private final AnimatedFloat mBubbleBarTranslationY = new AnimatedFloat(
+ this::updateTranslationY);
+
+ // Modified when swipe up is happening on the bubble bar or task bar.
+ private float mBubbleBarSwipeUpTranslationY;
+
+ // Whether the bar is hidden for a sysui state.
+ private boolean mHiddenForSysui;
+ // Whether the bar is hidden because there are no bubbles.
+ private boolean mHiddenForNoBubbles;
+
+ public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView) {
+ mActivity = activity;
+ mBarView = barView;
+ mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */);
+ mBubbleBarAlpha.setUpdateVisibility(true);
+ mIconSize = activity.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
+ }
+
+ public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
+ mActivity.addOnDeviceProfileChangeListener(dp ->
+ mBarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarHeight
+ );
+ mBarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarHeight;
+ mBubbleBarScale.updateValue(1f);
+ mBubbleClickListener = v -> onBubbleClicked(v);
+ mBubbleBarClickListener = v -> setExpanded(true);
+ mBarView.setOnClickListener(mBubbleBarClickListener);
+ // TODO: when barView layout changes tell taskbarInsetsController the insets have changed.
+ }
+
+ private void onBubbleClicked(View v) {
+ BubbleBarBubble bubble = ((BubbleView) v).getBubble();
+ if (bubble == null) {
+ Log.e(TAG, "bubble click listener, bubble was null");
+ }
+ // TODO: handle the click
+ }
+
+ //
+ // The below animators are exposed to BubbleStashController so it can manage the stashing
+ // animation.
+ //
+
+ public MultiPropertyFactory<View> getBubbleBarAlpha() {
+ return mBubbleBarAlpha;
+ }
+
+ public AnimatedFloat getBubbleBarScale() {
+ return mBubbleBarScale;
+ }
+
+ public AnimatedFloat getBubbleBarTranslationY() {
+ return mBubbleBarTranslationY;
+ }
+
+ /**
+ * Whether the bubble bar is visible or not.
+ */
+ public boolean isBubbleBarVisible() {
+ return mBarView.getVisibility() == VISIBLE;
+ }
+
+ /**
+ * The bounds of the bubble bar.
+ */
+ public Rect getBubbleBarBounds() {
+ return mBarView.getBubbleBarBounds();
+ }
+
+ /**
+ * When the bubble bar is not stashed, it can be collapsed (the icons are in a stack) or
+ * expanded (the icons are in a row). This indicates whether the bubble bar is expanded.
+ */
+ public boolean isExpanded() {
+ return mBarView.isExpanded();
+ }
+
+ /**
+ * Whether the motion event is within the bounds of the bubble bar.
+ */
+ public boolean isEventOverAnyItem(MotionEvent ev) {
+ return mBarView.isEventOverAnyItem(ev);
+ }
+
+ //
+ // Visibility of the bubble bar
+ //
+
+ /**
+ * Returns whether the bubble bar is hidden because there are no bubbles.
+ */
+ public boolean isHiddenForNoBubbles() {
+ return mHiddenForNoBubbles;
+ }
+
+ /**
+ * Sets whether the bubble bar should be hidden because there are no bubbles.
+ */
+ public void setHiddenForBubbles(boolean hidden) {
+ if (mHiddenForNoBubbles != hidden) {
+ mHiddenForNoBubbles = hidden;
+ updateVisibilityForStateChange();
+ }
+ }
+
+ /**
+ * Sets whether the bubble bar should be hidden due to SysUI state (e.g. on lockscreen).
+ */
+ public void setHiddenForSysui(boolean hidden) {
+ if (mHiddenForSysui != hidden) {
+ mHiddenForSysui = hidden;
+ updateVisibilityForStateChange();
+ }
+ }
+
+ // TODO: (b/273592694) animate it
+ private void updateVisibilityForStateChange() {
+ // TODO: check if it's stashed
+ if (!mHiddenForSysui && !mHiddenForNoBubbles) {
+ mBarView.setVisibility(VISIBLE);
+ } else {
+ mBarView.setVisibility(INVISIBLE);
+ }
+ }
+
+ //
+ // Modifying view related properties.
+ //
+
+ /**
+ * Sets the translation of the bubble bar during the swipe up gesture.
+ */
+ public void setTranslationYForSwipe(float transY) {
+ mBubbleBarSwipeUpTranslationY = transY;
+ updateTranslationY();
+ }
+
+ private void updateTranslationY() {
+ mBarView.setTranslationY(mBubbleBarTranslationY.value
+ + mBubbleBarSwipeUpTranslationY);
+ }
+
+ /**
+ * Applies scale properties for the entire bubble bar.
+ */
+ private void updateScale() {
+ float scale = mBubbleBarScale.value;
+ mBarView.setScaleX(scale);
+ mBarView.setScaleY(scale);
+ }
+
+ //
+ // Manipulating the specific bubble views in the bar
+ //
+
+ /**
+ * Removes the provided bubble from the bubble bar.
+ */
+ public void removeBubble(BubbleBarBubble b) {
+ if (b != null) {
+ mBarView.removeView(b.getView());
+ } else {
+ Log.w(TAG, "removeBubble, bubble was null!");
+ }
+ }
+
+ /**
+ * Adds the provided bubble to the bubble bar.
+ */
+ public void addBubble(BubbleBarBubble b) {
+ if (b != null) {
+ mBarView.addView(b.getView(), 0, new FrameLayout.LayoutParams(mIconSize, mIconSize));
+ b.getView().setOnClickListener(mBubbleClickListener);
+ } else {
+ Log.w(TAG, "addBubble, bubble was null!");
+ }
+ }
+
+ /**
+ * Reorders the bubbles based on the provided list.
+ */
+ public void reorderBubbles(List<BubbleBarBubble> newOrder) {
+ List<BubbleView> viewList = newOrder.stream().filter(Objects::nonNull)
+ .map(BubbleBarBubble::getView).toList();
+ mBarView.reorder(viewList);
+ }
+
+ /**
+ * Updates the selected bubble.
+ */
+ public void updateSelectedBubble(BubbleBarBubble newlySelected) {
+ mBarView.setSelectedBubble(newlySelected.getView());
+ }
+
+ /**
+ * Sets whether the bubble bar should be expanded (not unstashed, but have the contents
+ * within it expanded). This method notifies SystemUI that the bubble bar is expanded and
+ * showing a selected bubble. This method should ONLY be called from UI events originating
+ * from Launcher.
+ */
+ public void setExpanded(boolean isExpanded) {
+ if (isExpanded != mBarView.isExpanded()) {
+ mBarView.setExpanded(isExpanded);
+ if (!isExpanded) {
+ // TODO: Tell SysUi to collapse the bubble
+ } else {
+ // TODO: Tell SysUi to show the bubble
+ // TODO: Tell taskbar stash controller to stash without bubbles following
+ }
+ }
+ }
+
+ /**
+ * Sets whether the bubble bar should be expanded. This method is used in response to UI events
+ * from SystemUI.
+ */
+ public void setExpandedFromSysui(boolean isExpanded) {
+ // TODO: Tell bubble bar stash controller to stash or unstash the bubble bar
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
new file mode 100644
index 0000000..e92d4fb
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -0,0 +1,66 @@
+/*
+ * 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.launcher3.taskbar.bubbles;
+
+import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.util.RunnableList;
+
+/**
+ * Hosts various bubble controllers to facilitate passing between one another.
+ */
+public class BubbleControllers {
+
+ public final BubbleBarViewController bubbleBarViewController;
+
+ private final RunnableList mPostInitRunnables = new RunnableList();
+
+ /**
+ * Want to add a new controller? Don't forget to:
+ * * Call init
+ * * Call onDestroy
+ */
+ public BubbleControllers(BubbleBarViewController bubbleBarViewController) {
+ this.bubbleBarViewController = bubbleBarViewController;
+ }
+
+ /**
+ * Initializes all controllers. Note that controllers can now reference each other through this
+ * BubbleControllers instance, but should be careful to only access things that were created
+ * in constructors for now, as some controllers may still be waiting for init().
+ */
+ public void init(TaskbarControllers taskbarControllers) {
+ bubbleBarViewController.init(taskbarControllers, this);
+
+ mPostInitRunnables.executeAllAndDestroy();
+ }
+
+ /**
+ * If all controllers are already initialized, runs the given callback immediately. Otherwise,
+ * queues it to run after calling init() on all controllers. This should likely be used in any
+ * case where one controller is telling another controller to do something inside init().
+ */
+ public void runAfterInit(Runnable runnable) {
+ // If this has been executed in init, it automatically runs adds to it.
+ mPostInitRunnables.add(runnable);
+ }
+
+ /**
+ * Cleans up all controllers.
+ */
+ public void onDestroy() {
+ // TODO
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
new file mode 100644
index 0000000..e22e63a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -0,0 +1,134 @@
+/*
+ * 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.launcher3.taskbar.bubbles;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Outline;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.ImageView;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.icons.IconNormalizer;
+
+// TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
+// TODO: (b/269670235) currently this doesn't show the 'update dot'
+/**
+ * View that displays a bubble icon, along with an app badge on either the left or
+ * right side of the view.
+ */
+public class BubbleView extends ConstraintLayout {
+
+ // TODO: (b/269670235) currently we don't render the 'update dot', this will be used for that.
+ public static final int DEFAULT_PATH_SIZE = 100;
+
+ private final ImageView mBubbleIcon;
+ private final ImageView mAppIcon;
+ private final int mBubbleSize;
+
+ // TODO: (b/273310265) handle RTL
+ private boolean mOnLeft = false;
+
+ private BubbleBarBubble mBubble;
+
+ public BubbleView(Context context) {
+ this(context, null);
+ }
+
+ public BubbleView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public BubbleView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ // We manage positioning the badge ourselves
+ setLayoutDirection(LAYOUT_DIRECTION_LTR);
+
+ LayoutInflater.from(context).inflate(R.layout.bubble_view, this);
+
+ mBubbleSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
+ mBubbleIcon = findViewById(R.id.icon_view);
+ mAppIcon = findViewById(R.id.app_icon_view);
+
+ setFocusable(true);
+ setClickable(true);
+ setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ BubbleView.this.getOutline(outline);
+ }
+ });
+ }
+
+ private void getOutline(Outline outline) {
+ final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize);
+ final int inset = (mBubbleSize - normalizedSize) / 2;
+ outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
+ }
+
+ /** Sets the bubble being rendered in this view. */
+ void setBubble(BubbleBarBubble bubble) {
+ mBubble = bubble;
+ mBubbleIcon.setImageBitmap(bubble.getIcon());
+ mAppIcon.setImageBitmap(bubble.getBadge());
+ }
+
+ /** Returns the bubble being rendered in this view. */
+ @Nullable
+ BubbleBarBubble getBubble() {
+ return mBubble;
+ }
+
+ /** Shows the app badge on this bubble. */
+ void showBadge() {
+ Bitmap appBadgeBitmap = mBubble.getBadge();
+ if (appBadgeBitmap == null) {
+ mAppIcon.setVisibility(GONE);
+ return;
+ }
+
+ int translationX;
+ if (mOnLeft) {
+ translationX = -(mBubble.getIcon().getWidth() - appBadgeBitmap.getWidth());
+ } else {
+ translationX = 0;
+ }
+
+ mAppIcon.setTranslationX(translationX);
+ mAppIcon.setVisibility(VISIBLE);
+ }
+
+ /** Hides the app badge on this bubble. */
+ void hideBadge() {
+ mAppIcon.setVisibility(GONE);
+ }
+
+ @Override
+ public String toString() {
+ return "BubbleView{" + mBubble + "}";
+ }
+}