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