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 
226         return intersectsClosed(mContentRectOnScreen, mScreenRect)
227             && intersectsClosed(mContentRectOnScreen, mViewRectOnScreen);
228     }
229 
230     /*
231      * Same as Rect.intersects, but includes cases where the rectangles touch.
232     */
intersectsClosed(Rect a, Rect b)233     private static boolean intersectsClosed(Rect a, Rect b) {
234          return a.left <= b.right && b.left <= a.right
235                  && a.top <= b.bottom && b.top <= a.bottom;
236     }
237 
238     @Override
hide(long duration)239     public void hide(long duration) {
240         if (duration == ActionMode.DEFAULT_HIDE_DURATION) {
241             duration = ViewConfiguration.getDefaultActionModeHideDuration();
242         }
243         duration = Math.min(MAX_HIDE_DURATION, duration);
244         mOriginatingView.removeCallbacks(mHideOff);
245         if (duration <= 0) {
246             mHideOff.run();
247         } else {
248             mFloatingToolbarVisibilityHelper.setHideRequested(true);
249             mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
250             mOriginatingView.postDelayed(mHideOff, duration);
251         }
252     }
253 
254     /**
255      * If this is set to true, the action mode view will dismiss itself on touch events outside of
256      * its window. This only makes sense if the action mode view is a PopupWindow that is touchable
257      * but not focusable, which means touches outside of the window will be delivered to the window
258      * behind. The default is false.
259      *
260      * This is for internal use only and the approach to this may change.
261      * @hide
262      *
263      * @param outsideTouchable whether or not this action mode is "outside touchable"
264      * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself
265      */
setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss)266     public void setOutsideTouchable(
267             boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
268         mFloatingToolbar.setOutsideTouchable(outsideTouchable, onDismiss);
269     }
270 
271     @Override
onWindowFocusChanged(boolean hasWindowFocus)272     public void onWindowFocusChanged(boolean hasWindowFocus) {
273         mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus);
274         mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
275     }
276 
277     @Override
finish()278     public void finish() {
279         reset();
280         mCallback.onDestroyActionMode(this);
281     }
282 
283     @Override
getMenu()284     public Menu getMenu() {
285         return mMenu;
286     }
287 
288     @Override
getTitle()289     public CharSequence getTitle() {
290         return null;
291     }
292 
293     @Override
getSubtitle()294     public CharSequence getSubtitle() {
295         return null;
296     }
297 
298     @Override
getCustomView()299     public View getCustomView() {
300         return null;
301     }
302 
303     @Override
getMenuInflater()304     public MenuInflater getMenuInflater() {
305         return new MenuInflater(mContext);
306     }
307 
reset()308     private void reset() {
309         mFloatingToolbar.dismiss();
310         mFloatingToolbarVisibilityHelper.deactivate();
311         mOriginatingView.removeCallbacks(mMovingOff);
312         mOriginatingView.removeCallbacks(mHideOff);
313     }
314 
isViewStillActive()315     private boolean isViewStillActive() {
316         return mOriginatingView.getWindowVisibility() == View.VISIBLE
317                 && mOriginatingView.isShown();
318     }
319 
320     /**
321      * A helper for showing/hiding the floating toolbar depending on certain states.
322      */
323     private static final class FloatingToolbarVisibilityHelper {
324 
325         private static final long MIN_SHOW_DURATION_FOR_MOVE_HIDE = 500;
326 
327         private final FloatingToolbar mToolbar;
328 
329         private boolean mHideRequested;
330         private boolean mMoving;
331         private boolean mOutOfBounds;
332         private boolean mWindowFocused = true;
333 
334         private boolean mActive;
335 
336         private long mLastShowTime;
337 
FloatingToolbarVisibilityHelper(FloatingToolbar toolbar)338         public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) {
339             mToolbar = Objects.requireNonNull(toolbar);
340         }
341 
activate()342         public void activate() {
343             mHideRequested = false;
344             mMoving = false;
345             mOutOfBounds = false;
346             mWindowFocused = true;
347 
348             mActive = true;
349         }
350 
deactivate()351         public void deactivate() {
352             mActive = false;
353             mToolbar.dismiss();
354         }
355 
setHideRequested(boolean hide)356         public void setHideRequested(boolean hide) {
357             mHideRequested = hide;
358         }
359 
setMoving(boolean moving)360         public void setMoving(boolean moving) {
361             // Avoid unintended flickering by allowing the toolbar to show long enough before
362             // triggering the 'moving' flag - which signals a hide.
363             final boolean showingLongEnough =
364                 System.currentTimeMillis() - mLastShowTime > MIN_SHOW_DURATION_FOR_MOVE_HIDE;
365             if (!moving || showingLongEnough) {
366                 mMoving = moving;
367             }
368         }
369 
setOutOfBounds(boolean outOfBounds)370         public void setOutOfBounds(boolean outOfBounds) {
371             mOutOfBounds = outOfBounds;
372         }
373 
setWindowFocused(boolean windowFocused)374         public void setWindowFocused(boolean windowFocused) {
375             mWindowFocused = windowFocused;
376         }
377 
updateToolbarVisibility()378         public void updateToolbarVisibility() {
379             if (!mActive) {
380                 return;
381             }
382 
383             if (mHideRequested || mMoving || mOutOfBounds || !mWindowFocused) {
384                 mToolbar.hide();
385             } else {
386                 mToolbar.show();
387                 mLastShowTime = System.currentTimeMillis();
388             }
389         }
390     }
391 }
392