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