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