1 /* 2 * Copyright (C) 2018 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 17 package com.android.launcher3.widget; 18 19 import android.appwidget.AppWidgetHostView; 20 import android.appwidget.AppWidgetProviderInfo; 21 import android.content.Context; 22 import android.graphics.PointF; 23 import android.graphics.Rect; 24 import android.view.KeyEvent; 25 import android.view.View; 26 import android.view.ViewDebug; 27 import android.view.ViewGroup; 28 29 import com.android.launcher3.BaseActivity; 30 import com.android.launcher3.DeviceProfile; 31 import com.android.launcher3.Reorderable; 32 import com.android.launcher3.dragndrop.DraggableView; 33 import com.android.launcher3.views.ActivityContext; 34 35 import java.util.ArrayList; 36 37 /** 38 * Extension of AppWidgetHostView with support for controlled keyboard navigation. 39 */ 40 public abstract class NavigableAppWidgetHostView extends AppWidgetHostView 41 implements DraggableView, Reorderable { 42 43 /** 44 * The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY. 45 */ 46 private float mScaleToFit = 1f; 47 48 /** 49 * The translation values to center the widget within its cellspans. 50 */ 51 private final PointF mTranslationForCentering = new PointF(0, 0); 52 53 private final PointF mTranslationForReorderBounce = new PointF(0, 0); 54 private final PointF mTranslationForReorderPreview = new PointF(0, 0); 55 private float mScaleForReorderBounce = 1f; 56 57 private final Rect mTempRect = new Rect(); 58 59 @ViewDebug.ExportedProperty(category = "launcher") 60 private boolean mChildrenFocused; 61 62 protected final BaseActivity mActivity; 63 NavigableAppWidgetHostView(Context context)64 public NavigableAppWidgetHostView(Context context) { 65 super(context); 66 mActivity = ActivityContext.lookupContext(context); 67 } 68 69 @Override getDescendantFocusability()70 public int getDescendantFocusability() { 71 return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS 72 : ViewGroup.FOCUS_BLOCK_DESCENDANTS; 73 } 74 75 @Override dispatchKeyEvent(KeyEvent event)76 public boolean dispatchKeyEvent(KeyEvent event) { 77 if (mChildrenFocused && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE 78 && event.getAction() == KeyEvent.ACTION_UP) { 79 mChildrenFocused = false; 80 requestFocus(); 81 return true; 82 } 83 return super.dispatchKeyEvent(event); 84 } 85 86 @Override onKeyDown(int keyCode, KeyEvent event)87 public boolean onKeyDown(int keyCode, KeyEvent event) { 88 if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { 89 event.startTracking(); 90 return true; 91 } 92 return super.onKeyDown(keyCode, event); 93 } 94 95 @Override onKeyUp(int keyCode, KeyEvent event)96 public boolean onKeyUp(int keyCode, KeyEvent event) { 97 if (event.isTracking()) { 98 if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { 99 mChildrenFocused = true; 100 ArrayList<View> focusableChildren = getFocusables(FOCUS_FORWARD); 101 focusableChildren.remove(this); 102 int childrenCount = focusableChildren.size(); 103 switch (childrenCount) { 104 case 0: 105 mChildrenFocused = false; 106 break; 107 case 1: { 108 if (shouldAllowDirectClick()) { 109 focusableChildren.get(0).performClick(); 110 mChildrenFocused = false; 111 return true; 112 } 113 // continue; 114 } 115 default: 116 focusableChildren.get(0).requestFocus(); 117 return true; 118 } 119 } 120 } 121 return super.onKeyUp(keyCode, event); 122 } 123 124 /** 125 * For a widget with only a single interactive element, return true if whole widget should act 126 * as a single interactive element, and clicking 'enter' should activate the child element 127 * directly. Otherwise clicking 'enter' will only move the focus inside the widget. 128 */ shouldAllowDirectClick()129 protected abstract boolean shouldAllowDirectClick(); 130 131 @Override onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)132 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 133 if (gainFocus) { 134 mChildrenFocused = false; 135 dispatchChildFocus(false); 136 } 137 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 138 } 139 140 @Override requestChildFocus(View child, View focused)141 public void requestChildFocus(View child, View focused) { 142 super.requestChildFocus(child, focused); 143 dispatchChildFocus(mChildrenFocused && focused != null); 144 if (focused != null) { 145 focused.setFocusableInTouchMode(false); 146 } 147 } 148 149 @Override clearChildFocus(View child)150 public void clearChildFocus(View child) { 151 super.clearChildFocus(child); 152 dispatchChildFocus(false); 153 } 154 155 @Override dispatchUnhandledMove(View focused, int direction)156 public boolean dispatchUnhandledMove(View focused, int direction) { 157 return mChildrenFocused; 158 } 159 dispatchChildFocus(boolean childIsFocused)160 private void dispatchChildFocus(boolean childIsFocused) { 161 // The host view's background changes when selected, to indicate the focus is inside. 162 setSelected(childIsFocused); 163 } 164 getView()165 public View getView() { 166 return this; 167 } 168 updateTranslation()169 private void updateTranslation() { 170 super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x 171 + mTranslationForCentering.x); 172 super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y 173 + mTranslationForCentering.y); 174 } 175 setTranslationForCentering(float x, float y)176 public void setTranslationForCentering(float x, float y) { 177 mTranslationForCentering.set(x, y); 178 updateTranslation(); 179 } 180 setReorderBounceOffset(float x, float y)181 public void setReorderBounceOffset(float x, float y) { 182 mTranslationForReorderBounce.set(x, y); 183 updateTranslation(); 184 } 185 getReorderBounceOffset(PointF offset)186 public void getReorderBounceOffset(PointF offset) { 187 offset.set(mTranslationForReorderBounce); 188 } 189 190 @Override setReorderPreviewOffset(float x, float y)191 public void setReorderPreviewOffset(float x, float y) { 192 mTranslationForReorderPreview.set(x, y); 193 updateTranslation(); 194 } 195 196 @Override getReorderPreviewOffset(PointF offset)197 public void getReorderPreviewOffset(PointF offset) { 198 offset.set(mTranslationForReorderPreview); 199 } 200 updateScale()201 private void updateScale() { 202 super.setScaleX(mScaleToFit * mScaleForReorderBounce); 203 super.setScaleY(mScaleToFit * mScaleForReorderBounce); 204 } 205 setReorderBounceScale(float scale)206 public void setReorderBounceScale(float scale) { 207 mScaleForReorderBounce = scale; 208 updateScale(); 209 } 210 getReorderBounceScale()211 public float getReorderBounceScale() { 212 return mScaleForReorderBounce; 213 } 214 setScaleToFit(float scale)215 public void setScaleToFit(float scale) { 216 mScaleToFit = scale; 217 updateScale(); 218 } 219 getScaleToFit()220 public float getScaleToFit() { 221 return mScaleToFit; 222 } 223 224 @Override getViewType()225 public int getViewType() { 226 return DRAGGABLE_WIDGET; 227 } 228 229 @Override getWorkspaceVisualDragBounds(Rect bounds)230 public void getWorkspaceVisualDragBounds(Rect bounds) { 231 int width = (int) (getMeasuredWidth() * mScaleToFit); 232 int height = (int) (getMeasuredHeight() * mScaleToFit); 233 234 getWidgetInset(mActivity.getDeviceProfile(), mTempRect); 235 bounds.set(mTempRect.left, mTempRect.top, width - mTempRect.right, 236 height - mTempRect.bottom); 237 } 238 239 /** 240 * Widgets have padding added by the system. We may choose to inset this padding if the grid 241 * supports it. 242 */ getWidgetInset(DeviceProfile grid, Rect out)243 public void getWidgetInset(DeviceProfile grid, Rect out) { 244 if (!grid.shouldInsetWidgets()) { 245 out.setEmpty(); 246 return; 247 } 248 AppWidgetProviderInfo info = getAppWidgetInfo(); 249 if (info == null) { 250 out.set(grid.inv.defaultWidgetPadding); 251 } else { 252 AppWidgetHostView.getDefaultPaddingForWidget(getContext(), info.provider, out); 253 } 254 } 255 } 256