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.Rect; 23 import android.view.KeyEvent; 24 import android.view.View; 25 import android.view.ViewDebug; 26 import android.view.ViewGroup; 27 28 import com.android.launcher3.Reorderable; 29 import com.android.launcher3.dragndrop.DraggableView; 30 import com.android.launcher3.util.MultiTranslateDelegate; 31 import com.android.launcher3.views.ActivityContext; 32 33 import java.util.ArrayList; 34 35 /** 36 * Extension of AppWidgetHostView with support for controlled keyboard navigation. 37 */ 38 public abstract class NavigableAppWidgetHostView extends AppWidgetHostView 39 implements DraggableView, Reorderable { 40 41 private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this); 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 private float mScaleForReorderBounce = 1f; 49 50 @ViewDebug.ExportedProperty(category = "launcher") 51 private boolean mChildrenFocused; 52 53 protected final ActivityContext mActivity; 54 55 private boolean mDisableSetPadding = false; 56 NavigableAppWidgetHostView(Context context)57 public NavigableAppWidgetHostView(Context context) { 58 super(context); 59 mActivity = ActivityContext.lookupContext(context); 60 } 61 62 @Override getDescendantFocusability()63 public int getDescendantFocusability() { 64 return mChildrenFocused ? ViewGroup.FOCUS_BEFORE_DESCENDANTS 65 : ViewGroup.FOCUS_BLOCK_DESCENDANTS; 66 } 67 68 @Override dispatchKeyEvent(KeyEvent event)69 public boolean dispatchKeyEvent(KeyEvent event) { 70 if (mChildrenFocused && event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE 71 && event.getAction() == KeyEvent.ACTION_UP) { 72 mChildrenFocused = false; 73 requestFocus(); 74 return true; 75 } 76 return super.dispatchKeyEvent(event); 77 } 78 79 @Override onKeyDown(int keyCode, KeyEvent event)80 public boolean onKeyDown(int keyCode, KeyEvent event) { 81 if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { 82 event.startTracking(); 83 return true; 84 } 85 return super.onKeyDown(keyCode, event); 86 } 87 88 @Override onKeyUp(int keyCode, KeyEvent event)89 public boolean onKeyUp(int keyCode, KeyEvent event) { 90 if (event.isTracking()) { 91 if (!mChildrenFocused && keyCode == KeyEvent.KEYCODE_ENTER) { 92 mChildrenFocused = true; 93 ArrayList<View> focusableChildren = getFocusables(FOCUS_FORWARD); 94 focusableChildren.remove(this); 95 int childrenCount = focusableChildren.size(); 96 switch (childrenCount) { 97 case 0: 98 mChildrenFocused = false; 99 break; 100 case 1: { 101 if (shouldAllowDirectClick()) { 102 focusableChildren.get(0).performClick(); 103 mChildrenFocused = false; 104 return true; 105 } 106 // continue; 107 } 108 default: 109 focusableChildren.get(0).requestFocus(); 110 return true; 111 } 112 } 113 } 114 return super.onKeyUp(keyCode, event); 115 } 116 117 /** 118 * For a widget with only a single interactive element, return true if whole widget should act 119 * as a single interactive element, and clicking 'enter' should activate the child element 120 * directly. Otherwise clicking 'enter' will only move the focus inside the widget. 121 */ shouldAllowDirectClick()122 protected abstract boolean shouldAllowDirectClick(); 123 124 @Override onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)125 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 126 if (gainFocus) { 127 mChildrenFocused = false; 128 dispatchChildFocus(false); 129 } 130 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 131 } 132 133 @Override requestChildFocus(View child, View focused)134 public void requestChildFocus(View child, View focused) { 135 super.requestChildFocus(child, focused); 136 dispatchChildFocus(mChildrenFocused && focused != null); 137 if (focused != null) { 138 focused.setFocusableInTouchMode(false); 139 } 140 } 141 142 @Override clearChildFocus(View child)143 public void clearChildFocus(View child) { 144 super.clearChildFocus(child); 145 dispatchChildFocus(false); 146 } 147 148 @Override setAppWidget(int appWidgetId, AppWidgetProviderInfo info)149 public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) { 150 // Prevent default padding being set on the view based on provider info. Launcher manages 151 // its own widget spacing 152 mDisableSetPadding = true; 153 super.setAppWidget(appWidgetId, info); 154 mDisableSetPadding = false; 155 } 156 157 @Override setPadding(int left, int top, int right, int bottom)158 public void setPadding(int left, int top, int right, int bottom) { 159 if (!mDisableSetPadding) { 160 super.setPadding(left, top, right, bottom); 161 } 162 } 163 164 @Override dispatchUnhandledMove(View focused, int direction)165 public boolean dispatchUnhandledMove(View focused, int direction) { 166 return mChildrenFocused; 167 } 168 dispatchChildFocus(boolean childIsFocused)169 private void dispatchChildFocus(boolean childIsFocused) { 170 // The host view's background changes when selected, to indicate the focus is inside. 171 setSelected(childIsFocused); 172 } 173 updateScale()174 private void updateScale() { 175 super.setScaleX(mScaleToFit * mScaleForReorderBounce); 176 super.setScaleY(mScaleToFit * mScaleForReorderBounce); 177 } 178 179 @Override getTranslateDelegate()180 public MultiTranslateDelegate getTranslateDelegate() { 181 return mTranslateDelegate; 182 } 183 184 @Override setReorderBounceScale(float scale)185 public void setReorderBounceScale(float scale) { 186 mScaleForReorderBounce = scale; 187 updateScale(); 188 } 189 190 @Override getReorderBounceScale()191 public float getReorderBounceScale() { 192 return mScaleForReorderBounce; 193 } 194 setScaleToFit(float scale)195 public void setScaleToFit(float scale) { 196 mScaleToFit = scale; 197 updateScale(); 198 } 199 getScaleToFit()200 public float getScaleToFit() { 201 return mScaleToFit; 202 } 203 204 @Override getViewType()205 public int getViewType() { 206 return DRAGGABLE_WIDGET; 207 } 208 209 @Override getWorkspaceVisualDragBounds(Rect bounds)210 public void getWorkspaceVisualDragBounds(Rect bounds) { 211 int width = (int) (getMeasuredWidth() * mScaleToFit); 212 int height = (int) (getMeasuredHeight() * mScaleToFit); 213 bounds.set(0, 0, width, height); 214 } 215 } 216