diff --git a/quickstep/res/layout/bubble_view.xml b/quickstep/res/layout/bubble_view.xml new file mode 100644 index 0000000000..0b1ed9f343 --- /dev/null +++ b/quickstep/res/layout/bubble_view.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + \ 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 0000000000..64fc4dfa3d --- /dev/null +++ b/quickstep/res/layout/bubblebar_item_view.xml @@ -0,0 +1,21 @@ + + + diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index 3df5d57b55..cdb3b1ccb8 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -344,6 +344,19 @@ 30dp + + 72dp + 55dp + @dimen/transient_taskbar_stashed_height + @dimen/taskbar_stashed_handle_height + 8dp + + 50dp + 24dp + 12dp + 3dp + 1dp + 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 0000000000..667c6f5b6c --- /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 0000000000..b1633e7156 --- /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 0000000000..07daf065ab --- /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. + *

+ * 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. + *

+ * 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 + *

+ * 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. + *

+ * 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 viewOrder) { + if (isExpanded()) { + mReorderRunnable = () -> doReorder(viewOrder); + } else { + doReorder(viewOrder); + } + } + + // TODO: (b/273592694) animate it + private void doReorder(List 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 0000000000..deac42fa26 --- /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 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 newOrder) { + List 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 0000000000..e92d4fbe07 --- /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 0000000000..e22e63aa36 --- /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 + "}"; + } +}