• 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 import android.widget.PopupWindow;
33 
34 import com.android.internal.R;
35 import com.android.internal.view.menu.MenuBuilder;
36 import com.android.internal.widget.floatingtoolbar.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         updateViewLocationInWindow(/* forceRepositionToolbar= */ true);
152     }
153 
updateViewLocationInWindow()154     public void updateViewLocationInWindow() {
155         updateViewLocationInWindow(/* forceRepositionToolbar= */ false);
156     }
157 
updateViewLocationInWindow(boolean forceRepositionToolbar)158     private void updateViewLocationInWindow(boolean forceRepositionToolbar) {
159         mOriginatingView.getLocationOnScreen(mViewPositionOnScreen);
160         mOriginatingView.getRootView().getLocationOnScreen(mRootViewPositionOnScreen);
161         mOriginatingView.getGlobalVisibleRect(mViewRectOnScreen);
162         mViewRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]);
163 
164         if (forceRepositionToolbar
165                 || !Arrays.equals(mViewPositionOnScreen, mPreviousViewPositionOnScreen)
166                 || !mViewRectOnScreen.equals(mPreviousViewRectOnScreen)) {
167             repositionToolbar();
168             mPreviousViewPositionOnScreen[0] = mViewPositionOnScreen[0];
169             mPreviousViewPositionOnScreen[1] = mViewPositionOnScreen[1];
170             mPreviousViewRectOnScreen.set(mViewRectOnScreen);
171         }
172     }
173 
repositionToolbar()174     private void repositionToolbar() {
175         mContentRectOnScreen.set(mContentRect);
176 
177         // Offset the content rect into screen coordinates, taking into account any transformations
178         // that may be applied to the originating view or its ancestors.
179         final ViewParent parent = mOriginatingView.getParent();
180         if (parent instanceof ViewGroup) {
181             ((ViewGroup) parent).getChildVisibleRect(
182                     mOriginatingView, mContentRectOnScreen,
183                     null /* offset */, true /* forceParentCheck */);
184             mContentRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]);
185         } else {
186             mContentRectOnScreen.offset(mViewPositionOnScreen[0], mViewPositionOnScreen[1]);
187         }
188 
189         if (isContentRectWithinBounds()) {
190             mFloatingToolbarVisibilityHelper.setOutOfBounds(false);
191             // Make sure that content rect is not out of the view's visible bounds.
192             mContentRectOnScreen.set(
193                     Math.max(mContentRectOnScreen.left, mViewRectOnScreen.left),
194                     Math.max(mContentRectOnScreen.top, mViewRectOnScreen.top),
195                     Math.min(mContentRectOnScreen.right, mViewRectOnScreen.right),
196                     Math.min(mContentRectOnScreen.bottom,
197                             mViewRectOnScreen.bottom + mBottomAllowance));
198 
199             if (!mContentRectOnScreen.equals(mPreviousContentRectOnScreen)) {
200                 // Content rect is moving
201                 if (!mPreviousContentRectOnScreen.isEmpty()) {
202                     mOriginatingView.removeCallbacks(mMovingOff);
203                     mFloatingToolbarVisibilityHelper.setMoving(true);
204                     mOriginatingView.postDelayed(mMovingOff, MOVING_HIDE_DELAY);
205                 } else {
206                     // mPreviousContentRectOnScreen is empty. That means we are are showing the
207                     // toolbar rather than moving it. And we should show it right away.
208                 }
209 
210                 mFloatingToolbar.setContentRect(mContentRectOnScreen);
211                 mFloatingToolbar.updateLayout();
212             }
213         } else {
214             mFloatingToolbarVisibilityHelper.setOutOfBounds(true);
215             mContentRectOnScreen.setEmpty();
216         }
217         mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
218 
219         mPreviousContentRectOnScreen.set(mContentRectOnScreen);
220     }
221 
isContentRectWithinBounds()222     private boolean isContentRectWithinBounds() {
223         mContext.getDisplayNoVerify().getRealSize(mDisplaySize);
224         mScreenRect.set(0, 0, mDisplaySize.x, mDisplaySize.y);
225         mScreenRect.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]);
226 
227         return intersectsClosed(mContentRectOnScreen, mScreenRect)
228             && intersectsClosed(mContentRectOnScreen, mViewRectOnScreen);
229     }
230 
231     /*
232      * Same as Rect.intersects, but includes cases where the rectangles touch.
233     */
intersectsClosed(Rect a, Rect b)234     private static boolean intersectsClosed(Rect a, Rect b) {
235          return a.left <= b.right && b.left <= a.right
236                  && a.top <= b.bottom && b.top <= a.bottom;
237     }
238 
239     @Override
hide(long duration)240     public void hide(long duration) {
241         if (duration == ActionMode.DEFAULT_HIDE_DURATION) {
242             duration = ViewConfiguration.getDefaultActionModeHideDuration();
243         }
244         duration = Math.min(MAX_HIDE_DURATION, duration);
245         mOriginatingView.removeCallbacks(mHideOff);
246         if (duration <= 0) {
247             mHideOff.run();
248         } else {
249             mFloatingToolbarVisibilityHelper.setHideRequested(true);
250             mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
251             mOriginatingView.postDelayed(mHideOff, duration);
252         }
253     }
254 
255     /**
256      * If this is set to true, the action mode view will dismiss itself on touch events outside of
257      * its window. This only makes sense if the action mode view is a PopupWindow that is touchable
258      * but not focusable, which means touches outside of the window will be delivered to the window
259      * behind. The default is false.
260      *
261      * This is for internal use only and the approach to this may change.
262      * @hide
263      *
264      * @param outsideTouchable whether or not this action mode is "outside touchable"
265      * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself
266      */
setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss)267     public void setOutsideTouchable(
268             boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
269         mFloatingToolbar.setOutsideTouchable(outsideTouchable, onDismiss);
270     }
271 
272     @Override
onWindowFocusChanged(boolean hasWindowFocus)273     public void onWindowFocusChanged(boolean hasWindowFocus) {
274         mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus);
275         mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
276     }
277 
278     @Override
finish()279     public void finish() {
280         reset();
281         mCallback.onDestroyActionMode(this);
282     }
283 
284     @Override
getMenu()285     public Menu getMenu() {
286         return mMenu;
287     }
288 
289     @Override
getTitle()290     public CharSequence getTitle() {
291         return null;
292     }
293 
294     @Override
getSubtitle()295     public CharSequence getSubtitle() {
296         return null;
297     }
298 
299     @Override
getCustomView()300     public View getCustomView() {
301         return null;
302     }
303 
304     @Override
getMenuInflater()305     public MenuInflater getMenuInflater() {
306         return new MenuInflater(mContext);
307     }
308 
reset()309     private void reset() {
310         mFloatingToolbar.dismiss();
311         mFloatingToolbarVisibilityHelper.deactivate();
312         mOriginatingView.removeCallbacks(mMovingOff);
313         mOriginatingView.removeCallbacks(mHideOff);
314     }
315 
isViewStillActive()316     private boolean isViewStillActive() {
317         return mOriginatingView.getWindowVisibility() == View.VISIBLE
318                 && mOriginatingView.isShown();
319     }
320 
321     /**
322      * A helper for showing/hiding the floating toolbar depending on certain states.
323      */
324     private static final class FloatingToolbarVisibilityHelper {
325 
326         private static final long MIN_SHOW_DURATION_FOR_MOVE_HIDE = 500;
327 
328         private final FloatingToolbar mToolbar;
329 
330         private boolean mHideRequested;
331         private boolean mMoving;
332         private boolean mOutOfBounds;
333         private boolean mWindowFocused = true;
334 
335         private boolean mActive;
336 
337         private long mLastShowTime;
338 
FloatingToolbarVisibilityHelper(FloatingToolbar toolbar)339         public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) {
340             mToolbar = Objects.requireNonNull(toolbar);
341         }
342 
activate()343         public void activate() {
344             mHideRequested = false;
345             mMoving = false;
346             mOutOfBounds = false;
347             mWindowFocused = true;
348 
349             mActive = true;
350         }
351 
deactivate()352         public void deactivate() {
353             mActive = false;
354             mToolbar.dismiss();
355         }
356 
setHideRequested(boolean hide)357         public void setHideRequested(boolean hide) {
358             mHideRequested = hide;
359         }
360 
setMoving(boolean moving)361         public void setMoving(boolean moving) {
362             // Avoid unintended flickering by allowing the toolbar to show long enough before
363             // triggering the 'moving' flag - which signals a hide.
364             final boolean showingLongEnough =
365                 System.currentTimeMillis() - mLastShowTime > MIN_SHOW_DURATION_FOR_MOVE_HIDE;
366             if (!moving || showingLongEnough) {
367                 mMoving = moving;
368             }
369         }
370 
setOutOfBounds(boolean outOfBounds)371         public void setOutOfBounds(boolean outOfBounds) {
372             mOutOfBounds = outOfBounds;
373         }
374 
setWindowFocused(boolean windowFocused)375         public void setWindowFocused(boolean windowFocused) {
376             mWindowFocused = windowFocused;
377         }
378 
updateToolbarVisibility()379         public void updateToolbarVisibility() {
380             if (!mActive) {
381                 return;
382             }
383 
384             if (mHideRequested || mMoving || mOutOfBounds || !mWindowFocused) {
385                 mToolbar.hide();
386             } else {
387                 mToolbar.show();
388                 mLastShowTime = System.currentTimeMillis();
389             }
390         }
391     }
392 }
393