1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.keyboard; 17 18 import static android.app.Activity.DEFAULT_KEYS_SEARCH_LOCAL; 19 20 import static com.android.launcher3.LauncherState.SPRING_LOADED; 21 22 import android.app.Activity; 23 import android.content.Context; 24 import android.graphics.Canvas; 25 import android.graphics.Paint.Style; 26 import android.graphics.Rect; 27 import android.graphics.RectF; 28 import android.util.AttributeSet; 29 import android.view.KeyEvent; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.ViewParent; 33 import android.widget.TextView; 34 35 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 36 37 import com.android.launcher3.AbstractFloatingView; 38 import com.android.launcher3.CellLayout; 39 import com.android.launcher3.Insettable; 40 import com.android.launcher3.Launcher; 41 import com.android.launcher3.LauncherState; 42 import com.android.launcher3.PagedView; 43 import com.android.launcher3.R; 44 import com.android.launcher3.Utilities; 45 import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate; 46 import com.android.launcher3.dragndrop.DragOptions; 47 import com.android.launcher3.folder.Folder; 48 import com.android.launcher3.model.data.ItemInfo; 49 import com.android.launcher3.statemanager.StateManager.StateListener; 50 import com.android.launcher3.touch.ItemLongClickListener; 51 import com.android.launcher3.util.Themes; 52 53 import java.util.ArrayList; 54 import java.util.Objects; 55 import java.util.function.ToIntBiFunction; 56 import java.util.function.ToIntFunction; 57 58 /** 59 * A floating view to allow keyboard navigation across virtual nodes 60 */ 61 public class KeyboardDragAndDropView extends AbstractFloatingView 62 implements Insettable, StateListener<LauncherState> { 63 64 private static final long MINOR_AXIS_WEIGHT = 13; 65 66 private final ArrayList<Integer> mIntList = new ArrayList<>(); 67 private final ArrayList<DragAndDropAccessibilityDelegate> mDelegates = new ArrayList<>(); 68 private final ArrayList<VirtualNodeInfo> mNodes = new ArrayList<>(); 69 70 private final Rect mTempRect = new Rect(); 71 private final Rect mTempRect2 = new Rect(); 72 private final AccessibilityNodeInfoCompat mTempNodeInfo = AccessibilityNodeInfoCompat.obtain(); 73 74 private final RectFocusIndicator mFocusIndicator; 75 76 private final Launcher mLauncher; 77 private VirtualNodeInfo mCurrentSelection; 78 79 KeyboardDragAndDropView(Context context, AttributeSet attrs)80 public KeyboardDragAndDropView(Context context, AttributeSet attrs) { 81 this(context, attrs, 0); 82 } 83 KeyboardDragAndDropView(Context context, AttributeSet attrs, int defStyleAttr)84 public KeyboardDragAndDropView(Context context, AttributeSet attrs, int defStyleAttr) { 85 super(context, attrs, defStyleAttr); 86 mLauncher = Launcher.getLauncher(context); 87 mFocusIndicator = new RectFocusIndicator(this); 88 setWillNotDraw(false); 89 } 90 91 @Override handleClose(boolean animate)92 protected void handleClose(boolean animate) { 93 mLauncher.getDragLayer().removeView(this); 94 mLauncher.getStateManager().removeStateListener(this); 95 mLauncher.setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); 96 mIsOpen = false; 97 } 98 99 @Override isOfType(int type)100 protected boolean isOfType(int type) { 101 return (type & TYPE_DRAG_DROP_POPUP) != 0; 102 } 103 104 @Override onControllerInterceptTouchEvent(MotionEvent ev)105 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 106 // Consume all touch 107 return true; 108 } 109 110 @Override setInsets(Rect insets)111 public void setInsets(Rect insets) { 112 setPadding(insets.left, insets.top, insets.right, insets.bottom); 113 } 114 115 @Override onStateTransitionStart(LauncherState toState)116 public void onStateTransitionStart(LauncherState toState) { 117 if (toState != SPRING_LOADED) { 118 close(false); 119 } 120 } 121 122 @Override onStateTransitionComplete(LauncherState finalState)123 public void onStateTransitionComplete(LauncherState finalState) { 124 if (mCurrentSelection != null) { 125 setCurrentSelection(mCurrentSelection); 126 } 127 } 128 setCurrentSelection(VirtualNodeInfo nodeInfo)129 private void setCurrentSelection(VirtualNodeInfo nodeInfo) { 130 mCurrentSelection = nodeInfo; 131 ((TextView) findViewById(R.id.label)) 132 .setText(nodeInfo.populate(mTempNodeInfo).getContentDescription()); 133 134 Rect bounds = new Rect(); 135 mTempNodeInfo.getBoundsInParent(bounds); 136 View host = nodeInfo.delegate.getHost(); 137 ViewParent parent = host.getParent(); 138 if (parent instanceof PagedView) { 139 PagedView pv = (PagedView) parent; 140 int pageIndex = pv.indexOfChild(host); 141 142 pv.setCurrentPage(pageIndex); 143 bounds.offset(pv.getScrollX() - pv.getScrollForPage(pageIndex), 0); 144 } 145 float[] pos = new float[] {bounds.left, bounds.top, bounds.right, bounds.bottom}; 146 Utilities.getDescendantCoordRelativeToAncestor(host, mLauncher.getDragLayer(), pos, true); 147 148 new RectF(pos[0], pos[1], pos[2], pos[3]).roundOut(bounds); 149 mFocusIndicator.changeFocus(bounds, true); 150 } 151 152 @Override onDraw(Canvas canvas)153 protected void onDraw(Canvas canvas) { 154 mFocusIndicator.draw(canvas); 155 } 156 157 @Override dispatchUnhandledMove(View focused, int direction)158 public boolean dispatchUnhandledMove(View focused, int direction) { 159 VirtualNodeInfo nodeInfo = getNextSelection(direction); 160 if (nodeInfo == null) { 161 return false; 162 } 163 setCurrentSelection(nodeInfo); 164 return true; 165 } 166 167 /** 168 * Focus finding logic: 169 * Collect all virtual nodes in reading order (used for forward and backwards). 170 * Then find the closest view by comparing the distances spatially. Since it is a move 171 * operation. consider all cell sizes to be approximately of the same size. 172 */ getNextSelection(int direction)173 private VirtualNodeInfo getNextSelection(int direction) { 174 // Collect all virtual nodes 175 mDelegates.clear(); 176 mNodes.clear(); 177 178 Folder openFolder = Folder.getOpen(mLauncher); 179 PagedView pv = openFolder == null ? mLauncher.getWorkspace() : openFolder.getContent(); 180 int count = pv.getPageCount(); 181 for (int i = 0; i < count; i++) { 182 mDelegates.add(((CellLayout) pv.getChildAt(i)).getDragAndDropAccessibilityDelegate()); 183 } 184 if (openFolder == null) { 185 mDelegates.add(pv.getNextPage() + 1, 186 mLauncher.getHotseat().getDragAndDropAccessibilityDelegate()); 187 } 188 mDelegates.forEach(delegate -> { 189 mIntList.clear(); 190 delegate.getVisibleVirtualViews(mIntList); 191 mIntList.forEach(id -> mNodes.add(new VirtualNodeInfo(delegate, id))); 192 }); 193 194 if (mNodes.isEmpty()) { 195 return null; 196 } 197 int index = mNodes.indexOf(mCurrentSelection); 198 if (mCurrentSelection == null || index < 0) { 199 return null; 200 } 201 int totalNodes = mNodes.size(); 202 203 final ToIntBiFunction<Rect, Rect> majorAxis; 204 final ToIntFunction<Rect> minorAxis; 205 206 switch (direction) { 207 case View.FOCUS_RIGHT: 208 majorAxis = (source, dest) -> dest.left - source.left; 209 minorAxis = Rect::centerY; 210 break; 211 case View.FOCUS_LEFT: 212 majorAxis = (source, dest) -> source.left - dest.left; 213 minorAxis = Rect::centerY; 214 break; 215 case View.FOCUS_UP: 216 majorAxis = (source, dest) -> source.top - dest.top; 217 minorAxis = Rect::centerX; 218 break; 219 case View.FOCUS_DOWN: 220 majorAxis = (source, dest) -> dest.top - source.top; 221 minorAxis = Rect::centerX; 222 break; 223 case View.FOCUS_FORWARD: 224 return mNodes.get((index + 1) % totalNodes); 225 case View.FOCUS_BACKWARD: 226 return mNodes.get((index + totalNodes - 1) % totalNodes); 227 default: 228 // Unknown direction 229 return null; 230 } 231 mCurrentSelection.populate(mTempNodeInfo).getBoundsInScreen(mTempRect); 232 233 float minWeight = Float.MAX_VALUE; 234 VirtualNodeInfo match = null; 235 for (int i = 0; i < totalNodes; i++) { 236 VirtualNodeInfo node = mNodes.get(i); 237 node.populate(mTempNodeInfo).getBoundsInScreen(mTempRect2); 238 239 int majorAxisWeight = majorAxis.applyAsInt(mTempRect, mTempRect2); 240 if (majorAxisWeight <= 0) { 241 continue; 242 } 243 int minorAxisWeight = minorAxis.applyAsInt(mTempRect2) 244 - minorAxis.applyAsInt(mTempRect); 245 246 float weight = majorAxisWeight * majorAxisWeight 247 + minorAxisWeight * minorAxisWeight * MINOR_AXIS_WEIGHT; 248 if (weight < minWeight) { 249 minWeight = weight; 250 match = node; 251 } 252 } 253 return match; 254 } 255 256 @Override onKeyUp(int keyCode, KeyEvent event)257 public boolean onKeyUp(int keyCode, KeyEvent event) { 258 if (keyCode == KeyEvent.KEYCODE_ENTER && mCurrentSelection != null) { 259 mCurrentSelection.delegate.onPerformActionForVirtualView( 260 mCurrentSelection.id, AccessibilityNodeInfoCompat.ACTION_CLICK, null); 261 return true; 262 } 263 return super.onKeyUp(keyCode, event); 264 } 265 266 /** 267 * Shows the keyboard drag popup for the provided view 268 */ showForIcon(View icon, ItemInfo item, DragOptions dragOptions)269 public void showForIcon(View icon, ItemInfo item, DragOptions dragOptions) { 270 mIsOpen = true; 271 mLauncher.getDragLayer().addView(this); 272 mLauncher.getStateManager().addStateListener(this); 273 274 // Find current selection 275 CellLayout currentParent = (CellLayout) icon.getParent().getParent(); 276 float[] iconPos = new float[] {currentParent.getCellWidth() / 2, 277 currentParent.getCellHeight() / 2}; 278 Utilities.getDescendantCoordRelativeToAncestor(icon, currentParent, iconPos, false); 279 280 ItemLongClickListener.beginDrag(icon, mLauncher, item, dragOptions); 281 282 DragAndDropAccessibilityDelegate dndDelegate = 283 currentParent.getDragAndDropAccessibilityDelegate(); 284 setCurrentSelection(new VirtualNodeInfo( 285 dndDelegate, dndDelegate.getVirtualViewAt(iconPos[0], iconPos[1]))); 286 287 mLauncher.setDefaultKeyMode(Activity.DEFAULT_KEYS_DISABLE); 288 requestFocus(); 289 } 290 291 private static class VirtualNodeInfo { 292 public final DragAndDropAccessibilityDelegate delegate; 293 public final int id; 294 VirtualNodeInfo(DragAndDropAccessibilityDelegate delegate, int id)295 VirtualNodeInfo(DragAndDropAccessibilityDelegate delegate, int id) { 296 this.id = id; 297 this.delegate = delegate; 298 } 299 300 @Override equals(Object o)301 public boolean equals(Object o) { 302 if (this == o) { 303 return true; 304 } 305 if (!(o instanceof VirtualNodeInfo)) { 306 return false; 307 } 308 VirtualNodeInfo that = (VirtualNodeInfo) o; 309 return id == that.id && delegate.equals(that.delegate); 310 } 311 populate(AccessibilityNodeInfoCompat nodeInfo)312 public AccessibilityNodeInfoCompat populate(AccessibilityNodeInfoCompat nodeInfo) { 313 delegate.onPopulateNodeForVirtualView(id, nodeInfo); 314 return nodeInfo; 315 } 316 getBounds(AccessibilityNodeInfoCompat nodeInfo, Rect out)317 public void getBounds(AccessibilityNodeInfoCompat nodeInfo, Rect out) { 318 delegate.onPopulateNodeForVirtualView(id, nodeInfo); 319 nodeInfo.getBoundsInScreen(out); 320 } 321 322 @Override hashCode()323 public int hashCode() { 324 return Objects.hash(id, delegate); 325 } 326 } 327 328 private static class RectFocusIndicator extends ItemFocusIndicatorHelper<Rect> { 329 RectFocusIndicator(View container)330 RectFocusIndicator(View container) { 331 super(container, Themes.getColorAccent(container.getContext())); 332 mPaint.setStrokeWidth(container.getResources() 333 .getDimension(R.dimen.keyboard_drag_stroke_width)); 334 mPaint.setStyle(Style.STROKE); 335 } 336 337 @Override viewToRect(Rect item, Rect outRect)338 public void viewToRect(Rect item, Rect outRect) { 339 outRect.set(item); 340 } 341 } 342 } 343