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.widget.PopupWindow; 33 34 import com.android.internal.R; 35 import com.android.internal.view.menu.MenuBuilder; 36 import com.android.internal.widget.floatingtoolbar.FloatingToolbar; 37 38 import java.util.Arrays; 39 import java.util.Objects; 40 41 public final class FloatingActionMode extends ActionMode { 42 43 private static final int MAX_HIDE_DURATION = 3000; 44 private static final int MOVING_HIDE_DELAY = 50; 45 46 @NonNull private final Context mContext; 47 @NonNull private final ActionMode.Callback2 mCallback; 48 @NonNull private final MenuBuilder mMenu; 49 @NonNull private final Rect mContentRect; 50 @NonNull private final Rect mContentRectOnScreen; 51 @NonNull private final Rect mPreviousContentRectOnScreen; 52 @NonNull private final int[] mViewPositionOnScreen; 53 @NonNull private final int[] mPreviousViewPositionOnScreen; 54 @NonNull private final int[] mRootViewPositionOnScreen; 55 @NonNull private final Rect mViewRectOnScreen; 56 @NonNull private final Rect mPreviousViewRectOnScreen; 57 @NonNull private final Rect mScreenRect; 58 @NonNull private final View mOriginatingView; 59 @NonNull private final Point mDisplaySize; 60 private final int mBottomAllowance; 61 62 private final Runnable mMovingOff = new Runnable() { 63 public void run() { 64 if (isViewStillActive()) { 65 mFloatingToolbarVisibilityHelper.setMoving(false); 66 mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); 67 } 68 } 69 }; 70 71 private final Runnable mHideOff = new Runnable() { 72 public void run() { 73 if (isViewStillActive()) { 74 mFloatingToolbarVisibilityHelper.setHideRequested(false); 75 mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); 76 } 77 } 78 }; 79 80 @NonNull private FloatingToolbar mFloatingToolbar; 81 @NonNull private FloatingToolbarVisibilityHelper mFloatingToolbarVisibilityHelper; 82 FloatingActionMode( Context context, ActionMode.Callback2 callback, View originatingView, FloatingToolbar floatingToolbar)83 public FloatingActionMode( 84 Context context, ActionMode.Callback2 callback, 85 View originatingView, FloatingToolbar floatingToolbar) { 86 mContext = Objects.requireNonNull(context); 87 mCallback = Objects.requireNonNull(callback); 88 mMenu = new MenuBuilder(context).setDefaultShowAsAction( 89 MenuItem.SHOW_AS_ACTION_IF_ROOM); 90 setType(ActionMode.TYPE_FLOATING); 91 mMenu.setCallback(new MenuBuilder.Callback() { 92 @Override 93 public void onMenuModeChange(MenuBuilder menu) {} 94 95 @Override 96 public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { 97 return mCallback.onActionItemClicked(FloatingActionMode.this, item); 98 } 99 }); 100 mContentRect = new Rect(); 101 mContentRectOnScreen = new Rect(); 102 mPreviousContentRectOnScreen = new Rect(); 103 mViewPositionOnScreen = new int[2]; 104 mPreviousViewPositionOnScreen = new int[2]; 105 mRootViewPositionOnScreen = new int[2]; 106 mViewRectOnScreen = new Rect(); 107 mPreviousViewRectOnScreen = new Rect(); 108 mScreenRect = new Rect(); 109 mOriginatingView = Objects.requireNonNull(originatingView); 110 mOriginatingView.getLocationOnScreen(mViewPositionOnScreen); 111 // Allow the content rect to overshoot a little bit beyond the 112 // bottom view bound if necessary. 113 mBottomAllowance = context.getResources() 114 .getDimensionPixelSize(R.dimen.content_rect_bottom_clip_allowance); 115 mDisplaySize = new Point(); 116 setFloatingToolbar(Objects.requireNonNull(floatingToolbar)); 117 } 118 setFloatingToolbar(FloatingToolbar floatingToolbar)119 private void setFloatingToolbar(FloatingToolbar floatingToolbar) { 120 mFloatingToolbar = floatingToolbar 121 .setMenu(mMenu) 122 .setOnMenuItemClickListener(item -> mMenu.performItemAction(item, 0)); 123 mFloatingToolbarVisibilityHelper = new FloatingToolbarVisibilityHelper(mFloatingToolbar); 124 mFloatingToolbarVisibilityHelper.activate(); 125 } 126 127 @Override setTitle(CharSequence title)128 public void setTitle(CharSequence title) {} 129 130 @Override setTitle(int resId)131 public void setTitle(int resId) {} 132 133 @Override setSubtitle(CharSequence subtitle)134 public void setSubtitle(CharSequence subtitle) {} 135 136 @Override setSubtitle(int resId)137 public void setSubtitle(int resId) {} 138 139 @Override setCustomView(View view)140 public void setCustomView(View view) {} 141 142 @Override invalidate()143 public void invalidate() { 144 mCallback.onPrepareActionMode(this, mMenu); 145 invalidateContentRect(); // Will re-layout and show the toolbar if necessary. 146 } 147 148 @Override invalidateContentRect()149 public void invalidateContentRect() { 150 mCallback.onGetContentRect(this, mOriginatingView, mContentRect); 151 updateViewLocationInWindow(/* forceRepositionToolbar= */ true); 152 } 153 updateViewLocationInWindow()154 public void updateViewLocationInWindow() { 155 updateViewLocationInWindow(/* forceRepositionToolbar= */ false); 156 } 157 updateViewLocationInWindow(boolean forceRepositionToolbar)158 private void updateViewLocationInWindow(boolean forceRepositionToolbar) { 159 mOriginatingView.getLocationOnScreen(mViewPositionOnScreen); 160 mOriginatingView.getRootView().getLocationOnScreen(mRootViewPositionOnScreen); 161 mOriginatingView.getGlobalVisibleRect(mViewRectOnScreen); 162 mViewRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]); 163 164 if (forceRepositionToolbar 165 || !Arrays.equals(mViewPositionOnScreen, mPreviousViewPositionOnScreen) 166 || !mViewRectOnScreen.equals(mPreviousViewRectOnScreen)) { 167 repositionToolbar(); 168 mPreviousViewPositionOnScreen[0] = mViewPositionOnScreen[0]; 169 mPreviousViewPositionOnScreen[1] = mViewPositionOnScreen[1]; 170 mPreviousViewRectOnScreen.set(mViewRectOnScreen); 171 } 172 } 173 repositionToolbar()174 private void repositionToolbar() { 175 mContentRectOnScreen.set(mContentRect); 176 177 // Offset the content rect into screen coordinates, taking into account any transformations 178 // that may be applied to the originating view or its ancestors. 179 final ViewParent parent = mOriginatingView.getParent(); 180 if (parent instanceof ViewGroup) { 181 ((ViewGroup) parent).getChildVisibleRect( 182 mOriginatingView, mContentRectOnScreen, 183 null /* offset */, true /* forceParentCheck */); 184 mContentRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]); 185 } else { 186 mContentRectOnScreen.offset(mViewPositionOnScreen[0], mViewPositionOnScreen[1]); 187 } 188 189 if (isContentRectWithinBounds()) { 190 mFloatingToolbarVisibilityHelper.setOutOfBounds(false); 191 // Make sure that content rect is not out of the view's visible bounds. 192 mContentRectOnScreen.set( 193 Math.max(mContentRectOnScreen.left, mViewRectOnScreen.left), 194 Math.max(mContentRectOnScreen.top, mViewRectOnScreen.top), 195 Math.min(mContentRectOnScreen.right, mViewRectOnScreen.right), 196 Math.min(mContentRectOnScreen.bottom, 197 mViewRectOnScreen.bottom + mBottomAllowance)); 198 199 if (!mContentRectOnScreen.equals(mPreviousContentRectOnScreen)) { 200 // Content rect is moving 201 if (!mPreviousContentRectOnScreen.isEmpty()) { 202 mOriginatingView.removeCallbacks(mMovingOff); 203 mFloatingToolbarVisibilityHelper.setMoving(true); 204 mOriginatingView.postDelayed(mMovingOff, MOVING_HIDE_DELAY); 205 } else { 206 // mPreviousContentRectOnScreen is empty. That means we are are showing the 207 // toolbar rather than moving it. And we should show it right away. 208 } 209 210 mFloatingToolbar.setContentRect(mContentRectOnScreen); 211 mFloatingToolbar.updateLayout(); 212 } 213 } else { 214 mFloatingToolbarVisibilityHelper.setOutOfBounds(true); 215 mContentRectOnScreen.setEmpty(); 216 } 217 mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); 218 219 mPreviousContentRectOnScreen.set(mContentRectOnScreen); 220 } 221 isContentRectWithinBounds()222 private boolean isContentRectWithinBounds() { 223 mContext.getDisplayNoVerify().getRealSize(mDisplaySize); 224 mScreenRect.set(0, 0, mDisplaySize.x, mDisplaySize.y); 225 226 return intersectsClosed(mContentRectOnScreen, mScreenRect) 227 && intersectsClosed(mContentRectOnScreen, mViewRectOnScreen); 228 } 229 230 /* 231 * Same as Rect.intersects, but includes cases where the rectangles touch. 232 */ intersectsClosed(Rect a, Rect b)233 private static boolean intersectsClosed(Rect a, Rect b) { 234 return a.left <= b.right && b.left <= a.right 235 && a.top <= b.bottom && b.top <= a.bottom; 236 } 237 238 @Override hide(long duration)239 public void hide(long duration) { 240 if (duration == ActionMode.DEFAULT_HIDE_DURATION) { 241 duration = ViewConfiguration.getDefaultActionModeHideDuration(); 242 } 243 duration = Math.min(MAX_HIDE_DURATION, duration); 244 mOriginatingView.removeCallbacks(mHideOff); 245 if (duration <= 0) { 246 mHideOff.run(); 247 } else { 248 mFloatingToolbarVisibilityHelper.setHideRequested(true); 249 mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); 250 mOriginatingView.postDelayed(mHideOff, duration); 251 } 252 } 253 254 /** 255 * If this is set to true, the action mode view will dismiss itself on touch events outside of 256 * its window. This only makes sense if the action mode view is a PopupWindow that is touchable 257 * but not focusable, which means touches outside of the window will be delivered to the window 258 * behind. The default is false. 259 * 260 * This is for internal use only and the approach to this may change. 261 * @hide 262 * 263 * @param outsideTouchable whether or not this action mode is "outside touchable" 264 * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself 265 */ setOutsideTouchable( boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss)266 public void setOutsideTouchable( 267 boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) { 268 mFloatingToolbar.setOutsideTouchable(outsideTouchable, onDismiss); 269 } 270 271 @Override onWindowFocusChanged(boolean hasWindowFocus)272 public void onWindowFocusChanged(boolean hasWindowFocus) { 273 mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus); 274 mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); 275 } 276 277 @Override finish()278 public void finish() { 279 reset(); 280 mCallback.onDestroyActionMode(this); 281 } 282 283 @Override getMenu()284 public Menu getMenu() { 285 return mMenu; 286 } 287 288 @Override getTitle()289 public CharSequence getTitle() { 290 return null; 291 } 292 293 @Override getSubtitle()294 public CharSequence getSubtitle() { 295 return null; 296 } 297 298 @Override getCustomView()299 public View getCustomView() { 300 return null; 301 } 302 303 @Override getMenuInflater()304 public MenuInflater getMenuInflater() { 305 return new MenuInflater(mContext); 306 } 307 reset()308 private void reset() { 309 mFloatingToolbar.dismiss(); 310 mFloatingToolbarVisibilityHelper.deactivate(); 311 mOriginatingView.removeCallbacks(mMovingOff); 312 mOriginatingView.removeCallbacks(mHideOff); 313 } 314 isViewStillActive()315 private boolean isViewStillActive() { 316 return mOriginatingView.getWindowVisibility() == View.VISIBLE 317 && mOriginatingView.isShown(); 318 } 319 320 /** 321 * A helper for showing/hiding the floating toolbar depending on certain states. 322 */ 323 private static final class FloatingToolbarVisibilityHelper { 324 325 private static final long MIN_SHOW_DURATION_FOR_MOVE_HIDE = 500; 326 327 private final FloatingToolbar mToolbar; 328 329 private boolean mHideRequested; 330 private boolean mMoving; 331 private boolean mOutOfBounds; 332 private boolean mWindowFocused = true; 333 334 private boolean mActive; 335 336 private long mLastShowTime; 337 FloatingToolbarVisibilityHelper(FloatingToolbar toolbar)338 public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) { 339 mToolbar = Objects.requireNonNull(toolbar); 340 } 341 activate()342 public void activate() { 343 mHideRequested = false; 344 mMoving = false; 345 mOutOfBounds = false; 346 mWindowFocused = true; 347 348 mActive = true; 349 } 350 deactivate()351 public void deactivate() { 352 mActive = false; 353 mToolbar.dismiss(); 354 } 355 setHideRequested(boolean hide)356 public void setHideRequested(boolean hide) { 357 mHideRequested = hide; 358 } 359 setMoving(boolean moving)360 public void setMoving(boolean moving) { 361 // Avoid unintended flickering by allowing the toolbar to show long enough before 362 // triggering the 'moving' flag - which signals a hide. 363 final boolean showingLongEnough = 364 System.currentTimeMillis() - mLastShowTime > MIN_SHOW_DURATION_FOR_MOVE_HIDE; 365 if (!moving || showingLongEnough) { 366 mMoving = moving; 367 } 368 } 369 setOutOfBounds(boolean outOfBounds)370 public void setOutOfBounds(boolean outOfBounds) { 371 mOutOfBounds = outOfBounds; 372 } 373 setWindowFocused(boolean windowFocused)374 public void setWindowFocused(boolean windowFocused) { 375 mWindowFocused = windowFocused; 376 } 377 updateToolbarVisibility()378 public void updateToolbarVisibility() { 379 if (!mActive) { 380 return; 381 } 382 383 if (mHideRequested || mMoving || mOutOfBounds || !mWindowFocused) { 384 mToolbar.hide(); 385 } else { 386 mToolbar.show(); 387 mLastShowTime = System.currentTimeMillis(); 388 } 389 } 390 } 391 } 392