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