Adding support for keyboard based drag and drop

For keyboard DnD, we use Accessible DnD implementation.
A placeholder FloatingView draws the focus indicator
for the virtual views

Test: Visible
Bug: 178781566
Change-Id: I632fc7377dffa1e05e3f0a9c3ad18641deb5a1a4
This commit is contained in:
Sunny Goyal 2021-02-02 13:45:34 -08:00
parent d462965698
commit a4647b681f
18 changed files with 723 additions and 288 deletions

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<com.android.launcher3.keyboard.KeyboardDragAndDropView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:orientation="vertical"
android:elevation="6dp">
<TextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:background="?attr/folderFillColor"
android:padding="8dp"
android:textColor="?attr/folderTextColor"
/>
</com.android.launcher3.keyboard.KeyboardDragAndDropView>

View File

@ -139,6 +139,7 @@
<dimen name="drag_flingToDeleteMinVelocity">-1500dp</dimen>
<dimen name="spring_loaded_panel_border">1dp</dimen>
<dimen name="keyboard_drag_stroke_width">4dp</dimen>
<!-- Folders -->
<dimen name="page_indicator_dot_size">8dp</dimen>

View File

@ -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;

View File

@ -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();

View File

@ -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.

View File

@ -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;

View File

@ -2681,7 +2681,8 @@ public class Launcher extends StatefulActivity<LauncherState> 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;
}

View File

@ -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<Integer> virtualViews) {
public void getVisibleVirtualViews(List<Integer> 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);

View File

@ -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,9 +428,18 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme
DragOptions options = new DragOptions();
options.isAccessibleDrag = true;
options.isKeyboardDrag = fromKeyboard;
options.simulatedDndStartPoint = new Point(pos.centerX(), pos.centerY());
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
public void onDragStart(DragObject dragObject, DragOptions options) {

View File

@ -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;

View File

@ -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();

View File

@ -85,9 +85,6 @@ public class DragLayer extends BaseDragLayer<Launcher> {
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<Launcher> {
public void setup(DragController dragController, Workspace workspace) {
mDragController = dragController;
mWorkspaceScrim.setWorkspace(workspace);
mMoveTarget = workspace;
recreateControllers();
}
@ -214,12 +210,6 @@ public class DragLayer extends BaseDragLayer<Launcher> {
}
}
@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);

View File

@ -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

View File

@ -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);
}
}

View File

@ -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<FocusIndicatorHelper, Float> ALPHA =
new Property<FocusIndicatorHelper, Float>(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<FocusIndicatorHelper, Float> SHIFT =
new Property<FocusIndicatorHelper, Float>(
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<View>
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();
}
}
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;
changeFocus(v, hasFocus);
}
@Override
public void onAnimationCancel(Animator animation) {
if (!mCallOnCancel) {
mCalled = true;
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (!mCalled) {
setCurrentView(mViewToSet);
mCalled = true;
}
}
protected boolean shouldDraw(View item) {
return item.isAttachedToWindow();
}
/**

View File

@ -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 <T> Item type
*/
public abstract class ItemFocusIndicatorHelper<T> implements AnimatorUpdateListener {
private static final float MIN_VISIBLE_ALPHA = 0.2f;
private static final long ANIM_DURATION = 150;
public static final FloatProperty<ItemFocusIndicatorHelper> ALPHA =
new FloatProperty<ItemFocusIndicatorHelper>("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<ItemFocusIndicatorHelper> SHIFT =
new FloatProperty<ItemFocusIndicatorHelper>("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;
}
}
}
}

View File

@ -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<LauncherState> {
private static final long MINOR_AXIS_WEIGHT = 13;
private final ArrayList<Integer> mIntList = new ArrayList<>();
private final ArrayList<DragAndDropAccessibilityDelegate> mDelegates = new ArrayList<>();
private final ArrayList<VirtualNodeInfo> 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<Rect, Rect> majorAxis;
final ToIntFunction<Rect> 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<Rect> {
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);
}
}
}

View File

@ -454,12 +454,6 @@ public abstract class BaseDragLayer<T extends Context & ActivityContext>
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);