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