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