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