Folder support in Taskbar

- Add TaskbarActivityContext which allows shared Launcher elements to
  "just work" using existing generic ActivityContext.
- TaskbarContainerView extends BaseDragLayer<TaskbarActivityContext>.
- Inflate FolderIcon and Folder using TaskbarActivityContext to be
  shown in TaskbarContainerView.
- Use TaskbarActivityContext's DeviceProfile to determine icon size
  instead of overriding in styles. This also ensures that normal
  BubbleTextView icons have the same size as FolderIcons.

Test: Place a folder in home screen hotseat, ensure it shows up in
taskbar and can be opened, and that apps inside it can be launched
or dragged.
Bug: 171917176

Change-Id: Ic25d2f84bcd7e3399c88989305ea565497c030d9
This commit is contained in:
Tony Wickham 2021-02-02 17:12:08 -08:00
parent 1906cc33f9
commit 7ba547cd2d
12 changed files with 265 additions and 25 deletions

View File

@ -130,4 +130,5 @@
<dimen name="taskbar_icon_spacing">14dp</dimen>
<dimen name="taskbar_divider_thickness">1dp</dimen>
<dimen name="taskbar_divider_height">24dp</dimen>
<dimen name="taskbar_folder_margin">16dp</dimen>
</resources>

View File

@ -89,6 +89,5 @@
<!-- Icon displayed on the taskbar -->
<style name="BaseIcon.Workspace.Taskbar" >
<item name="iconDisplay">taskbar</item>
<item name="iconSizeOverride">@dimen/taskbar_icon_size</item>
</style>
</resources>

View File

@ -30,7 +30,6 @@ import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.Nullable;
@ -43,7 +42,7 @@ import com.android.launcher3.proxy.StartActivityParams;
import com.android.launcher3.statehandlers.BackButtonAlphaHandler;
import com.android.launcher3.statehandlers.DepthController;
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.taskbar.TaskbarContainerView;
import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.launcher3.taskbar.TaskbarController;
import com.android.launcher3.taskbar.TaskbarStateHandler;
import com.android.launcher3.uioverrides.RecentsViewStateController;
@ -207,6 +206,7 @@ public abstract class BaseQuickstepLauncher extends Launcher
mActionsView.updateVerticalMargin(SysUINavigationMode.getMode(this));
addTaskbarIfNecessary();
addOnDeviceProfileChangeListener(newDp -> addTaskbarIfNecessary());
}
@Override
@ -223,9 +223,9 @@ public abstract class BaseQuickstepLauncher extends Launcher
mTaskbarController = null;
}
if (FeatureFlags.ENABLE_TASKBAR.get() && mDeviceProfile.isTablet) {
TaskbarContainerView taskbarContainer = (TaskbarContainerView) LayoutInflater.from(this)
.inflate(R.layout.taskbar, null, false);
mTaskbarController = new TaskbarController(this, taskbarContainer);
TaskbarActivityContext taskbarActivityContext = new TaskbarActivityContext(this);
mTaskbarController = new TaskbarController(this,
taskbarActivityContext.getTaskbarContainerView());
mTaskbarController.init();
}
}

View File

@ -0,0 +1,78 @@
/*
* 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.ContextWrapper;
import android.graphics.Rect;
import android.view.LayoutInflater;
import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
/**
* The {@link ActivityContext} with which we inflate Taskbar-related Views. This allows UI elements
* that are used by both Launcher and Taskbar (such as Folder) to reference a generic
* ActivityContext and BaseDragLayer instead of the Launcher activity and its DragLayer.
*/
public class TaskbarActivityContext extends ContextWrapper implements ActivityContext {
private final DeviceProfile mDeviceProfile;
private final LayoutInflater mLayoutInflater;
private final TaskbarContainerView mTaskbarContainerView;
public TaskbarActivityContext(BaseQuickstepLauncher launcher) {
super(launcher);
mDeviceProfile = launcher.getDeviceProfile().copy(this);
float taskbarIconSize = getResources().getDimension(R.dimen.taskbar_icon_size);
float iconScale = taskbarIconSize / mDeviceProfile.iconSizePx;
mDeviceProfile.updateIconSize(iconScale, getResources());
mLayoutInflater = LayoutInflater.from(this).cloneInContext(this);
mTaskbarContainerView = (TaskbarContainerView) mLayoutInflater
.inflate(R.layout.taskbar, null, false);
}
public TaskbarContainerView getTaskbarContainerView() {
return mTaskbarContainerView;
}
/**
* @return A LayoutInflater to use in this Context. Views inflated with this LayoutInflater will
* be able to access this TaskbarActivityContext via ActivityContext.lookupContext().
*/
public LayoutInflater getLayoutInflater() {
return mLayoutInflater;
}
@Override
public BaseDragLayer<TaskbarActivityContext> getDragLayer() {
return mTaskbarContainerView;
}
@Override
public DeviceProfile getDeviceProfile() {
return mDeviceProfile;
}
@Override
public Rect getFolderBoundingBox() {
return mTaskbarContainerView.getFolderBoundingBox();
}
}

