mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-03-01 00:06:47 +00:00
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
343 lines
12 KiB
Java
343 lines
12 KiB
Java
/*
|
|
* 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);
|
|
}
|
|
}
|
|
}
|