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