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