View File

@ -19,19 +19,29 @@ import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsI
import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_REGION;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
import com.android.launcher3.anim.AlphaUpdateListener;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.BaseDragLayer;
import com.android.systemui.shared.system.ViewTreeObserverWrapper;
/**
* Top-level ViewGroup that hosts the TaskbarView as well as Views created by it such as Folder.
*/
public class TaskbarContainerView extends FrameLayout {
public class TaskbarContainerView extends BaseDragLayer<TaskbarActivityContext> {
private final int[] mTempLoc = new int[2];
private final int mFolderMargin;
// Initialized in TaskbarController constructor.
private TaskbarController.TaskbarContainerViewCallbacks mControllerCallbacks;
// Initialized in init.
private TaskbarView mTaskbarView;
@ -52,12 +62,23 @@ public class TaskbarContainerView extends FrameLayout {
public TaskbarContainerView(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
super(context, attrs, 1 /* alphaChannelCount */);
mFolderMargin = getResources().getDimensionPixelSize(R.dimen.taskbar_folder_margin);
}
protected void construct(TaskbarController.TaskbarContainerViewCallbacks callbacks) {
mControllerCallbacks = callbacks;
}
protected void init(TaskbarView taskbarView) {
mTaskbarView = taskbarView;
mTaskbarInsetsComputer = createTaskbarInsetsComputer();
recreateControllers();
}
@Override
public void recreateControllers() {
mControllers = new TouchController[0];
}
private ViewTreeObserverWrapper.OnComputeInsetsListener createTaskbarInsetsComputer() {
@ -70,6 +91,17 @@ public class TaskbarContainerView extends FrameLayout {
// We're visible again, accept touches anywhere in our bounds.
insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME);
}
// TaskbarContainerView provides insets to other apps based on contentInsets. These
// insets should stay consistent even if we expand TaskbarContainerView's bounds, e.g.
// to show a floating view like Folder. Thus, we set the contentInsets to be where
// mTaskbarView is, since its position never changes and insets rather than overlays.
int[] loc = mTempLoc;
mTaskbarView.getLocationInWindow(loc);
insetsInfo.contentInsets.left = loc[0];
insetsInfo.contentInsets.top = loc[1];
insetsInfo.contentInsets.right = getWidth() - (loc[0] + mTaskbarView.getWidth());
insetsInfo.contentInsets.bottom = getHeight() - (loc[1] + mTaskbarView.getHeight());
};
}
@ -91,4 +123,30 @@ public class TaskbarContainerView extends FrameLayout {
cleanup();
}
@Override
protected boolean canFindActiveController() {
// Unlike super class, we want to be able to find controllers when touches occur in the
// gesture area. For example, this allows Folder to close itself when touching the Taskbar.
return true;
}
@Override
public void onViewRemoved(View child) {
super.onViewRemoved(child);
mControllerCallbacks.onViewRemoved();
}
/**
* @return Bounds (in our coordinates) where an opened Folder can display.
*/
protected Rect getFolderBoundingBox() {
Rect boundingBox = new Rect(0, 0, getWidth(), getHeight() - mTaskbarView.getHeight());
boundingBox.inset(mFolderMargin, mFolderMargin);
return boundingBox;
}
protected TaskbarActivityContext getTaskbarActivityContext() {
return mActivity;
}
}

View File

