Merge "Creates BubbleBarView & BubbleBarViewController & friends" into udc-dev
This commit is contained in:
@@ -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>
|
||||
@@ -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"/>
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 + "}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user