diff --git a/res/layout/keyboard_drag_and_drop.xml b/res/layout/keyboard_drag_and_drop.xml new file mode 100644 index 0000000000..e9463c40a0 --- /dev/null +++ b/res/layout/keyboard_drag_and_drop.xml @@ -0,0 +1,34 @@ + + + + + + + \ No newline at end of file diff --git a/res/values/dimens.xml b/res/values/dimens.xml index cf51f77707..eaf7a5ff8c 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -139,6 +139,7 @@ -1500dp 1dp + 4dp 8dp diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java index 3c34444b28..6037c96f82 100644 --- a/src/com/android/launcher3/AbstractFloatingView.java +++ b/src/com/android/launcher3/AbstractFloatingView.java @@ -76,17 +76,18 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch public static final int TYPE_SNACKBAR = 1 << 7; public static final int TYPE_LISTENER = 1 << 8; public static final int TYPE_ALL_APPS_EDU = 1 << 9; + public static final int TYPE_DRAG_DROP_POPUP = 1 << 10; // Popups related to quickstep UI - public static final int TYPE_TASK_MENU = 1 << 10; - public static final int TYPE_OPTIONS_POPUP = 1 << 11; - public static final int TYPE_ICON_SURFACE = 1 << 12; + public static final int TYPE_TASK_MENU = 1 << 11; + public static final int TYPE_OPTIONS_POPUP = 1 << 12; + public static final int TYPE_ICON_SURFACE = 1 << 13; public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU - | TYPE_ICON_SURFACE; + | TYPE_ICON_SURFACE | TYPE_DRAG_DROP_POPUP; // Type of popups which should be kept open during launcher rebind public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET @@ -103,7 +104,7 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch // These view all have particular operation associated with swipe down interaction. public static final int TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW = TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGETS_FULL_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_ON_BOARD_POPUP | - TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU ; + TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU | TYPE_DRAG_DROP_POPUP; protected boolean mIsOpen; diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java index df005e6357..459b9a841e 100644 --- a/src/com/android/launcher3/ButtonDropTarget.java +++ b/src/com/android/launcher3/ButtonDropTarget.java @@ -240,7 +240,7 @@ public abstract class ButtonDropTarget extends TextView @Override public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { - mActive = supportsDrop(dragObject.dragInfo); + mActive = !options.isKeyboardDrag && supportsDrop(dragObject.dragInfo); mDrawable.setColorFilter(null); if (mCurrentColorAnim != null) { mCurrentColorAnim.cancel(); diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 2809bd5a96..452207db3b 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -312,6 +312,13 @@ public class CellLayout extends ViewGroup { } } + /** + * Returns the currently set accessibility delegate + */ + public DragAndDropAccessibilityDelegate getDragAndDropAccessibilityDelegate() { + return mTouchHelper; + } + @Override public boolean dispatchHoverEvent(MotionEvent event) { // Always attempt to dispatch hover events to accessibility first. diff --git a/src/com/android/launcher3/DropTargetBar.java b/src/com/android/launcher3/DropTargetBar.java index ca001a35b2..c7684939db 100644 --- a/src/com/android/launcher3/DropTargetBar.java +++ b/src/com/android/launcher3/DropTargetBar.java @@ -131,7 +131,10 @@ public class DropTargetBar extends FrameLayout int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); - if (mIsVertical) { + int visibleCount = getVisibleButtonsCount(); + if (visibleCount == 0) { + // do nothing + } else if (mIsVertical) { int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); @@ -142,7 +145,6 @@ public class DropTargetBar extends FrameLayout } } } else { - int visibleCount = getVisibleButtonsCount(); int availableWidth = width / visibleCount; boolean textVisible = true; for (ButtonDropTarget buttons : mDropTargets) { @@ -165,7 +167,10 @@ public class DropTargetBar extends FrameLayout @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (mIsVertical) { + int visibleCount = getVisibleButtonsCount(); + if (visibleCount == 0) { + // do nothing + } else if (mIsVertical) { int gap = getResources().getDimensionPixelSize(R.dimen.drop_target_vertical_gap); int start = gap; int end; @@ -178,7 +183,6 @@ public class DropTargetBar extends FrameLayout } } } else { - int visibleCount = getVisibleButtonsCount(); int frameSize = (right - left) / visibleCount; int start = frameSize / 2; diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 0274775244..70a0be9f56 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -2681,7 +2681,8 @@ public class Launcher extends StatefulActivity implements Launche && focusedView.getTag() instanceof ItemInfo && mAccessibilityDelegate.performAction(focusedView, (ItemInfo) focusedView.getTag(), - LauncherAccessibilityDelegate.DEEP_SHORTCUTS)) { + LauncherAccessibilityDelegate.DEEP_SHORTCUTS, + true)) { PopupContainerWithArrow.getOpen(this).requestFocus(); return true; } diff --git a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java index ddb547ffb7..d0fc17534e 100644 --- a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java @@ -31,6 +31,7 @@ import androidx.customview.widget.ExploreByTouchHelper; import com.android.launcher3.CellLayout; import com.android.launcher3.Launcher; import com.android.launcher3.R; +import com.android.launcher3.dragndrop.DragLayer; import java.util.List; @@ -41,30 +42,32 @@ public abstract class DragAndDropAccessibilityDelegate extends ExploreByTouchHel implements OnClickListener, OnHoverListener { protected static final int INVALID_POSITION = -1; - private static final int[] sTempArray = new int[2]; + protected final Rect mTempRect = new Rect(); + protected final int[] mTempCords = new int[2]; protected final CellLayout mView; protected final Context mContext; protected final LauncherAccessibilityDelegate mDelegate; - - private final Rect mTempRect = new Rect(); + protected final DragLayer mDragLayer; public DragAndDropAccessibilityDelegate(CellLayout forView) { super(forView); mView = forView; mContext = mView.getContext(); - mDelegate = Launcher.getLauncher(mContext).getAccessibilityDelegate(); + Launcher launcher = Launcher.getLauncher(mContext); + mDelegate = launcher.getAccessibilityDelegate(); + mDragLayer = launcher.getDragLayer(); } @Override - protected int getVirtualViewAt(float x, float y) { + public int getVirtualViewAt(float x, float y) { if (x < 0 || y < 0 || x > mView.getMeasuredWidth() || y > mView.getMeasuredHeight()) { return INVALID_ID; } - mView.pointToCellExact((int) x, (int) y, sTempArray); + mView.pointToCellExact((int) x, (int) y, mTempCords); // Map cell to id - int id = sTempArray[0] + sTempArray[1] * mView.getCountX(); + int id = mTempCords[0] + mTempCords[1] * mView.getCountX(); return intersectsValidDropTarget(id); } @@ -75,7 +78,7 @@ public abstract class DragAndDropAccessibilityDelegate extends ExploreByTouchHel protected abstract int intersectsValidDropTarget(int id); @Override - protected void getVisibleVirtualViews(List virtualViews) { + public void getVisibleVirtualViews(List virtualViews) { // We create a virtual view for each cell of the grid // The cell ids correspond to cells in reading order. int nCells = mView.getCountX() * mView.getCountY(); @@ -88,7 +91,7 @@ public abstract class DragAndDropAccessibilityDelegate extends ExploreByTouchHel } @Override - protected boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) { + public boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) { if (action == AccessibilityNodeInfoCompat.ACTION_CLICK && viewId != INVALID_ID) { String confirmation = getConfirmationForIconDrop(viewId); mDelegate.handleAccessibleDrop(mView, getItemBounds(viewId), confirmation); @@ -112,13 +115,25 @@ public abstract class DragAndDropAccessibilityDelegate extends ExploreByTouchHel } @Override - protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) { + public void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) { if (id == INVALID_ID) { throw new IllegalArgumentException("Invalid virtual view id"); } node.setContentDescription(getLocationDescriptionForIconDrop(id)); - node.setBoundsInParent(getItemBounds(id)); + + Rect itemBounds = getItemBounds(id); + node.setBoundsInParent(itemBounds); + + // ExploreByTouchHelper does not currently handle view scale. + // Update BoundsInScreen to appropriate value. + mTempCords[0] = mTempCords[1] = 0; + float scale = mDragLayer.getDescendantCoordRelativeToSelf(mView, mTempCords); + mTempRect.left = mTempCords[0] + (int) (itemBounds.left * scale); + mTempRect.right = mTempCords[0] + (int) (itemBounds.right * scale); + mTempRect.top = mTempCords[1] + (int) (itemBounds.top * scale); + mTempRect.bottom = mTempCords[1] + (int) (itemBounds.bottom * scale); + node.setBoundsInScreen(mTempRect); node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); node.setClickable(true); @@ -130,6 +145,13 @@ public abstract class DragAndDropAccessibilityDelegate extends ExploreByTouchHel return dispatchHoverEvent(motionEvent); } + /** + * Returns the target host container + */ + public View getHost() { + return mView; + } + protected abstract String getLocationDescriptionForIconDrop(int id); protected abstract String getConfirmationForIconDrop(int id); diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java index 136d43ea73..6fac79aa7e 100644 --- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java @@ -34,6 +34,7 @@ import com.android.launcher3.dragndrop.DragController.DragListener; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.folder.Folder; import com.android.launcher3.keyboard.CustomActionsPopup; +import com.android.launcher3.keyboard.KeyboardDragAndDropView; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; @@ -107,10 +108,6 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme launcher.getText(R.string.shortcuts_menu_with_notifications_description))); } - public void addAccessibilityAction(int action, int actionLabel) { - mActions.put(action, new AccessibilityAction(action, mLauncher.getText(actionLabel))); - } - @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); @@ -139,7 +136,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme } // Do not add move actions for keyboard request as this uses virtual nodes. - if (!fromKeyboard && itemSupportsAccessibleDrag(item)) { + if (itemSupportsAccessibleDrag(item)) { info.addAction(mActions.get(MOVE)); if (item.container >= 0) { @@ -178,13 +175,17 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { if ((host.getTag() instanceof ItemInfo) - && performAction(host, (ItemInfo) host.getTag(), action)) { + && performAction(host, (ItemInfo) host.getTag(), action, false)) { return true; } return super.performAccessibilityAction(host, action, args); } - public boolean performAction(final View host, final ItemInfo item, int action) { + /** + * Performs the provided action on the host + */ + public boolean performAction(final View host, final ItemInfo item, int action, + boolean fromKeyboard) { if (action == ACTION_LONG_CLICK) { if (PopupContainerWithArrow.canShow(host, item)) { // Long press should be consumed for workspace items, and it should invoke the @@ -205,7 +206,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme return true; } if (action == MOVE) { - beginAccessibleDrag(host, item); + return beginAccessibleDrag(host, item, fromKeyboard); } else if (action == ADD_TO_WORKSPACE) { final int[] coordinates = new int[2]; final int screenId = findSpaceOnWorkspace(item, coordinates); @@ -406,7 +407,11 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme } } - public void beginAccessibleDrag(View item, ItemInfo info) { + private boolean beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard) { + if (!itemSupportsAccessibleDrag(info)) { + return false; + } + mDragInfo = new DragInfo(); mDragInfo.info = info; mDragInfo.item = item; @@ -423,8 +428,17 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme DragOptions options = new DragOptions(); options.isAccessibleDrag = true; + options.isKeyboardDrag = fromKeyboard; options.simulatedDndStartPoint = new Point(pos.centerX(), pos.centerY()); - ItemLongClickListener.beginDrag(item, mLauncher, info, options); + + if (fromKeyboard) { + KeyboardDragAndDropView popup = (KeyboardDragAndDropView) mLauncher.getLayoutInflater() + .inflate(R.layout.keyboard_drag_and_drop, mLauncher.getDragLayer(), false); + popup.showForIcon(item, info, options); + } else { + ItemLongClickListener.beginDrag(item, mLauncher, info, options); + } + return true; } @Override diff --git a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java index d4ba11ef68..aaaff98e18 100644 --- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java @@ -59,7 +59,7 @@ public class ShortcutMenuAccessibilityDelegate extends LauncherAccessibilityDele } @Override - public boolean performAction(View host, ItemInfo item, int action) { + public boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) { if (action == ADD_TO_WORKSPACE) { if (!(host.getParent() instanceof DeepShortcutView)) { return false; diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java index 65a261de82..a331924f22 100644 --- a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java +++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java @@ -17,17 +17,12 @@ package com.android.launcher3.accessibility; import android.content.Context; -import android.graphics.Rect; import android.text.TextUtils; import android.view.View; -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; - import com.android.launcher3.CellLayout; -import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DragType; -import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; @@ -38,9 +33,6 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; */ public class WorkspaceAccessibilityHelper extends DragAndDropAccessibilityDelegate { - private final Rect mTempRect = new Rect(); - private final int[] mTempCords = new int[2]; - public WorkspaceAccessibilityHelper(CellLayout layout) { super(layout); } @@ -134,26 +126,6 @@ public class WorkspaceAccessibilityHelper extends DragAndDropAccessibilityDelega } return ""; } - - @Override - protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) { - super.onPopulateNodeForVirtualView(id, node); - - - // ExploreByTouchHelper does not currently handle view scale. - // Update BoundsInScreen to appropriate value. - DragLayer dragLayer = Launcher.getLauncher(mView.getContext()).getDragLayer(); - mTempCords[0] = mTempCords[1] = 0; - float scale = dragLayer.getDescendantCoordRelativeToSelf(mView, mTempCords); - - node.getBoundsInParent(mTempRect); - mTempRect.left = mTempCords[0] + (int) (mTempRect.left * scale); - mTempRect.right = mTempCords[0] + (int) (mTempRect.right * scale); - mTempRect.top = mTempCords[1] + (int) (mTempRect.top * scale); - mTempRect.bottom = mTempCords[1] + (int) (mTempRect.bottom * scale); - node.setBoundsInScreen(mTempRect); - } - @Override protected String getLocationDescriptionForIconDrop(int id) { int x = id % mView.getCountX(); diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java index ddf44cab3b..e71c12d433 100644 --- a/src/com/android/launcher3/dragndrop/DragLayer.java +++ b/src/com/android/launcher3/dragndrop/DragLayer.java @@ -85,9 +85,6 @@ public class DragLayer extends BaseDragLayer { private final WorkspaceAndHotseatScrim mWorkspaceScrim; private final OverviewScrim mOverviewScrim; - // View that should handle move events - private View mMoveTarget; - /** * Used to create a new DragLayer from XML. * @@ -109,7 +106,6 @@ public class DragLayer extends BaseDragLayer { public void setup(DragController dragController, Workspace workspace) { mDragController = dragController; mWorkspaceScrim.setWorkspace(workspace); - mMoveTarget = workspace; recreateControllers(); } @@ -214,12 +210,6 @@ public class DragLayer extends BaseDragLayer { } } - @Override - public boolean dispatchUnhandledMove(View focused, int direction) { - return super.dispatchUnhandledMove(focused, direction) - || mMoveTarget.dispatchUnhandledMove(focused, direction); - } - @Override public boolean dispatchTouchEvent(MotionEvent ev) { ev.offsetLocation(getTranslationX(), 0); diff --git a/src/com/android/launcher3/dragndrop/DragOptions.java b/src/com/android/launcher3/dragndrop/DragOptions.java index 959602beb4..e8ff8da058 100644 --- a/src/com/android/launcher3/dragndrop/DragOptions.java +++ b/src/com/android/launcher3/dragndrop/DragOptions.java @@ -28,6 +28,9 @@ public class DragOptions { /** Whether or not an accessible drag operation is in progress. */ public boolean isAccessibleDrag = false; + /** Whether or not the drag operation is controlled by keyboard. */ + public boolean isKeyboardDrag = false; + /** * Specifies the start location for a simulated DnD (like system drag or accessibility drag), * null when using internal DnD diff --git a/src/com/android/launcher3/keyboard/CustomActionsPopup.java b/src/com/android/launcher3/keyboard/CustomActionsPopup.java index 800598e23f..77ce4a8c88 100644 --- a/src/com/android/launcher3/keyboard/CustomActionsPopup.java +++ b/src/com/android/launcher3/keyboard/CustomActionsPopup.java @@ -88,6 +88,7 @@ public class CustomActionsPopup implements OnMenuItemClickListener { @Override public boolean onMenuItemClick(MenuItem menuItem) { - return mDelegate.performAction(mIcon, (ItemInfo) mIcon.getTag(), menuItem.getItemId()); + return mDelegate.performAction(mIcon, (ItemInfo) mIcon.getTag(), menuItem.getItemId(), + true); } } diff --git a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java index ae7ad106b8..83003ffb14 100644 --- a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java +++ b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java @@ -16,233 +16,30 @@ package com.android.launcher3.keyboard; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; -import android.animation.RectEvaluator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; import android.graphics.Rect; -import android.util.Property; import android.view.View; import android.view.View.OnFocusChangeListener; import com.android.launcher3.R; -import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.util.Themes; /** * A helper class to draw background of a focused view. */ -public abstract class FocusIndicatorHelper implements - OnFocusChangeListener, AnimatorUpdateListener { - - private static final float MIN_VISIBLE_ALPHA = 0.2f; - private static final long ANIM_DURATION = 150; - - public static final Property ALPHA = - new Property(Float.TYPE, "alpha") { - @Override - public void set(FocusIndicatorHelper object, Float value) { - object.setAlpha(value); - } - - @Override - public Float get(FocusIndicatorHelper object) { - return object.mAlpha; - } - }; - - public static final Property SHIFT = - new Property( - Float.TYPE, "shift") { - - @Override - public void set(FocusIndicatorHelper object, Float value) { - object.mShift = value; - } - - @Override - public Float get(FocusIndicatorHelper object) { - return object.mShift; - } - }; - - private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect()); - private static final Rect sTempRect1 = new Rect(); - private static final Rect sTempRect2 = new Rect(); - - private final View mContainer; - private final Paint mPaint; - private final int mMaxAlpha; - - private final Rect mDirtyRect = new Rect(); - private boolean mIsDirty = false; - - private View mLastFocusedView; - - private View mCurrentView; - private View mTargetView; - /** - * The fraction indicating the position of the focusRect between {@link #mCurrentView} - * & {@link #mTargetView} - */ - private float mShift; - - private ObjectAnimator mCurrentAnimation; - private float mAlpha; - private float mRadius; +public abstract class FocusIndicatorHelper extends ItemFocusIndicatorHelper + implements OnFocusChangeListener { public FocusIndicatorHelper(View container) { - mContainer = container; - - mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - int color = container.getResources().getColor(R.color.focused_background); - mMaxAlpha = Color.alpha(color); - mPaint.setColor(0xFF000000 | color); - - setAlpha(0); - mShift = 0; - if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) { - mRadius = Themes.getDialogCornerRadius(container.getContext()); - } - } - - protected void setAlpha(float alpha) { - mAlpha = alpha; - mPaint.setAlpha((int) (mAlpha * mMaxAlpha)); - } - - @Override - public void onAnimationUpdate(ValueAnimator animation) { - invalidateDirty(); - } - - protected void invalidateDirty() { - if (mIsDirty) { - mContainer.invalidate(mDirtyRect); - mIsDirty = false; - } - - Rect newRect = getDrawRect(); - if (newRect != null) { - mContainer.invalidate(newRect); - } - } - - public void draw(Canvas c) { - if (mAlpha <= 0) return; - - Rect newRect = getDrawRect(); - if (newRect != null) { - mDirtyRect.set(newRect); - c.drawRoundRect((float) mDirtyRect.left, (float) mDirtyRect.top, - (float) mDirtyRect.right, (float) mDirtyRect.bottom, - mRadius, mRadius, mPaint); - mIsDirty = true; - } - } - - private Rect getDrawRect() { - if (mCurrentView != null && mCurrentView.isAttachedToWindow()) { - viewToRect(mCurrentView, sTempRect1); - - if (mShift > 0 && mTargetView != null) { - viewToRect(mTargetView, sTempRect2); - return RECT_EVALUATOR.evaluate(mShift, sTempRect1, sTempRect2); - } else { - return sTempRect1; - } - } - return null; + super(container, container.getResources().getColor(R.color.focused_background)); } @Override public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { - endCurrentAnimation(); - - if (mAlpha > MIN_VISIBLE_ALPHA) { - mTargetView = v; - - mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this, - PropertyValuesHolder.ofFloat(ALPHA, 1), - PropertyValuesHolder.ofFloat(SHIFT, 1)); - mCurrentAnimation.addListener(new ViewSetListener(v, true)); - } else { - setCurrentView(v); - - mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this, - PropertyValuesHolder.ofFloat(ALPHA, 1)); - } - - mLastFocusedView = v; - } else { - if (mLastFocusedView == v) { - mLastFocusedView = null; - endCurrentAnimation(); - mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this, - PropertyValuesHolder.ofFloat(ALPHA, 0)); - mCurrentAnimation.addListener(new ViewSetListener(null, false)); - } - } - - // invalidate once - invalidateDirty(); - - mLastFocusedView = hasFocus ? v : null; - if (mCurrentAnimation != null) { - mCurrentAnimation.addUpdateListener(this); - mCurrentAnimation.setDuration(ANIM_DURATION).start(); - } + changeFocus(v, hasFocus); } - protected void endCurrentAnimation() { - if (mCurrentAnimation != null) { - mCurrentAnimation.cancel(); - mCurrentAnimation = null; - } - } - - protected void setCurrentView(View v) { - mCurrentView = v; - mShift = 0; - mTargetView = null; - } - - /** - * Gets the position of {@param v} relative to {@link #mContainer}. - */ - public abstract void viewToRect(View v, Rect outRect); - - private class ViewSetListener extends AnimatorListenerAdapter { - private final View mViewToSet; - private final boolean mCallOnCancel; - private boolean mCalled = false; - - public ViewSetListener(View v, boolean callOnCancel) { - mViewToSet = v; - mCallOnCancel = callOnCancel; - } - - @Override - public void onAnimationCancel(Animator animation) { - if (!mCallOnCancel) { - mCalled = true; - } - } - - @Override - public void onAnimationEnd(Animator animation) { - if (!mCalled) { - setCurrentView(mViewToSet); - mCalled = true; - } - } + @Override + protected boolean shouldDraw(View item) { + return item.isAttachedToWindow(); } /** diff --git a/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java new file mode 100644 index 0000000000..57fab2d06c --- /dev/null +++ b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java @@ -0,0 +1,252 @@ +/* + * 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.keyboard; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.RectEvaluator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.FloatProperty; +import android.view.View; + +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.util.Themes; + +/** + * A helper class to draw background of a focused item. + * @param Item type + */ +public abstract class ItemFocusIndicatorHelper implements AnimatorUpdateListener { + + private static final float MIN_VISIBLE_ALPHA = 0.2f; + private static final long ANIM_DURATION = 150; + + public static final FloatProperty ALPHA = + new FloatProperty("alpha") { + + @Override + public void setValue(ItemFocusIndicatorHelper object, float value) { + object.setAlpha(value); + } + + @Override + public Float get(ItemFocusIndicatorHelper object) { + return object.mAlpha; + } + }; + + public static final FloatProperty SHIFT = + new FloatProperty("shift") { + + @Override + public void setValue(ItemFocusIndicatorHelper object, float value) { + object.mShift = value; + } + + @Override + public Float get(ItemFocusIndicatorHelper object) { + return object.mShift; + } + }; + + private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect()); + private static final Rect sTempRect1 = new Rect(); + private static final Rect sTempRect2 = new Rect(); + + private final View mContainer; + protected final Paint mPaint; + private final int mMaxAlpha; + + private final Rect mDirtyRect = new Rect(); + private boolean mIsDirty = false; + + private T mLastFocusedItem; + + private T mCurrentItem; + private T mTargetItem; + /** + * The fraction indicating the position of the focusRect between {@link #mCurrentItem} + * & {@link #mTargetItem} + */ + private float mShift; + + private ObjectAnimator mCurrentAnimation; + private float mAlpha; + private float mRadius; + + public ItemFocusIndicatorHelper(View container, int color) { + mContainer = container; + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mMaxAlpha = Color.alpha(color); + mPaint.setColor(0xFF000000 | color); + + setAlpha(0); + mShift = 0; + if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) { + mRadius = Themes.getDialogCornerRadius(container.getContext()); + } + } + + protected void setAlpha(float alpha) { + mAlpha = alpha; + mPaint.setAlpha((int) (mAlpha * mMaxAlpha)); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + invalidateDirty(); + } + + protected void invalidateDirty() { + if (mIsDirty) { + mContainer.invalidate(mDirtyRect); + mIsDirty = false; + } + + Rect newRect = getDrawRect(); + if (newRect != null) { + mContainer.invalidate(newRect); + } + } + + /** + * Draws the indicator on the canvas + */ + public void draw(Canvas c) { + if (mAlpha <= 0) return; + + Rect newRect = getDrawRect(); + if (newRect != null) { + mDirtyRect.set(newRect); + c.drawRoundRect((float) mDirtyRect.left, (float) mDirtyRect.top, + (float) mDirtyRect.right, (float) mDirtyRect.bottom, + mRadius, mRadius, mPaint); + mIsDirty = true; + } + } + + private Rect getDrawRect() { + if (mCurrentItem != null && shouldDraw(mCurrentItem)) { + viewToRect(mCurrentItem, sTempRect1); + + if (mShift > 0 && mTargetItem != null) { + viewToRect(mTargetItem, sTempRect2); + return RECT_EVALUATOR.evaluate(mShift, sTempRect1, sTempRect2); + } else { + return sTempRect1; + } + } + return null; + } + + /** + * Returns true if the provided item is valid + */ + protected boolean shouldDraw(T item) { + return true; + } + + protected void changeFocus(T item, boolean hasFocus) { + if (hasFocus) { + endCurrentAnimation(); + + if (mAlpha > MIN_VISIBLE_ALPHA) { + mTargetItem = item; + + mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this, + PropertyValuesHolder.ofFloat(ALPHA, 1), + PropertyValuesHolder.ofFloat(SHIFT, 1)); + mCurrentAnimation.addListener(new ViewSetListener(item, true)); + } else { + setCurrentItem(item); + + mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this, + PropertyValuesHolder.ofFloat(ALPHA, 1)); + } + + mLastFocusedItem = item; + } else { + if (mLastFocusedItem == item) { + mLastFocusedItem = null; + endCurrentAnimation(); + mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this, + PropertyValuesHolder.ofFloat(ALPHA, 0)); + mCurrentAnimation.addListener(new ViewSetListener(null, false)); + } + } + + // invalidate once + invalidateDirty(); + + mLastFocusedItem = hasFocus ? item : null; + if (mCurrentAnimation != null) { + mCurrentAnimation.addUpdateListener(this); + mCurrentAnimation.setDuration(ANIM_DURATION).start(); + } + } + + protected void endCurrentAnimation() { + if (mCurrentAnimation != null) { + mCurrentAnimation.cancel(); + mCurrentAnimation = null; + } + } + + protected void setCurrentItem(T item) { + mCurrentItem = item; + mShift = 0; + mTargetItem = null; + } + + /** + * Gets the position of the item relative to {@link #mContainer}. + */ + public abstract void viewToRect(T item, Rect outRect); + + private class ViewSetListener extends AnimatorListenerAdapter { + private final T mItemToSet; + private final boolean mCallOnCancel; + private boolean mCalled = false; + + ViewSetListener(T item, boolean callOnCancel) { + mItemToSet = item; + mCallOnCancel = callOnCancel; + } + + @Override + public void onAnimationCancel(Animator animation) { + if (!mCallOnCancel) { + mCalled = true; + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mCalled) { + setCurrentItem(mItemToSet); + mCalled = true; + } + } + } +} diff --git a/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java b/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java new file mode 100644 index 0000000000..a6c897f169 --- /dev/null +++ b/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java @@ -0,0 +1,342 @@ +/* + * 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.keyboard; + +import static android.app.Activity.DEFAULT_KEYS_SEARCH_LOCAL; + +import static com.android.launcher3.LauncherState.SPRING_LOADED; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewParent; +import android.widget.TextView; + +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.CellLayout; +import com.android.launcher3.Insettable; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.PagedView; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate; +import com.android.launcher3.dragndrop.DragOptions; +import com.android.launcher3.folder.Folder; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.statemanager.StateManager.StateListener; +import com.android.launcher3.touch.ItemLongClickListener; +import com.android.launcher3.util.Themes; + +import java.util.ArrayList; +import java.util.Objects; +import java.util.function.ToIntBiFunction; +import java.util.function.ToIntFunction; + +/** + * A floating view to allow keyboard navigation across virtual nodes + */ +public class KeyboardDragAndDropView extends AbstractFloatingView + implements Insettable, StateListener { + + private static final long MINOR_AXIS_WEIGHT = 13; + + private final ArrayList mIntList = new ArrayList<>(); + private final ArrayList mDelegates = new ArrayList<>(); + private final ArrayList mNodes = new ArrayList<>(); + + private final Rect mTempRect = new Rect(); + private final Rect mTempRect2 = new Rect(); + private final AccessibilityNodeInfoCompat mTempNodeInfo = AccessibilityNodeInfoCompat.obtain(); + + private final RectFocusIndicator mFocusIndicator; + + private final Launcher mLauncher; + private VirtualNodeInfo mCurrentSelection; + + + public KeyboardDragAndDropView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyboardDragAndDropView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mLauncher = Launcher.getLauncher(context); + mFocusIndicator = new RectFocusIndicator(this); + setWillNotDraw(false); + } + + @Override + protected void handleClose(boolean animate) { + mLauncher.getDragLayer().removeView(this); + mLauncher.getStateManager().removeStateListener(this); + mLauncher.setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); + mIsOpen = false; + } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_DRAG_DROP_POPUP) != 0; + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + // Consume all touch + return true; + } + + @Override + public void setInsets(Rect insets) { + setPadding(insets.left, insets.top, insets.right, insets.bottom); + } + + @Override + public void onStateTransitionStart(LauncherState toState) { + if (toState != SPRING_LOADED) { + close(false); + } + } + + @Override + public void onStateTransitionComplete(LauncherState finalState) { + if (mCurrentSelection != null) { + setCurrentSelection(mCurrentSelection); + } + } + + private void setCurrentSelection(VirtualNodeInfo nodeInfo) { + mCurrentSelection = nodeInfo; + ((TextView) findViewById(R.id.label)) + .setText(nodeInfo.populate(mTempNodeInfo).getContentDescription()); + + Rect bounds = new Rect(); + mTempNodeInfo.getBoundsInParent(bounds); + View host = nodeInfo.delegate.getHost(); + ViewParent parent = host.getParent(); + if (parent instanceof PagedView) { + PagedView pv = (PagedView) parent; + int pageIndex = pv.indexOfChild(host); + + pv.setCurrentPage(pageIndex); + bounds.offset(pv.getScrollX() - pv.getScrollForPage(pageIndex), 0); + } + float[] pos = new float[] {bounds.left, bounds.top, bounds.right, bounds.bottom}; + Utilities.getDescendantCoordRelativeToAncestor(host, mLauncher.getDragLayer(), pos, true); + + new RectF(pos[0], pos[1], pos[2], pos[3]).roundOut(bounds); + mFocusIndicator.changeFocus(bounds, true); + } + + @Override + protected void onDraw(Canvas canvas) { + mFocusIndicator.draw(canvas); + } + + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + VirtualNodeInfo nodeInfo = getNextSelection(direction); + if (nodeInfo == null) { + return false; + } + setCurrentSelection(nodeInfo); + return true; + } + + /** + * Focus finding logic: + * Collect all virtual nodes in reading order (used for forward and backwards). + * Then find the closest view by comparing the distances spatially. Since it is a move + * operation. consider all cell sizes to be approximately of the same size. + */ + private VirtualNodeInfo getNextSelection(int direction) { + // Collect all virtual nodes + mDelegates.clear(); + mNodes.clear(); + + Folder openFolder = Folder.getOpen(mLauncher); + PagedView pv = openFolder == null ? mLauncher.getWorkspace() : openFolder.getContent(); + int count = pv.getPageCount(); + for (int i = 0; i < count; i++) { + mDelegates.add(((CellLayout) pv.getChildAt(i)).getDragAndDropAccessibilityDelegate()); + } + if (openFolder == null) { + mDelegates.add(pv.getNextPage() + 1, + mLauncher.getHotseat().getDragAndDropAccessibilityDelegate()); + } + mDelegates.forEach(delegate -> { + mIntList.clear(); + delegate.getVisibleVirtualViews(mIntList); + mIntList.forEach(id -> mNodes.add(new VirtualNodeInfo(delegate, id))); + }); + + if (mNodes.isEmpty()) { + return null; + } + int index = mNodes.indexOf(mCurrentSelection); + if (mCurrentSelection == null || index < 0) { + return null; + } + int totalNodes = mNodes.size(); + + final ToIntBiFunction majorAxis; + final ToIntFunction minorAxis; + + switch (direction) { + case View.FOCUS_RIGHT: + majorAxis = (source, dest) -> dest.left - source.left; + minorAxis = Rect::centerY; + break; + case View.FOCUS_LEFT: + majorAxis = (source, dest) -> source.left - dest.left; + minorAxis = Rect::centerY; + break; + case View.FOCUS_UP: + majorAxis = (source, dest) -> source.top - dest.top; + minorAxis = Rect::centerX; + break; + case View.FOCUS_DOWN: + majorAxis = (source, dest) -> dest.top - source.top; + minorAxis = Rect::centerX; + break; + case View.FOCUS_FORWARD: + return mNodes.get((index + 1) % totalNodes); + case View.FOCUS_BACKWARD: + return mNodes.get((index + totalNodes - 1) % totalNodes); + default: + // Unknown direction + return null; + } + mCurrentSelection.populate(mTempNodeInfo).getBoundsInScreen(mTempRect); + + float minWeight = Float.MAX_VALUE; + VirtualNodeInfo match = null; + for (int i = 0; i < totalNodes; i++) { + VirtualNodeInfo node = mNodes.get(i); + node.populate(mTempNodeInfo).getBoundsInScreen(mTempRect2); + + int majorAxisWeight = majorAxis.applyAsInt(mTempRect, mTempRect2); + if (majorAxisWeight <= 0) { + continue; + } + int minorAxisWeight = minorAxis.applyAsInt(mTempRect2) + - minorAxis.applyAsInt(mTempRect); + + float weight = majorAxisWeight * majorAxisWeight + + minorAxisWeight * minorAxisWeight * MINOR_AXIS_WEIGHT; + if (weight < minWeight) { + minWeight = weight; + match = node; + } + } + return match; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ENTER && mCurrentSelection != null) { + mCurrentSelection.delegate.onPerformActionForVirtualView( + mCurrentSelection.id, AccessibilityNodeInfoCompat.ACTION_CLICK, null); + return true; + } + return super.onKeyUp(keyCode, event); + } + + /** + * Shows the keyboard drag popup for the provided view + */ + public void showForIcon(View icon, ItemInfo item, DragOptions dragOptions) { + mIsOpen = true; + mLauncher.getDragLayer().addView(this); + mLauncher.getStateManager().addStateListener(this); + + // Find current selection + CellLayout currentParent = (CellLayout) icon.getParent().getParent(); + float[] iconPos = new float[] {currentParent.getCellWidth() / 2, + currentParent.getCellHeight() / 2}; + Utilities.getDescendantCoordRelativeToAncestor(icon, currentParent, iconPos, false); + + ItemLongClickListener.beginDrag(icon, mLauncher, item, dragOptions); + + DragAndDropAccessibilityDelegate dndDelegate = + currentParent.getDragAndDropAccessibilityDelegate(); + setCurrentSelection(new VirtualNodeInfo( + dndDelegate, dndDelegate.getVirtualViewAt(iconPos[0], iconPos[1]))); + + mLauncher.setDefaultKeyMode(Activity.DEFAULT_KEYS_DISABLE); + requestFocus(); + } + + private static class VirtualNodeInfo { + public final DragAndDropAccessibilityDelegate delegate; + public final int id; + + VirtualNodeInfo(DragAndDropAccessibilityDelegate delegate, int id) { + this.id = id; + this.delegate = delegate; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof VirtualNodeInfo)) { + return false; + } + VirtualNodeInfo that = (VirtualNodeInfo) o; + return id == that.id && delegate.equals(that.delegate); + } + + public AccessibilityNodeInfoCompat populate(AccessibilityNodeInfoCompat nodeInfo) { + delegate.onPopulateNodeForVirtualView(id, nodeInfo); + return nodeInfo; + } + + public void getBounds(AccessibilityNodeInfoCompat nodeInfo, Rect out) { + delegate.onPopulateNodeForVirtualView(id, nodeInfo); + nodeInfo.getBoundsInScreen(out); + } + + @Override + public int hashCode() { + return Objects.hash(id, delegate); + } + } + + private static class RectFocusIndicator extends ItemFocusIndicatorHelper { + + RectFocusIndicator(View container) { + super(container, Themes.getColorAccent(container.getContext())); + mPaint.setStrokeWidth(container.getResources() + .getDimension(R.dimen.keyboard_drag_stroke_width)); + mPaint.setStyle(Style.STROKE); + } + + @Override + public void viewToRect(Rect item, Rect outRect) { + outRect.set(item); + } + } +} diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java index 5464dd8ec9..15f7730fd7 100644 --- a/src/com/android/launcher3/views/BaseDragLayer.java +++ b/src/com/android/launcher3/views/BaseDragLayer.java @@ -454,12 +454,6 @@ public abstract class BaseDragLayer r.set(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight()); } - @Override - public boolean dispatchUnhandledMove(View focused, int direction) { - // Consume the unhandled move if a container is open, to avoid switching pages underneath. - return AbstractFloatingView.getTopOpenView(mActivity) != null; - } - @Override protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { View topView = AbstractFloatingView.getTopOpenView(mActivity);