blob: bb82d19734d8ac065becb97f02d1b2a1ba19bd13 [file] [log] [blame]
/*
* Copyright (C) 2021 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;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.SystemProperties;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.ColorUtils;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.icons.ThemedIconDrawable;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.LauncherBindableItemsContainer;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.AllAppsButton;
import com.android.launcher3.views.DoubleShadowBubbleTextView;
import java.util.function.Predicate;
/**
* Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps.
*/
public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconParent, Insettable {
private static final String TAG = TaskbarView.class.getSimpleName();
private static final float TASKBAR_BACKGROUND_LUMINANCE = 0.30f;
public int mThemeIconsBackground;
private final int[] mTempOutLocation = new int[2];
private final Rect mIconLayoutBounds = new Rect();
private final int mIconTouchSize;
private final int mItemMarginLeftRight;
private final int mItemPadding;
private final TaskbarActivityContext mActivityContext;
// Initialized in init.
private TaskbarViewController.TaskbarViewCallbacks mControllerCallbacks;
private View.OnClickListener mIconClickListener;
private View.OnLongClickListener mIconLongClickListener;
// Prevents dispatching touches to children if true
private boolean mTouchEnabled = true;
// Only non-null when the corresponding Folder is open.
private @Nullable FolderIcon mLeaveBehindFolderIcon;
// Only non-null when device supports having an All Apps button.
private @Nullable AllAppsButton mAllAppsButton;
private View mQsb;
// Only non-null when device supports having a floating task.
private @Nullable BubbleTextView mFloatingTaskButton;
private @Nullable Intent mFloatingTaskIntent;
private static final boolean FLOATING_TASKS_ENABLED =
SystemProperties.getBoolean("persist.wm.debug.floating_tasks", false);
public TaskbarView(@NonNull Context context) {
this(context, null);
}
public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mActivityContext = ActivityContext.lookupContext(context);
Resources resources = getResources();
mIconTouchSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_touch_size);
int actualMargin = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
int actualIconSize = mActivityContext.getDeviceProfile().iconSizePx;
// We layout the icons to be of mIconTouchSize in width and height
mItemMarginLeftRight = actualMargin - (mIconTouchSize - actualIconSize) / 2;
mItemPadding = (mIconTouchSize - actualIconSize) / 2;
// Needed to draw folder leave-behind when opening one.
setWillNotDraw(false);
mThemeIconsBackground = calculateThemeIconsBackground();
if (FeatureFlags.ENABLE_ALL_APPS_IN_TASKBAR.get()) {
mAllAppsButton = new AllAppsButton(context);
mAllAppsButton.setLayoutParams(
new ViewGroup.LayoutParams(mIconTouchSize, mIconTouchSize));
mAllAppsButton.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
}
// TODO: Disable touch events on QSB otherwise it can crash.
mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
if (FLOATING_TASKS_ENABLED) {
mFloatingTaskIntent = FloatingTaskIntentResolver.getIntent(context);
if (mFloatingTaskIntent != null) {
mFloatingTaskButton = new LaunchFloatingTaskButton(context);
mFloatingTaskButton.setLayoutParams(
new ViewGroup.LayoutParams(mIconTouchSize, mIconTouchSize));
mFloatingTaskButton.setPadding(mItemPadding, mItemPadding, mItemPadding,
mItemPadding);
} else {
Log.d(TAG, "Floating tasks is enabled but no intent was found!");
}
}
}
private int getColorWithGivenLuminance(int color, float luminance) {
float[] colorHSL = new float[3];
ColorUtils.colorToHSL(color, colorHSL);
colorHSL[2] = luminance;
return ColorUtils.HSLToColor(colorHSL);
}
private int calculateThemeIconsBackground() {
int color = ThemedIconDrawable.getColors(mContext)[0];
if (Utilities.isDarkTheme(mContext)) {
return getColorWithGivenLuminance(color, TASKBAR_BACKGROUND_LUMINANCE);
}
return color;
}
protected void init(TaskbarViewController.TaskbarViewCallbacks callbacks) {
mControllerCallbacks = callbacks;
mIconClickListener = mControllerCallbacks.getIconOnClickListener();
mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener();
setOnLongClickListener(mControllerCallbacks.getBackgroundOnLongClickListener());
if (mAllAppsButton != null) {
mAllAppsButton.setOnClickListener(mControllerCallbacks.getAllAppsButtonClickListener());
}
if (mFloatingTaskButton != null) {
mFloatingTaskButton.setOnClickListener(
mControllerCallbacks.getFloatingTaskButtonListener(mFloatingTaskIntent));
}
}
private void removeAndRecycle(View view) {
removeView(view);
view.setOnClickListener(null);
view.setOnLongClickListener(null);
if (!(view.getTag() instanceof FolderInfo)) {
mActivityContext.getViewCache().recycleView(view.getSourceLayoutResId(), view);
}
view.setTag(null);
}
/**
* Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
*/
protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
int nextViewIndex = 0;
int numViewsAnimated = 0;
if (mAllAppsButton != null) {
removeView(mAllAppsButton);
}
removeView(mQsb);
if (mFloatingTaskButton != null) {
removeView(mFloatingTaskButton);
}
for (int i = 0; i < hotseatItemInfos.length; i++) {
ItemInfo hotseatItemInfo = hotseatItemInfos[i];
if (hotseatItemInfo == null) {
continue;
}
// Replace any Hotseat views with the appropriate type if it's not already that type.
final int expectedLayoutResId;
boolean isFolder = false;
if (hotseatItemInfo.isPredictedItem()) {
expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
} else if (hotseatItemInfo instanceof FolderInfo) {
expectedLayoutResId = R.layout.folder_icon;
isFolder = true;
} else {
expectedLayoutResId = R.layout.taskbar_app_icon;
}
View hotseatView = null;
while (nextViewIndex < getChildCount()) {
hotseatView = getChildAt(nextViewIndex);
// see if the view can be reused
if ((hotseatView.getSourceLayoutResId() != expectedLayoutResId)
|| (isFolder && (hotseatView.getTag() != hotseatItemInfo))) {
// Unlike for BubbleTextView, we can't reapply a new FolderInfo after inflation,
// so if the info changes we need to reinflate. This should only happen if a new
// folder is dragged to the position that another folder previously existed.
removeAndRecycle(hotseatView);
hotseatView = null;
} else {
// View found
break;
}
}
if (hotseatView == null) {
if (isFolder) {
FolderInfo folderInfo = (FolderInfo) hotseatItemInfo;
FolderIcon folderIcon = FolderIcon.inflateFolderAndIcon(expectedLayoutResId,
mActivityContext, this, folderInfo);
folderIcon.setTextVisible(false);
hotseatView = folderIcon;
} else {
hotseatView = inflate(expectedLayoutResId);
}
LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize);
hotseatView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
addView(hotseatView, nextViewIndex, lp);
}
// Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
if (hotseatView instanceof BubbleTextView
&& hotseatItemInfo instanceof WorkspaceItemInfo) {
BubbleTextView btv = (BubbleTextView) hotseatView;
WorkspaceItemInfo workspaceInfo = (WorkspaceItemInfo) hotseatItemInfo;
boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo);
btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated);
if (animate) {
numViewsAnimated++;
}
}
setClickAndLongClickListenersForIcon(hotseatView);
nextViewIndex++;
}
// Remove remaining views
while (nextViewIndex < getChildCount()) {
removeAndRecycle(getChildAt(nextViewIndex));
}
if (mAllAppsButton != null) {
int index = Utilities.isRtl(getResources()) ? 0 : getChildCount();
addView(mAllAppsButton, index);
}
if (mActivityContext.getDeviceProfile().isQsbInline) {
addView(mQsb, Utilities.isRtl(getResources()) ? getChildCount() : 0);
// Always set QSB to invisible after re-adding.
mQsb.setVisibility(View.INVISIBLE);
}
if (mFloatingTaskButton != null) {
int index = Utilities.isRtl(getResources()) ? 0 : getChildCount();
addView(mFloatingTaskButton, index);
}
mThemeIconsBackground = calculateThemeIconsBackground();
setThemedIconsBackgroundColor(mThemeIconsBackground);
}
/**
* Traverse all the child views and change the background of themeIcons
**/
public void setThemedIconsBackgroundColor(int color) {
for (View icon : getIconViews()) {
if (icon instanceof DoubleShadowBubbleTextView) {
DoubleShadowBubbleTextView textView = ((DoubleShadowBubbleTextView) icon);
if (textView.getIcon() != null
&& textView.getIcon() instanceof ThemedIconDrawable) {
((ThemedIconDrawable) textView.getIcon()).changeBackgroundColor(color);
}
}
}
}
/**
* Sets OnClickListener and OnLongClickListener for the given view.
*/
public void setClickAndLongClickListenersForIcon(View icon) {
icon.setOnClickListener(mIconClickListener);
icon.setOnLongClickListener(mIconLongClickListener);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int count = getChildCount();
int countExcludingQsb = count;
DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
if (deviceProfile.isQsbInline) {
countExcludingQsb--;
}
int spaceNeeded = countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize);
int navSpaceNeeded = deviceProfile.hotseatBarEndOffset;
boolean layoutRtl = isLayoutRtl();
int iconEnd = right - (right - left - spaceNeeded) / 2;
boolean needMoreSpaceForNav = layoutRtl ?
navSpaceNeeded > (iconEnd - spaceNeeded) :
iconEnd > (right - navSpaceNeeded);
if (needMoreSpaceForNav) {
int offset = layoutRtl ?
navSpaceNeeded - (iconEnd - spaceNeeded) :
(right - navSpaceNeeded) - iconEnd;
iconEnd += offset;
}
// Layout the children
mIconLayoutBounds.right = iconEnd;
mIconLayoutBounds.top = (bottom - top - mIconTouchSize) / 2;
mIconLayoutBounds.bottom = mIconLayoutBounds.top + mIconTouchSize;
for (int i = count; i > 0; i--) {
View child = getChildAt(i - 1);
if (child == mQsb) {
int qsbStart;
int qsbEnd;
if (layoutRtl) {
qsbStart = iconEnd + mItemMarginLeftRight;
qsbEnd = qsbStart + deviceProfile.hotseatQsbWidth;
} else {
qsbEnd = iconEnd - mItemMarginLeftRight;
qsbStart = qsbEnd - deviceProfile.hotseatQsbWidth;
}
int qsbTop = (bottom - top - deviceProfile.hotseatQsbHeight) / 2;
int qsbBottom = qsbTop + deviceProfile.hotseatQsbHeight;
child.layout(qsbStart, qsbTop, qsbEnd, qsbBottom);
} else {
iconEnd -= mItemMarginLeftRight;
int iconStart = iconEnd - mIconTouchSize;
child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom);
iconEnd = iconStart - mItemMarginLeftRight;
}
}
mIconLayoutBounds.left = iconEnd;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!mTouchEnabled) {
return true;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mTouchEnabled) {
return true;
}
if (mIconLayoutBounds.left <= event.getX() && event.getX() <= mIconLayoutBounds.right) {
// Don't allow long pressing between icons, or above/below them.
return true;
}
if (mControllerCallbacks.onTouchEvent(event)) {
int oldAction = event.getAction();
try {
event.setAction(MotionEvent.ACTION_CANCEL);
return super.onTouchEvent(event);
} finally {
event.setAction(oldAction);
}
}
return super.onTouchEvent(event);
}
public void setTouchesEnabled(boolean touchEnabled) {
this.mTouchEnabled = touchEnabled;
}
/**
* Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's
* touch bounds.
*/
public boolean isEventOverAnyItem(MotionEvent ev) {
getLocationOnScreen(mTempOutLocation);
int xInOurCoordinates = (int) ev.getX() - mTempOutLocation[0];
int yInOurCoorindates = (int) ev.getY() - mTempOutLocation[1];
return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates);
}
public Rect getIconLayoutBounds() {
return mIconLayoutBounds;
}
/**
* Returns the app icons currently shown in the taskbar.
*/
public View[] getIconViews() {
final int count = getChildCount();
View[] icons = new View[count];
for (int i = 0; i < count; i++) {
icons[i] = getChildAt(i);
}
return icons;
}
/**
* Returns the all apps button in the taskbar.
*/
public View getAllAppsButtonView() {
return mAllAppsButton;
}
/**
* Returns the QSB in the taskbar.
*/
public View getQsb() {
return mQsb;
}
// FolderIconParent implemented methods.
@Override
public void drawFolderLeaveBehindForIcon(FolderIcon child) {
mLeaveBehindFolderIcon = child;
invalidate();
}
@Override
public void clearFolderLeaveBehind(FolderIcon child) {
mLeaveBehindFolderIcon = null;
invalidate();
}
// End FolderIconParent implemented methods.
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mLeaveBehindFolderIcon != null) {
canvas.save();
canvas.translate(mLeaveBehindFolderIcon.getLeft(), mLeaveBehindFolderIcon.getTop());
mLeaveBehindFolderIcon.getFolderBackground().drawLeaveBehind(canvas);
canvas.restore();
}
}
private View inflate(@LayoutRes int layoutResId) {
return mActivityContext.getViewCache().getView(layoutResId, mActivityContext, this);
}
@Override
public void setInsets(Rect insets) {
// Ignore, we just implement Insettable to draw behind system insets.
}
public boolean areIconsVisible() {
// Consider the overall visibility
return getVisibility() == VISIBLE;
}
/**
* Maps {@code op} over all the child views.
*/
public void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) {
// map over all the shortcuts on the taskbar
for (int i = 0; i < getChildCount(); i++) {
View item = getChildAt(i);
if (op.evaluate((ItemInfo) item.getTag(), item)) {
return;
}
}
}
/**
* Finds the first icon to match one of the given matchers, from highest to lowest priority.
* @return The first match, or All Apps button if no match was found.
*/
public View getFirstMatch(Predicate<ItemInfo>... matchers) {
for (Predicate<ItemInfo> matcher : matchers) {
for (int i = 0; i < getChildCount(); i++) {
View item = getChildAt(i);
if (!(item.getTag() instanceof ItemInfo)) {
// Should only happen for All Apps button.
continue;
}
ItemInfo info = (ItemInfo) item.getTag();
if (matcher.test(info)) {
return item;
}
}
}
return mAllAppsButton;
}
}