@ -34,11 +34,15 @@ import android.view.WindowManager;
import androidx.annotation.Nullable;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.QuickstepAppTransitionManagerImpl;
import com.android.launcher3.R;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.touch.ItemClickHandler;
@ -81,8 +85,9 @@ public class TaskbarController {
TaskbarContainerView taskbarContainerView) {
mLauncher = launcher;
mTaskbarContainerView = taskbarContainerView;
mTaskbarContainerView.construct(createTaskbarContainerViewCallbacks());
mTaskbarView = mTaskbarContainerView.findViewById(R.id.taskbar_view);
mTaskbarView.setCallbacks(createTaskbarViewCallbacks());
mTaskbarView.construct(createTaskbarViewCallbacks());
mWindowManager = mLauncher.getWindowManager();
mTaskbarSize = new Point(MATCH_PARENT,
mLauncher.getResources().getDimensionPixelSize(R.dimen.taskbar_size));
@ -110,6 +115,18 @@ public class TaskbarController {
};
}
private TaskbarContainerViewCallbacks createTaskbarContainerViewCallbacks() {
return new TaskbarContainerViewCallbacks() {
@Override
public void onViewRemoved() {
if (mTaskbarContainerView.getChildCount() == 1) {
// Only TaskbarView remains.
setTaskbarWindowFullscreen(false);
}
}
};
}
private TaskbarViewCallbacks createTaskbarViewCallbacks() {
return new TaskbarViewCallbacks() {
@Override
@ -120,9 +137,29 @@ public class TaskbarController {
Task task = (Task) tag;
ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
ActivityOptions.makeBasic());
} else if (tag instanceof FolderInfo) {
FolderIcon folderIcon = (FolderIcon) view;
Folder folder = folderIcon.getFolder();
setTaskbarWindowFullscreen(true);
mTaskbarContainerView.post(() -> {
folder.animateOpen();
folder.iterateOverItems((itemInfo, itemView) -> {
itemView.setOnClickListener(getItemOnClickListener());
itemView.setOnLongClickListener(getItemOnLongClickListener());
// To play haptic when dragging, like other Taskbar items do.
itemView.setHapticFeedbackEnabled(true);
return false;
});
});
} else {
ItemClickHandler.INSTANCE.onClick(view);
}
AbstractFloatingView.closeAllOpenViews(
mTaskbarContainerView.getTaskbarActivityContext());
};
}
@ -344,6 +381,20 @@ public class TaskbarController {
return mTaskbarContainerView.getWindowId().equals(v.getWindowId());
}
/**
* Updates the TaskbarContainer to MATCH_PARENT vs original Taskbar size.
*/
private void setTaskbarWindowFullscreen(boolean fullscreen) {
if (fullscreen) {
mWindowLayoutParams.width = MATCH_PARENT;
mWindowLayoutParams.height = MATCH_PARENT;
} else {
mWindowLayoutParams.width = mTaskbarSize.x;
mWindowLayoutParams.height = mTaskbarSize.y;
}
mWindowManager.updateViewLayout(mTaskbarContainerView, mWindowLayoutParams);
}
/**
* Contains methods that TaskbarStateHandler can call to interface with TaskbarController.
*/
@ -360,6 +411,13 @@ public class TaskbarController {
void updateTaskbarVisibilityAlpha(float alpha);
}
/**
* Contains methods that TaskbarContainerView can call to interface with TaskbarController.
*/
protected interface TaskbarContainerViewCallbacks {
void onViewRemoved();
}
/**
* Contains methods that TaskbarView can call to interface with TaskbarController.
*/

View File

