blob: a6c897f1696b5e97f99e738f9c8ee8d342cb6c16 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.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);
}
}
}