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