• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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