2020-12-22 12:40:09 -06:00
|
|
|
/*
|
|
|
|
|
* 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;
|
2021-01-11 14:54:23 -06:00
|
|
|
import android.content.res.Resources;
|
2021-02-10 12:35:28 -08:00
|
|
|
import android.graphics.Canvas;
|
2021-03-22 14:43:58 -07:00
|
|
|
import android.graphics.Rect;
|
2020-12-22 12:40:09 -06:00
|
|
|
import android.util.AttributeSet;
|
2021-01-22 11:49:11 -08:00
|
|
|
import android.view.MotionEvent;
|
2021-01-11 14:54:23 -06:00
|
|
|
import android.view.View;
|
2021-05-25 14:35:01 -07:00
|
|
|
import android.widget.FrameLayout;
|
2020-12-22 12:40:09 -06:00
|
|
|
|
2021-01-11 14:54:23 -06:00
|
|
|
import androidx.annotation.LayoutRes;
|
2020-12-22 12:40:09 -06:00
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
|
|
2021-01-11 14:54:23 -06:00
|
|
|
import com.android.launcher3.BubbleTextView;
|
2021-03-22 14:43:58 -07:00
|
|
|
import com.android.launcher3.Insettable;
|
2021-01-11 14:54:23 -06:00
|
|
|
import com.android.launcher3.R;
|
2021-02-02 17:12:08 -08:00
|
|
|
import com.android.launcher3.folder.FolderIcon;
|
|
|
|
|
import com.android.launcher3.model.data.FolderInfo;
|
2021-01-11 14:54:23 -06:00
|
|
|
import com.android.launcher3.model.data.ItemInfo;
|
|
|
|
|
import com.android.launcher3.model.data.WorkspaceItemInfo;
|
2021-05-25 14:35:01 -07:00
|
|
|
import com.android.launcher3.uioverrides.ApiWrapper;
|
2021-02-02 17:12:08 -08:00
|
|
|
import com.android.launcher3.views.ActivityContext;
|
2021-01-11 14:54:23 -06:00
|
|
|
|
2020-12-22 12:40:09 -06:00
|
|
|
/**
|
|
|
|
|
* Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps.
|
|
|
|
|
*/
|
2021-05-25 14:35:01 -07:00
|
|
|
public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconParent, Insettable {
|
2020-12-23 16:12:18 -06:00
|
|
|
|
2021-01-22 18:45:04 -08:00
|
|
|
private final int[] mTempOutLocation = new int[2];
|
2021-01-11 14:54:23 -06:00
|
|
|
|
2021-05-25 14:35:01 -07:00
|
|
|
private final Rect mIconLayoutBounds = new Rect();
|
|
|
|
|
private final int mIconTouchSize;
|
2021-05-20 20:18:47 +00:00
|
|
|
private final int mItemMarginLeftRight;
|
2021-05-25 14:35:01 -07:00
|
|
|
private final int mItemPadding;
|
2021-05-20 20:18:47 +00:00
|
|
|
|
|
|
|
|
private final TaskbarActivityContext mActivityContext;
|
|
|
|
|
|
2021-05-24 15:47:38 -07:00
|
|
|
// Initialized in init.
|
2021-06-08 20:03:43 -07:00
|
|
|
private TaskbarViewController.TaskbarViewCallbacks mControllerCallbacks;
|
2021-05-20 20:18:47 +00:00
|
|
|
private View.OnClickListener mIconClickListener;
|
|
|
|
|
private View.OnLongClickListener mIconLongClickListener;
|
2021-02-02 17:12:08 -08:00
|
|
|
|
2021-04-16 12:50:22 -07:00
|
|
|
// Prevents dispatching touches to children if true
|
|
|
|
|
private boolean mTouchEnabled = true;
|
2021-01-22 11:49:11 -08:00
|
|
|
|
2021-02-10 12:35:28 -08:00
|
|
|
// Only non-null when the corresponding Folder is open.
|
|
|
|
|
private @Nullable FolderIcon mLeaveBehindFolderIcon;
|
2021-01-22 18:45:04 -08:00
|
|
|
|
2020-12-22 12:40:09 -06:00
|
|
|
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);
|
2021-05-20 20:18:47 +00:00
|
|
|
mActivityContext = ActivityContext.lookupContext(context);
|
2021-01-11 14:54:23 -06:00
|
|
|
|
|
|
|
|
Resources resources = getResources();
|
2021-01-22 11:49:11 -08:00
|
|
|
mIconTouchSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_touch_size);
|
2021-05-20 20:18:47 +00:00
|
|
|
|
2021-05-25 14:35:01 -07:00
|
|
|
int actualMargin = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
|
|
|
|
|
int actualIconSize = mActivityContext.getDeviceProfile().iconSizePx;
|
2021-01-11 14:54:23 -06:00
|
|
|
|
2021-05-25 14:35:01 -07:00
|
|
|
// We layout the icons to be of mIconTouchSize in width and height
|
|
|
|
|
mItemMarginLeftRight = actualMargin - (mIconTouchSize - actualIconSize) / 2;
|
|
|
|
|
mItemPadding = (mIconTouchSize - actualIconSize) / 2;
|
2021-08-24 17:06:41 -07:00
|
|
|
|
|
|
|
|
// Needed to draw folder leave-behind when opening one.
|
|
|
|
|
setWillNotDraw(false);
|
2021-05-20 09:50:23 +00:00
|
|
|
}
|
2021-05-05 14:04:11 -07:00
|
|
|
|
2021-06-08 20:03:43 -07:00
|
|
|
protected void init(TaskbarViewController.TaskbarViewCallbacks callbacks) {
|
|
|
|
|
mControllerCallbacks = callbacks;
|
2021-06-01 16:54:07 -07:00
|
|
|
mIconClickListener = mControllerCallbacks.getIconOnClickListener();
|
|
|
|
|
mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener();
|
|
|
|
|
|
|
|
|
|
setOnLongClickListener(mControllerCallbacks.getBackgroundOnLongClickListener());
|
2021-01-11 14:54:23 -06:00
|
|
|
}
|
|
|
|
|
|
2021-06-14 11:40:10 -07:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-11 14:54:23 -06:00
|
|
|
/**
|
|
|
|
|
* Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
|
|
|
|
|
*/
|
|
|
|
|
protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
|
2021-06-14 11:40:10 -07:00
|
|
|
int nextViewIndex = 0;
|
2021-08-25 09:18:41 -07:00
|
|
|
int numViewsAnimated = 0;
|
2021-06-14 11:40:10 -07:00
|
|
|
|
2021-01-11 14:54:23 -06:00
|
|
|
for (int i = 0; i < hotseatItemInfos.length; i++) {
|
2021-05-25 14:35:01 -07:00
|
|
|
ItemInfo hotseatItemInfo = hotseatItemInfos[i];
|
2021-06-14 11:40:10 -07:00
|
|
|
if (hotseatItemInfo == null) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2021-01-11 14:54:23 -06:00
|
|
|
|
|
|
|
|
// Replace any Hotseat views with the appropriate type if it's not already that type.
|
|
|
|
|
final int expectedLayoutResId;
|
2021-02-02 17:12:08 -08:00
|
|
|
boolean isFolder = false;
|
2021-06-14 11:40:10 -07:00
|
|
|
if (hotseatItemInfo.isPredictedItem()) {
|
2021-01-11 14:54:23 -06:00
|
|
|
expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
|
2021-02-02 17:12:08 -08:00
|
|
|
} else if (hotseatItemInfo instanceof FolderInfo) {
|
|
|
|
|
expectedLayoutResId = R.layout.folder_icon;
|
|
|
|
|
isFolder = true;
|
2021-01-11 14:54:23 -06:00
|
|
|
} else {
|
|
|
|
|
expectedLayoutResId = R.layout.taskbar_app_icon;
|
|
|
|
|
}
|
2021-06-14 11:40:10 -07:00
|
|
|
|
|
|
|
|
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);
|
2021-06-15 14:49:28 -07:00
|
|
|
hotseatView = null;
|
2021-06-14 11:40:10 -07:00
|
|
|
} else {
|
|
|
|
|
// View found
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hotseatView == null) {
|
2021-02-02 17:12:08 -08:00
|
|
|
if (isFolder) {
|
|
|
|
|
FolderInfo folderInfo = (FolderInfo) hotseatItemInfo;
|
|
|
|
|
FolderIcon folderIcon = FolderIcon.inflateFolderAndIcon(expectedLayoutResId,
|
2021-05-20 20:18:47 +00:00
|
|
|
mActivityContext, this, folderInfo);
|
2021-02-02 17:12:08 -08:00
|
|
|
folderIcon.setTextVisible(false);
|
|
|
|
|
hotseatView = folderIcon;
|
|
|
|
|
} else {
|
|
|
|
|
hotseatView = inflate(expectedLayoutResId);
|
|
|
|
|
}
|
2021-05-25 14:35:01 -07:00
|
|
|
LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize);
|
|
|
|
|
hotseatView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
|
2021-06-14 11:40:10 -07:00
|
|
|
addView(hotseatView, nextViewIndex, lp);
|
2021-01-11 14:54:23 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
|
|
|
|
|
if (hotseatView instanceof BubbleTextView
|
|
|
|
|
&& hotseatItemInfo instanceof WorkspaceItemInfo) {
|
2021-08-25 09:18:41 -07:00
|
|
|
BubbleTextView btv = (BubbleTextView) hotseatView;
|
|
|
|
|
WorkspaceItemInfo workspaceInfo = (WorkspaceItemInfo) hotseatItemInfo;
|
|
|
|
|
|
|
|
|
|
boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo);
|
|
|
|
|
btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated);
|
|
|
|
|
if (animate) {
|
|
|
|
|
numViewsAnimated++;
|
|
|
|
|
}
|
2021-01-11 14:54:23 -06:00
|
|
|
}
|
2021-06-14 11:40:10 -07:00
|
|
|
setClickAndLongClickListenersForIcon(hotseatView);
|
|
|
|
|
nextViewIndex++;
|
|
|
|
|
}
|
|
|
|
|
// Remove remaining views
|
|
|
|
|
while (nextViewIndex < getChildCount()) {
|
|
|
|
|
removeAndRecycle(getChildAt(nextViewIndex));
|
2021-01-11 14:54:23 -06:00
|
|
|
}
|
2021-01-27 16:27:15 -08:00
|
|
|
}
|
|
|
|
|
|
2021-06-08 20:03:43 -07:00
|
|
|
/**
|
|
|
|
|
* Sets OnClickListener and OnLongClickListener for the given view.
|
|
|
|
|
*/
|
|
|
|
|
public void setClickAndLongClickListenersForIcon(View icon) {
|
|
|
|
|
icon.setOnClickListener(mIconClickListener);
|
|
|
|
|
icon.setOnLongClickListener(mIconLongClickListener);
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-25 14:35:01 -07:00
|
|
|
@Override
|
|
|
|
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
|
|
|
int count = getChildCount();
|
2021-06-14 11:40:10 -07:00
|
|
|
int spaceNeeded = count * (mItemMarginLeftRight * 2 + mIconTouchSize);
|
2021-07-19 12:53:28 -07:00
|
|
|
int navSpaceNeeded = ApiWrapper.getHotseatEndOffset(getContext());
|
|
|
|
|
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;
|
2021-05-24 15:47:38 -07:00
|
|
|
}
|
2021-05-25 14:35:01 -07:00
|
|
|
// Layout the children
|
2021-07-19 12:53:28 -07:00
|
|
|
mIconLayoutBounds.right = iconEnd;
|
2021-05-25 14:35:01 -07:00
|
|
|
mIconLayoutBounds.top = (bottom - top - mIconTouchSize) / 2;
|
|
|
|
|
mIconLayoutBounds.bottom = mIconLayoutBounds.top + mIconTouchSize;
|
2021-07-19 12:53:28 -07:00
|
|
|
for (int i = count; i > 0; i--) {
|
|
|
|
|
View child = getChildAt(i - 1);
|
|
|
|
|
iconEnd -= mItemMarginLeftRight;
|
|
|
|
|
int iconStart = iconEnd - mIconTouchSize;
|
2021-06-14 11:40:10 -07:00
|
|
|
child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom);
|
2021-07-19 12:53:28 -07:00
|
|
|
iconEnd = iconStart - mItemMarginLeftRight;
|
2021-05-25 14:35:01 -07:00
|
|
|
}
|
2021-07-19 12:53:28 -07:00
|
|
|
mIconLayoutBounds.left = iconEnd;
|
2021-03-03 17:33:28 -08:00
|
|
|
}
|
|
|
|
|
|
2021-04-16 12:50:22 -07:00
|
|
|
@Override
|
|
|
|
|
public boolean dispatchTouchEvent(MotionEvent ev) {
|
|
|
|
|
if (!mTouchEnabled) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return super.dispatchTouchEvent(ev);
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-16 12:15:35 -10:00
|
|
|
@Override
|
|
|
|
|
public boolean onTouchEvent(MotionEvent event) {
|
|
|
|
|
if (!mTouchEnabled) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
mControllerCallbacks.onTouchEvent(event);
|
|
|
|
|
return super.onTouchEvent(event);
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-16 12:50:22 -07:00
|
|
|
public void setTouchesEnabled(boolean touchEnabled) {
|
|
|
|
|
this.mTouchEnabled = touchEnabled;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-22 18:45:04 -08:00
|
|
|
/**
|
|
|
|
|
* Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's
|
|
|
|
|
* touch bounds.
|
|
|
|
|
*/
|
|
|
|
|
public boolean isEventOverAnyItem(MotionEvent ev) {
|
|
|
|
|
getLocationOnScreen(mTempOutLocation);
|
2021-05-25 14:35:01 -07:00
|
|
|
int xInOurCoordinates = (int) ev.getX() - mTempOutLocation[0];
|
|
|
|
|
int yInOurCoorindates = (int) ev.getY() - mTempOutLocation[1];
|
|
|
|
|
return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates);
|
2021-01-22 18:45:04 -08:00
|
|
|
}
|
|
|
|
|
|
2021-06-01 16:54:07 -07:00
|
|
|
public Rect getIconLayoutBounds() {
|
|
|
|
|
return mIconLayoutBounds;
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-24 10:56:31 -07:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-10 12:35:28 -08:00
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-11 14:54:23 -06:00
|
|
|
private View inflate(@LayoutRes int layoutResId) {
|
2021-06-14 11:40:10 -07:00
|
|
|
return mActivityContext.getViewCache().getView(layoutResId, mActivityContext, this);
|
2021-03-22 14:43:58 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void setInsets(Rect insets) {
|
|
|
|
|
// Ignore, we just implement Insettable to draw behind system insets.
|
2021-01-11 14:54:23 -06:00
|
|
|
}
|
2021-03-25 12:31:23 -07:00
|
|
|
|
2021-05-20 20:18:47 +00:00
|
|
|
public boolean areIconsVisible() {
|
2021-05-25 14:35:01 -07:00
|
|
|
// Consider the overall visibility
|
|
|
|
|
return getVisibility() == VISIBLE;
|
2021-03-25 12:31:23 -07:00
|
|
|
}
|
2020-12-22 12:40:09 -06:00
|
|
|
}
|