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 FloatingActionMode( Context context, ActionMode.Callback2 callback, View originatingView, FloatingToolbar floatingToolbar)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 setFloatingToolbar(FloatingToolbar floatingToolbar)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 setTitle(CharSequence title)129 public void setTitle(CharSequence title) {} 130 131 @Override setTitle(int resId)132 public void setTitle(int resId) {} 133 134 @Override setSubtitle(CharSequence subtitle)135 public void setSubtitle(CharSequence subtitle) {} 136 137 @Override setSubtitle(int resId)138 public void setSubtitle(int resId) {} 139 140 @Override setCustomView(View view)141 public void setCustomView(View view) {} 142 143 @Override invalidate()144 public void invalidate() { 145 mCallback.onPrepareActionMode(this, mMenu); 146 invalidateContentRect(); // Will re-layout and show the toolbar if necessary. 147 } 148 149 @Override invalidateContentRect()150 public void invalidateContentRect() { 151 mCallback.onGetContentRect(this, mOriginatingView, mContentRect); 152 repositionToolbar(); 153 } 154 updateViewLocationInWindow()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 repositionToolbar()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 isContentRectWithinBounds()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 */ 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 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 */ setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss)258 public void setOutsideTouchable( 259 boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) { 260 mFloatingToolbar.setOutsideTouchable(outsideTouchable, onDismiss); 261 } 262 263 @Override onWindowFocusChanged(boolean hasWindowFocus)264 public void onWindowFocusChanged(boolean hasWindowFocus) { 265 mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus); 266 mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); 267 } 268 269 @Override finish()270 public void finish() { 271 reset(); 272 mCallback.onDestroyActionMode(this); 273 } 274 275 @Override getMenu()276 public Menu getMenu() { 277 return mMenu; 278 } 279 280 @Override getTitle()281 public CharSequence getTitle() { 282 return null; 283 } 284 285 @Override getSubtitle()286 public CharSequence getSubtitle() { 287 return null; 288 } 289 290 @Override getCustomView()291 public View getCustomView() { 292 return null; 293 } 294 295 @Override getMenuInflater()296 public MenuInflater getMenuInflater() { 297 return new MenuInflater(mContext); 298 } 299 reset()300 private void reset() { 301 mFloatingToolbar.dismiss(); 302 mFloatingToolbarVisibilityHelper.deactivate(); 303 mOriginatingView.removeCallbacks(mMovingOff); 304 mOriginatingView.removeCallbacks(mHideOff); 305 } 306 isViewStillActive()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 FloatingToolbarVisibilityHelper(FloatingToolbar toolbar)330 public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) { 331 mToolbar = Preconditions.checkNotNull(toolbar); 332 } 333 activate()334 public void activate() { 335 mHideRequested = false; 336 mMoving = false; 337 mOutOfBounds = false; 338 mWindowFocused = true; 339 340 mActive = true; 341 } 342 deactivate()343 public void deactivate() { 344 mActive = false; 345 mToolbar.dismiss(); 346 } 347 setHideRequested(boolean hide)348 public void setHideRequested(boolean hide) { 349 mHideRequested = hide; 350 } 351 setMoving(boolean moving)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 setOutOfBounds(boolean outOfBounds)362 public void setOutOfBounds(boolean outOfBounds) { 363 mOutOfBounds = outOfBounds; 364 } 365 setWindowFocused(boolean windowFocused)366 public void setWindowFocused(boolean windowFocused) { 367 mWindowFocused = windowFocused; 368 } 369 updateToolbarVisibility()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