@ -22,7 +22,6 @@ import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.DragEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
@ -32,10 +31,14 @@ import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.R;
import com.android.launcher3.folder.FolderIcon;
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.views.ActivityContext;
import com.android.systemui.shared.recents.model.Task;
/**
@ -51,6 +54,9 @@ public class TaskbarView extends LinearLayout {
private final RectF mDelegateSlopBounds = new RectF();
private final int[] mTempOutLocation = new int[2];
// Initialized in TaskbarController constructor.
private TaskbarController.TaskbarViewCallbacks mControllerCallbacks;
// Initialized in init().
private int mHotseatStartIndex;
private int mHotseatEndIndex;
@ -58,8 +64,6 @@ public class TaskbarView extends LinearLayout {
private int mRecentsStartIndex;
private int mRecentsEndIndex;
private TaskbarController.TaskbarViewCallbacks mControllerCallbacks;
// Delegate touches to the closest view if within mIconTouchSize.
private boolean mDelegateTargeted;
private View mDelegateView;
@ -90,7 +94,7 @@ public class TaskbarView extends LinearLayout {
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
protected void setCallbacks(TaskbarController.TaskbarViewCallbacks taskbarViewCallbacks) {
protected void construct(TaskbarController.TaskbarViewCallbacks taskbarViewCallbacks) {
mControllerCallbacks = taskbarViewCallbacks;
}
@ -130,17 +134,37 @@ public class TaskbarView extends LinearLayout {
// Replace any Hotseat views with the appropriate type if it's not already that type.
final int expectedLayoutResId;
boolean isFolder = false;
boolean needsReinflate = false;
if (hotseatItemInfo != null && hotseatItemInfo.isPredictedItem()) {
expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
} else if (hotseatItemInfo instanceof FolderInfo) {
expectedLayoutResId = R.layout.folder_icon;
isFolder = true;
// 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.
needsReinflate = hotseatView != null && hotseatView.getTag() != hotseatItemInfo;
} else {
expectedLayoutResId = R.layout.taskbar_app_icon;
}
if (hotseatView == null || hotseatView.getSourceLayoutResId() != expectedLayoutResId) {
if (hotseatView == null || hotseatView.getSourceLayoutResId() != expectedLayoutResId
|| needsReinflate) {
removeView(hotseatView);
BubbleTextView btv = (BubbleTextView) inflate(expectedLayoutResId);
LayoutParams lp = new LayoutParams(btv.getIconSize(), btv.getIconSize());
TaskbarActivityContext activityContext =
ActivityContext.lookupContext(getContext());
if (isFolder) {
FolderInfo folderInfo = (FolderInfo) hotseatItemInfo;
FolderIcon folderIcon = FolderIcon.inflateFolderAndIcon(expectedLayoutResId,
activityContext, this, folderInfo);
folderIcon.setTextVisible(false);
hotseatView = folderIcon;
} else {
hotseatView = inflate(expectedLayoutResId);
}
int iconSize = activityContext.getDeviceProfile().iconSizePx;
LayoutParams lp = new LayoutParams(iconSize, iconSize);
lp.setMargins(mItemMarginLeftRight, 0, mItemMarginLeftRight, 0);
hotseatView = btv;
addView(hotseatView, hotseatIndex, lp);
}
@ -153,6 +177,11 @@ public class TaskbarView extends LinearLayout {
hotseatView.setOnClickListener(mControllerCallbacks.getItemOnClickListener());
hotseatView.setOnLongClickListener(
mControllerCallbacks.getItemOnLongClickListener());
} else if (isFolder) {
hotseatView.setVisibility(VISIBLE);
hotseatView.setOnClickListener(mControllerCallbacks.getItemOnClickListener());
hotseatView.setOnLongClickListener(
mControllerCallbacks.getItemOnLongClickListener());
} else {
hotseatView.setVisibility(GONE);
hotseatView.setOnClickListener(null);
@ -345,6 +374,7 @@ public class TaskbarView extends LinearLayout {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
mIsDraggingItem = true;
AbstractFloatingView.closeAllOpenViews(ActivityContext.lookupContext(getContext()));
return true;
case DragEvent.ACTION_DRAG_ENDED:
mIsDraggingItem = false;
@ -358,6 +388,7 @@ public class TaskbarView extends LinearLayout {
}
private View inflate(@LayoutRes int layoutResId) {
return LayoutInflater.from(getContext()).inflate(layoutResId, this, false);
TaskbarActivityContext taskbarActivityContext = ActivityContext.lookupContext(getContext());
return taskbarActivityContext.getLayoutInflater().inflate(layoutResId, this, false);
}
}

View File

@ -505,7 +505,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
* @param canvas The canvas to draw to.
*/
protected void drawDotIfNecessary(Canvas canvas) {
if (mDisplay == DISPLAY_TASKBAR) {
if (mActivity instanceof Launcher && ((Launcher) mActivity).isViewInTaskbar(this)) {
// TODO: support notification dots in Taskbar
return;
}

View File

@ -375,7 +375,7 @@ public class DeviceProfile {
* iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
* hotseat sizes, workspaceSpringLoadedShrinkFactor, folderIconSizePx, and folderIconOffsetYPx.
*/
private void updateIconSize(float scale, Resources res) {
public void updateIconSize(float scale, Resources res) {
// Workspace
final boolean isVerticalLayout = isVerticalBarLayout();
float invIconSizeDp = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize;

View File

@ -1616,6 +1616,11 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo
return false;
}
@Override
public boolean canInterceptEventsInSystemGestureRegion() {
return true;
}
/**
* Alternative to using {@link #getClipToOutline()} as it only works with derivatives of
* rounded rect.

View File

@ -754,6 +754,9 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel
}
public void clearLeaveBehindIfExists() {
if (!(getLayoutParams() instanceof CellLayout.LayoutParams)) {
return;
}
((CellLayout.LayoutParams) getLayoutParams()).canReorder = true;
if (isInHotseat()) {
CellLayout cl = (CellLayout) getParent().getParent();
@ -762,6 +765,9 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel
}
public void drawLeaveBehindIfExists() {
if (!(getLayoutParams() instanceof CellLayout.LayoutParams)) {
return;
}
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
// While the folder is open, the position of the icon cannot change.
lp.canReorder = false;

View File

@ -206,15 +206,19 @@ public abstract class BaseDragLayer<T extends Context & ActivityContext>
protected boolean findActiveController(MotionEvent ev) {
mActiveController = null;
if ((mTouchDispatchState & (TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION
| TOUCH_DISPATCHING_FROM_PROXY)) == 0) {
// Only look for controllers if we are not dispatching from gesture area and proxy is
// not active
if (canFindActiveController()) {
mActiveController = findControllerToHandleTouch(ev);
}
return mActiveController != null;
}
protected boolean canFindActiveController() {
// Only look for controllers if we are not dispatching from gesture area and proxy is
// not active
return (mTouchDispatchState & (TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION
| TOUCH_DISPATCHING_FROM_PROXY)) == 0;
}
@Override
public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
// Shortcuts can appear above folder