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