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.content.Context;
     20 import android.graphics.Rect;
     21 import android.view.ActionMode;
     22 import android.view.Menu;
     23 import android.view.MenuInflater;
     24 import android.view.MenuItem;
     25 import android.view.View;
     26 import android.view.ViewConfiguration;
     27 import android.view.ViewGroup;
     28 import android.view.ViewParent;
     29 import android.util.DisplayMetrics;
     30 
     31 import com.android.internal.R;
     32 import com.android.internal.util.Preconditions;
     33 import com.android.internal.view.menu.MenuBuilder;
     34 import com.android.internal.widget.FloatingToolbar;
     35 
     36 import java.util.Arrays;
     37 
     38 public class FloatingActionMode extends ActionMode {
     39 
     40     private static final int MAX_HIDE_DURATION = 3000;
     41     private static final int MOVING_HIDE_DELAY = 50;
     42 
     43     private final Context mContext;
     44     private final ActionMode.Callback2 mCallback;
     45     private final MenuBuilder mMenu;
     46     private final Rect mContentRect;
     47     private final Rect mContentRectOnScreen;
     48     private final Rect mPreviousContentRectOnScreen;
     49     private final int[] mViewPositionOnScreen;
     50     private final int[] mPreviousViewPositionOnScreen;
     51     private final int[] mRootViewPositionOnScreen;
     52     private final Rect mViewRectOnScreen;
     53     private final Rect mPreviousViewRectOnScreen;
     54     private final Rect mScreenRect;
     55     private final View mOriginatingView;
     56     private final int mBottomAllowance;
     57 
     58     private final Runnable mMovingOff = new Runnable() {
     59         public void run() {
     60             mFloatingToolbarVisibilityHelper.setMoving(false);
     61             mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
     62         }
     63     };
     64 
     65     private final Runnable mHideOff = new Runnable() {
     66         public void run() {
     67             mFloatingToolbarVisibilityHelper.setHideRequested(false);
     68             mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
     69         }
     70     };
     71 
     72     private FloatingToolbar mFloatingToolbar;
     73     private FloatingToolbarVisibilityHelper mFloatingToolbarVisibilityHelper;
     74 
     75     public FloatingActionMode(
     76             Context context, ActionMode.Callback2 callback, View originatingView) {
     77         mContext = Preconditions.checkNotNull(context);
     78         mCallback = Preconditions.checkNotNull(callback);
     79         mMenu = new MenuBuilder(context).setDefaultShowAsAction(
     80                 MenuItem.SHOW_AS_ACTION_IF_ROOM);
     81         setType(ActionMode.TYPE_FLOATING);
     82         mMenu.setCallback(new MenuBuilder.Callback() {
     83             @Override
     84             public void onMenuModeChange(MenuBuilder menu) {}
     85 
     86             @Override
     87             public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
     88                 return mCallback.onActionItemClicked(FloatingActionMode.this, item);
     89             }
     90         });
     91         mContentRect = new Rect();
     92         mContentRectOnScreen = new Rect();
     93         mPreviousContentRectOnScreen = new Rect();
     94         mViewPositionOnScreen = new int[2];
     95         mPreviousViewPositionOnScreen = new int[2];
     96         mRootViewPositionOnScreen = new int[2];
     97         mViewRectOnScreen = new Rect();
     98         mPreviousViewRectOnScreen = new Rect();
     99         mScreenRect = new Rect();
    100         mOriginatingView = Preconditions.checkNotNull(originatingView);
    101         mOriginatingView.getLocationOnScreen(mViewPositionOnScreen);
    102         // Allow the content rect to overshoot a little bit beyond the
    103         // bottom view bound if necessary.
    104         mBottomAllowance = context.getResources()
    105                 .getDimensionPixelSize(R.dimen.content_rect_bottom_clip_allowance);
    106     }
    107 
    108     public void setFloatingToolbar(FloatingToolbar floatingToolbar) {
    109         mFloatingToolbar = floatingToolbar
    110                 .setMenu(mMenu)
    111                 .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
    112                         @Override
    113                     public boolean onMenuItemClick(MenuItem item) {
    114                         return mMenu.performItemAction(item, 0);
    115                     }
    116                 });
    117         mFloatingToolbarVisibilityHelper = new FloatingToolbarVisibilityHelper(mFloatingToolbar);
    118         mFloatingToolbarVisibilityHelper.activate();
    119     }
    120 
    121     @Override
    122     public void setTitle(CharSequence title) {}
    123 
    124     @Override
    125     public void setTitle(int resId) {}
    126 
    127     @Override
    128     public void setSubtitle(CharSequence subtitle) {}
    129 
    130     @Override
    131     public void setSubtitle(int resId) {}
    132 
    133     @Override
    134     public void setCustomView(View view) {}
    135 
    136     @Override
    137     public void invalidate() {
    138         checkToolbarInitialized();
    139         mCallback.onPrepareActionMode(this, mMenu);
    140         invalidateContentRect();  // Will re-layout and show the toolbar if necessary.
    141     }
    142 
    143     @Override
    144     public void invalidateContentRect() {
    145         checkToolbarInitialized();
    146         mCallback.onGetContentRect(this, mOriginatingView, mContentRect);
    147         repositionToolbar();
    148     }
    149 
    150     public void updateViewLocationInWindow() {
    151         checkToolbarInitialized();
    152 
    153         mOriginatingView.getLocationOnScreen(mViewPositionOnScreen);
    154         mOriginatingView.getRootView().getLocationOnScreen(mRootViewPositionOnScreen);
    155         mOriginatingView.getGlobalVisibleRect(mViewRectOnScreen);
    156         mViewRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]);
    157 
    158         if (!Arrays.equals(mViewPositionOnScreen, mPreviousViewPositionOnScreen)
    159                 || !mViewRectOnScreen.equals(mPreviousViewRectOnScreen)) {
    160             repositionToolbar();
    161             mPreviousViewPositionOnScreen[0] = mViewPositionOnScreen[0];
    162             mPreviousViewPositionOnScreen[1] = mViewPositionOnScreen[1];
    163             mPreviousViewRectOnScreen.set(mViewRectOnScreen);
    164         }
    165     }
    166 
    167     private void repositionToolbar() {
    168         checkToolbarInitialized();
    169 
    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 
    212     private boolean isContentRectWithinBounds() {
    213         DisplayMetrics metrics = mContext.getApplicationContext()
    214                 .getResources().getDisplayMetrics();
    215         mScreenRect.set(0, 0, metrics.widthPixels, metrics.heightPixels);
    216 
    217         return intersectsClosed(mContentRectOnScreen, mScreenRect)
    218             && intersectsClosed(mContentRectOnScreen, mViewRectOnScreen);
    219     }
    220 
    221     /*
    222      * Same as Rect.intersects, but includes cases where the rectangles touch.
    223     */
    224     private static boolean intersectsClosed(Rect a, Rect b) {
    225          return a.left <= b.right && b.left <= a.right
    226                  && a.top <= b.bottom && b.top <= a.bottom;
    227     }
    228 
    229     @Override
    230     public void hide(long duration) {
    231         checkToolbarInitialized();
    232 
    233         if (duration == ActionMode.DEFAULT_HIDE_DURATION) {
    234             duration = ViewConfiguration.getDefaultActionModeHideDuration();
    235         }
    236         duration = Math.min(MAX_HIDE_DURATION, duration);
    237         mOriginatingView.removeCallbacks(mHideOff);
    238         if (duration <= 0) {
    239             mHideOff.run();
    240         } else {
    241             mFloatingToolbarVisibilityHelper.setHideRequested(true);
    242             mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
    243             mOriginatingView.postDelayed(mHideOff, duration);
    244         }
    245     }
    246 
    247     @Override
    248     public void onWindowFocusChanged(boolean hasWindowFocus) {
    249         checkToolbarInitialized();
    250         mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus);
    251         mFloatingToolbarVisibilityHelper.updateToolbarVisibility();
    252     }
    253 
    254     @Override
    255     public void finish() {
    256         checkToolbarInitialized();
    257         reset();
    258         mCallback.onDestroyActionMode(this);
    259     }
    260 
    261     @Override
    262     public Menu getMenu() {
    263         return mMenu;
    264     }
    265 
    266     @Override
    267     public CharSequence getTitle() {
    268         return null;
    269     }
    270 
    271     @Override
    272     public CharSequence getSubtitle() {
    273         return null;
    274     }
    275 
    276     @Override
    277     public View getCustomView() {
    278         return null;
    279     }
    280 
    281     @Override
    282     public MenuInflater getMenuInflater() {
    283         return new MenuInflater(mContext);
    284     }
    285 
    286     /**
    287      * @throws IllegalStateException
    288      */
    289     private void checkToolbarInitialized() {
    290         Preconditions.checkState(mFloatingToolbar != null);
    291         Preconditions.checkState(mFloatingToolbarVisibilityHelper != null);
    292     }
    293 
    294     private void reset() {
    295         mFloatingToolbar.dismiss();
    296         mFloatingToolbarVisibilityHelper.deactivate();
    297         mOriginatingView.removeCallbacks(mMovingOff);
    298         mOriginatingView.removeCallbacks(mHideOff);
    299     }
    300 
    301     /**
    302      * A helper for showing/hiding the floating toolbar depending on certain states.
    303      */
    304     private static final class FloatingToolbarVisibilityHelper {
    305 
    306         private final FloatingToolbar mToolbar;
    307 
    308         private boolean mHideRequested;
    309         private boolean mMoving;
    310         private boolean mOutOfBounds;
    311         private boolean mWindowFocused = true;
    312 
    313         private boolean mActive;
    314 
    315         public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) {
    316             mToolbar = Preconditions.checkNotNull(toolbar);
    317         }
    318 
    319         public void activate() {
    320             mHideRequested = false;
    321             mMoving = false;
    322             mOutOfBounds = false;
    323             mWindowFocused = true;
    324 
    325             mActive = true;
    326         }
    327 
    328         public void deactivate() {
    329             mActive = false;
    330             mToolbar.dismiss();
    331         }
    332 
    333         public void setHideRequested(boolean hide) {
    334             mHideRequested = hide;
    335         }
    336 
    337         public void setMoving(boolean moving) {
    338             mMoving = moving;
    339         }
    340 
    341         public void setOutOfBounds(boolean outOfBounds) {
    342             mOutOfBounds = outOfBounds;
    343         }
    344 
    345         public void setWindowFocused(boolean windowFocused) {
    346             mWindowFocused = windowFocused;
    347         }
    348 
    349         public void updateToolbarVisibility() {
    350             if (!mActive) {
    351                 return;
    352             }
    353 
    354             if (mHideRequested || mMoving || mOutOfBounds || !mWindowFocused) {
    355                 mToolbar.hide();
    356             } else {
    357                 mToolbar.show();
    358             }
    359         }
    360     }
    361 }
    362