1 /*
2  * Copyright (C) 2015 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.internal.view;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.view.ActionMode;
22 import android.view.Menu;
23 import android.view.MenuInflater;
24 import android.view.MenuItem;
25 import android.view.View;
26 import android.view.ViewConfiguration;
27 import android.view.ViewGroup;
28 import android.view.ViewParent;
29 
30 import com.android.internal.R;
31 import com.android.internal.util.Preconditions;
32 import com.android.internal.view.menu.MenuBuilder;
33 import com.android.internal.widget.FloatingToolbar;
34 
35 import java.util.Arrays;
36 
37 public class FloatingActionMode extends ActionMode {
38 
39     private static final int MAX_HIDE_DURATION = 3000;
40     private static final int MOVING_HIDE_DELAY = 50;
41 
42     private final Context mContext;
43     private final ActionMode.Callback2 mCallback;
44     private final MenuBuilder mMenu;
45     private final Rect mContentRect;
46     private final Rect mContentRectOnScreen;
47     private final Rect mPreviousContentRectOnScreen;
48     private final int[] mViewPositionOnScreen;
49     private final int[] mPreviousViewPositionOnScreen;
50     private final int[] mRootViewPositionOnScreen;
51     private final Rect mViewRectOnScreen;
52     private final Rect mPreviousViewRectOnScreen;
53     private final Rect mScreenRect;
54     private final View mOriginatingView;
55     private final int mBottomAllowance;
56 
57     private final Runnable mMovingOff = new Runnable() {
58         public void run() {
59             mFloatingToolbarVisibilityHelper.setMoving(false);
60             mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
61         }
62     };
63 
64     private final Runnable mHideOff = new Runnable() {
65         public void run() {
66             mFloatingToolbarVisibilityHelper.setHideRequested(false);
67             mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
68         }
69     };
70 
71     private FloatingToolbar mFloatingToolbar;
72     private FloatingToolbarVisibilityHelper mFloatingToolbarVisibilityHelper;
73 
FloatingActionMode( Context context, ActionMode.Callback2 callback, View originatingView)74     public FloatingActionMode(
75             Context context, ActionMode.Callback2 callback, View originatingView) {
76         mContext = Preconditions.checkNotNull(context);
77         mCallback = Preconditions.checkNotNull(callback);
78         mMenu = new MenuBuilder(context).setDefaultShowAsAction(
79                 MenuItem.SHOW_AS_ACTION_IF_ROOM);
80         setType(ActionMode.TYPE_FLOATING);
81         mMenu.setCallback(new MenuBuilder.Callback() {
82             @Override
83             public void onMenuModeChange(MenuBuilder menu) {}
84 
85             @Override
86             public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
87                 return mCallback.onActionItemClicked(FloatingActionMode.this, item);
88             }
89         });
90         mContentRect = new Rect();
91         mContentRectOnScreen = new Rect();
92         mPreviousContentRectOnScreen = new Rect();
93         mViewPositionOnScreen = new int[2];
94         mPreviousViewPositionOnScreen = new int[2];
95         mRootViewPositionOnScreen = new int[2];
96         mViewRectOnScreen = new Rect();
97         mPreviousViewRectOnScreen = new Rect();
98         mScreenRect = new Rect();
99         mOriginatingView = Preconditions.checkNotNull(originatingView);
100         mOriginatingView.getLocationOnScreen(mViewPositionOnScreen);
101         // Allow the content rect to overshoot a little bit beyond the
102         // bottom view bound if necessary.
103         mBottomAllowance = context.getResources()
104                 .getDimensionPixelSize(R.dimen.content_rect_bottom_clip_allowance);
105     }
106 
setFloatingToolbar(FloatingToolbar floatingToolbar)107     public void setFloatingToolbar(FloatingToolbar floatingToolbar) {
108         mFloatingToolbar = floatingToolbar
109                 .setMenu(mMenu)
110                 .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
111                         @Override
112                     public boolean onMenuItemClick(MenuItem item) {
113                         return mMenu.performItemAction(item, 0);
114                     }
115                 });
116         mFloatingToolbarVisibilityHelper = new FloatingToolbarVisibilityHelper(mFloatingToolbar);
117         mFloatingToolbarVisibilityHelper.activate();
118     }
119 
120     @Override
setTitle(CharSequence title)121     public void setTitle(CharSequence title) {}
122 
123     @Override
setTitle(int resId)124     public void setTitle(int resId) {}
125 
126     @Override
setSubtitle(CharSequence subtitle)127     public void setSubtitle(CharSequence subtitle) {}
128 
129     @Override
setSubtitle(int resId)130     public void setSubtitle(int resId) {}
131 
132     @Override
setCustomView(View view)133     public void setCustomView(View view) {}
134 
135     @Override
invalidate()136     public void invalidate() {
137         checkToolbarInitialized();
138         mCallback.onPrepareActionMode(this, mMenu);
139         invalidateContentRect();  // Will re-layout and show the toolbar if necessary.
140     }
141 
142     @Override
invalidateContentRect()143     public void invalidateContentRect() {
144         checkToolbarInitialized();
145         mCallback.onGetContentRect(this, mOriginatingView, mContentRect);
146         repositionToolbar();
147     }
148 
updateViewLocationInWindow()149     public void updateViewLocationInWindow() {
150         checkToolbarInitialized();
151 
152         mOriginatingView.getLocationOnScreen(mViewPositionOnScreen);
153         mOriginatingView.getRootView().getLocationOnScreen(mRootViewPositionOnScreen);
154         mOriginatingView.getGlobalVisibleRect(mViewRectOnScreen);
155         mViewRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]);
156 
157         if (!Arrays.equals(mViewPositionOnScreen, mPreviousViewPositionOnScreen)
158                 || !mViewRectOnScreen.equals(mPreviousViewRectOnScreen)) {
159             repositionToolbar();
160             mPreviousViewPositionOnScreen[0] = mViewPositionOnScreen[0];
161             mPreviousViewPositionOnScreen[1] = mViewPositionOnScreen[1];
162             mPreviousViewRectOnScreen.set(mViewRectOnScreen);
163         }
164     }
165 
repositionToolbar()166     private void repositionToolbar() {
167         checkToolbarInitialized();
168 
169         mContentRectOnScreen.set(mContentRect);
170 
171         // Offset the content rect into screen coordinates, taking into account any transformations
172         // that may be applied to the originating view or its ancestors.
173         final ViewParent parent = mOriginatingView.getParent();
174         if (parent instanceof ViewGroup) {
175             ((ViewGroup) parent).getChildVisibleRect(
176                     mOriginatingView, mContentRectOnScreen,
177                     null /* offset */, true /* forceParentCheck */);
178             mContentRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]);
179         } else {
180             mContentRectOnScreen.offset(mViewPositionOnScreen[0], mViewPositionOnScreen[1]);
181         }
182 
183         if (isContentRectWithinBounds()) {
184             mFloatingToolbarVisibilityHelper.setOutOfBounds(false);
185             // Make sure that content rect is not out of the view's visible bounds.
186             mContentRectOnScreen.set(
187                     Math.max(mContentRectOnScreen.left, mViewRectOnScreen.left),
188                     Math.max(mContentRectOnScreen.top, mViewRectOnScreen.top),
189                     Math.min(mContentRectOnScreen.right, mViewRectOnScreen.right),
190                     Math.min(mContentRectOnScreen.bottom,
191                             mViewRectOnScreen.bottom + mBottomAllowance));
192 
193             if (!mContentRectOnScreen.equals(mPreviousContentRectOnScreen)) {
194                 // Content rect is moving.
195                 mOriginatingView.removeCallbacks(mMovingOff);
196                 mFloatingToolbarVisibilityHelper.setMoving(true);
197                 mOriginatingView.postDelayed(mMovingOff, MOVING_HIDE_DELAY);
198 
199                 mFloatingToolbar.setContentRect(mContentRectOnScreen);
200                 mFloatingToolbar.updateLayout();
201             }
202         } else {
203             mFloatingToolbarVisibilityHelper.setOutOfBounds(true);
204             mContentRectOnScreen.setEmpty();
205         }
206         mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
207 
208         mPreviousContentRectOnScreen.set(mContentRectOnScreen);
209     }
210 
isContentRectWithinBounds()211     private boolean isContentRectWithinBounds() {
212         mScreenRect.set(
213             0,
214             0,
215             mContext.getResources().getDisplayMetrics().widthPixels,
216             mContext.getResources().getDisplayMetrics().heightPixels);
217 
218         return intersectsClosed(mContentRectOnScreen, mScreenRect)
219             && intersectsClosed(mContentRectOnScreen, mViewRectOnScreen);
220     }
221 
222     /*
223      * Same as Rect.intersects, but includes cases where the rectangles touch.
224     */
intersectsClosed(Rect a, Rect b)225     private static boolean intersectsClosed(Rect a, Rect b) {
226          return a.left <= b.right && b.left <= a.right
227                  && a.top <= b.bottom && b.top <= a.bottom;
228     }
229 
230     @Override
hide(long duration)231     public void hide(long duration) {
232         checkToolbarInitialized();
233 
234         if (duration == ActionMode.DEFAULT_HIDE_DURATION) {
235             duration = ViewConfiguration.getDefaultActionModeHideDuration();
236         }
237         duration = Math.min(MAX_HIDE_DURATION, duration);
238         mOriginatingView.removeCallbacks(mHideOff);
239         if (duration <= 0) {
240             mHideOff.run();
241         } else {
242             mFloatingToolbarVisibilityHelper.setHideRequested(true);
243             mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
244             mOriginatingView.postDelayed(mHideOff, duration);
245         }
246     }
247 
248     @Override
onWindowFocusChanged(boolean hasWindowFocus)249     public void onWindowFocusChanged(boolean hasWindowFocus) {
250         checkToolbarInitialized();
251         mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus);
252         mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
253     }
254 
255     @Override
finish()256     public void finish() {
257         checkToolbarInitialized();
258         reset();
259         mCallback.onDestroyActionMode(this);
260     }
261 
262     @Override
getMenu()263     public Menu getMenu() {
264         return mMenu;
265     }
266 
267     @Override
getTitle()268     public CharSequence getTitle() {
269         return null;
270     }
271 
272     @Override
getSubtitle()273     public CharSequence getSubtitle() {
274         return null;
275     }
276 
277     @Override
getCustomView()278     public View getCustomView() {
279         return null;
280     }
281 
282     @Override
getMenuInflater()283     public MenuInflater getMenuInflater() {
284         return new MenuInflater(mContext);
285     }
286 
287     /**
288      * @throws IllegalStateException
289      */
checkToolbarInitialized()290     private void checkToolbarInitialized() {
291         Preconditions.checkState(mFloatingToolbar != null);
292         Preconditions.checkState(mFloatingToolbarVisibilityHelper != null);
293     }
294 
reset()295     private void reset() {
296         mFloatingToolbar.dismiss();
297         mFloatingToolbarVisibilityHelper.deactivate();
298         mOriginatingView.removeCallbacks(mMovingOff);
299         mOriginatingView.removeCallbacks(mHideOff);
300     }
301 
302     /**
303      * A helper for showing/hiding the floating toolbar depending on certain states.
304      */
305     private static final class FloatingToolbarVisibilityHelper {
306 
307         private final FloatingToolbar mToolbar;
308 
309         private boolean mHideRequested;
310         private boolean mMoving;
311         private boolean mOutOfBounds;
312         private boolean mWindowFocused = true;
313 
314         private boolean mActive;
315 
FloatingToolbarVisibilityHelper(FloatingToolbar toolbar)316         public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) {
317             mToolbar = Preconditions.checkNotNull(toolbar);
318         }
319 
activate()320         public void activate() {
321             mHideRequested = false;
322             mMoving = false;
323             mOutOfBounds = false;
324             mWindowFocused = true;
325 
326             mActive = true;
327         }
328 
deactivate()329         public void deactivate() {
330             mActive = false;
331             mToolbar.dismiss();
332         }
333 
setHideRequested(boolean hide)334         public void setHideRequested(boolean hide) {
335             mHideRequested = hide;
336         }
337 
setMoving(boolean moving)338         public void setMoving(boolean moving) {
339             mMoving = moving;
340         }
341 
setOutOfBounds(boolean outOfBounds)342         public void setOutOfBounds(boolean outOfBounds) {
343             mOutOfBounds = outOfBounds;
344         }
345 
setWindowFocused(boolean windowFocused)346         public void setWindowFocused(boolean windowFocused) {
347             mWindowFocused = windowFocused;
348         }
349 
updateToolbarVisibility()350         public void updateToolbarVisibility() {
351             if (!mActive) {
352                 return;
353             }
354 
355             if (mHideRequested || mMoving || mOutOfBounds || !mWindowFocused) {
356                 mToolbar.hide();
357             } else {
358                 mToolbar.show();
359             }
360         }
361     }
362 }
363