/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.view; import android.content.Context; import android.graphics.Rect; import android.view.ActionMode; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import com.android.internal.R; import com.android.internal.util.Preconditions; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.widget.FloatingToolbar; import java.util.Arrays; public class FloatingActionMode extends ActionMode { private static final int MAX_HIDE_DURATION = 3000; private static final int MOVING_HIDE_DELAY = 50; private final Context mContext; private final ActionMode.Callback2 mCallback; private final MenuBuilder mMenu; private final Rect mContentRect; private final Rect mContentRectOnScreen; private final Rect mPreviousContentRectOnScreen; private final int[] mViewPositionOnScreen; private final int[] mPreviousViewPositionOnScreen; private final int[] mRootViewPositionOnScreen; private final Rect mViewRectOnScreen; private final Rect mPreviousViewRectOnScreen; private final Rect mScreenRect; private final View mOriginatingView; private final int mBottomAllowance; private final Runnable mMovingOff = new Runnable() { public void run() { mFloatingToolbarVisibilityHelper.setMoving(false); mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); } }; private final Runnable mHideOff = new Runnable() { public void run() { mFloatingToolbarVisibilityHelper.setHideRequested(false); mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); } }; private FloatingToolbar mFloatingToolbar; private FloatingToolbarVisibilityHelper mFloatingToolbarVisibilityHelper; public FloatingActionMode( Context context, ActionMode.Callback2 callback, View originatingView) { mContext = Preconditions.checkNotNull(context); mCallback = Preconditions.checkNotNull(callback); mMenu = new MenuBuilder(context).setDefaultShowAsAction( MenuItem.SHOW_AS_ACTION_IF_ROOM); setType(ActionMode.TYPE_FLOATING); mMenu.setCallback(new MenuBuilder.Callback() { @Override public void onMenuModeChange(MenuBuilder menu) {} @Override public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { return mCallback.onActionItemClicked(FloatingActionMode.this, item); } }); mContentRect = new Rect(); mContentRectOnScreen = new Rect(); mPreviousContentRectOnScreen = new Rect(); mViewPositionOnScreen = new int[2]; mPreviousViewPositionOnScreen = new int[2]; mRootViewPositionOnScreen = new int[2]; mViewRectOnScreen = new Rect(); mPreviousViewRectOnScreen = new Rect(); mScreenRect = new Rect(); mOriginatingView = Preconditions.checkNotNull(originatingView); mOriginatingView.getLocationOnScreen(mViewPositionOnScreen); // Allow the content rect to overshoot a little bit beyond the // bottom view bound if necessary. mBottomAllowance = context.getResources() .getDimensionPixelSize(R.dimen.content_rect_bottom_clip_allowance); } public void setFloatingToolbar(FloatingToolbar floatingToolbar) { mFloatingToolbar = floatingToolbar .setMenu(mMenu) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { return mMenu.performItemAction(item, 0); } }); mFloatingToolbarVisibilityHelper = new FloatingToolbarVisibilityHelper(mFloatingToolbar); mFloatingToolbarVisibilityHelper.activate(); } @Override public void setTitle(CharSequence title) {} @Override public void setTitle(int resId) {} @Override public void setSubtitle(CharSequence subtitle) {} @Override public void setSubtitle(int resId) {} @Override public void setCustomView(View view) {} @Override public void invalidate() { checkToolbarInitialized(); mCallback.onPrepareActionMode(this, mMenu); invalidateContentRect(); // Will re-layout and show the toolbar if necessary. } @Override public void invalidateContentRect() { checkToolbarInitialized(); mCallback.onGetContentRect(this, mOriginatingView, mContentRect); repositionToolbar(); } public void updateViewLocationInWindow() { checkToolbarInitialized(); mOriginatingView.getLocationOnScreen(mViewPositionOnScreen); mOriginatingView.getRootView().getLocationOnScreen(mRootViewPositionOnScreen); mOriginatingView.getGlobalVisibleRect(mViewRectOnScreen); mViewRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]); if (!Arrays.equals(mViewPositionOnScreen, mPreviousViewPositionOnScreen) || !mViewRectOnScreen.equals(mPreviousViewRectOnScreen)) { repositionToolbar(); mPreviousViewPositionOnScreen[0] = mViewPositionOnScreen[0]; mPreviousViewPositionOnScreen[1] = mViewPositionOnScreen[1]; mPreviousViewRectOnScreen.set(mViewRectOnScreen); } } private void repositionToolbar() { checkToolbarInitialized(); mContentRectOnScreen.set(mContentRect); // Offset the content rect into screen coordinates, taking into account any transformations // that may be applied to the originating view or its ancestors. final ViewParent parent = mOriginatingView.getParent(); if (parent instanceof ViewGroup) { ((ViewGroup) parent).getChildVisibleRect( mOriginatingView, mContentRectOnScreen, null /* offset */, true /* forceParentCheck */); mContentRectOnScreen.offset(mRootViewPositionOnScreen[0], mRootViewPositionOnScreen[1]); } else { mContentRectOnScreen.offset(mViewPositionOnScreen[0], mViewPositionOnScreen[1]); } if (isContentRectWithinBounds()) { mFloatingToolbarVisibilityHelper.setOutOfBounds(false); // Make sure that content rect is not out of the view's visible bounds. mContentRectOnScreen.set( Math.max(mContentRectOnScreen.left, mViewRectOnScreen.left), Math.max(mContentRectOnScreen.top, mViewRectOnScreen.top), Math.min(mContentRectOnScreen.right, mViewRectOnScreen.right), Math.min(mContentRectOnScreen.bottom, mViewRectOnScreen.bottom + mBottomAllowance)); if (!mContentRectOnScreen.equals(mPreviousContentRectOnScreen)) { // Content rect is moving. mOriginatingView.removeCallbacks(mMovingOff); mFloatingToolbarVisibilityHelper.setMoving(true); mOriginatingView.postDelayed(mMovingOff, MOVING_HIDE_DELAY); mFloatingToolbar.setContentRect(mContentRectOnScreen); mFloatingToolbar.updateLayout(); } } else { mFloatingToolbarVisibilityHelper.setOutOfBounds(true); mContentRectOnScreen.setEmpty(); } mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); mPreviousContentRectOnScreen.set(mContentRectOnScreen); } private boolean isContentRectWithinBounds() { mScreenRect.set( 0, 0, mContext.getResources().getDisplayMetrics().widthPixels, mContext.getResources().getDisplayMetrics().heightPixels); return intersectsClosed(mContentRectOnScreen, mScreenRect) && intersectsClosed(mContentRectOnScreen, mViewRectOnScreen); } /* * Same as Rect.intersects, but includes cases where the rectangles touch. */ private static boolean intersectsClosed(Rect a, Rect b) { return a.left <= b.right && b.left <= a.right && a.top <= b.bottom && b.top <= a.bottom; } @Override public void hide(long duration) { checkToolbarInitialized(); if (duration == ActionMode.DEFAULT_HIDE_DURATION) { duration = ViewConfiguration.getDefaultActionModeHideDuration(); } duration = Math.min(MAX_HIDE_DURATION, duration); mOriginatingView.removeCallbacks(mHideOff); if (duration <= 0) { mHideOff.run(); } else { mFloatingToolbarVisibilityHelper.setHideRequested(true); mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); mOriginatingView.postDelayed(mHideOff, duration); } } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { checkToolbarInitialized(); mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus); mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); } @Override public void finish() { checkToolbarInitialized(); reset(); mCallback.onDestroyActionMode(this); } @Override public Menu getMenu() { return mMenu; } @Override public CharSequence getTitle() { return null; } @Override public CharSequence getSubtitle() { return null; } @Override public View getCustomView() { return null; } @Override public MenuInflater getMenuInflater() { return new MenuInflater(mContext); } /** * @throws IllegalStateException */ private void checkToolbarInitialized() { Preconditions.checkState(mFloatingToolbar != null); Preconditions.checkState(mFloatingToolbarVisibilityHelper != null); } private void reset() { mFloatingToolbar.dismiss(); mFloatingToolbarVisibilityHelper.deactivate(); mOriginatingView.removeCallbacks(mMovingOff); mOriginatingView.removeCallbacks(mHideOff); } /** * A helper for showing/hiding the floating toolbar depending on certain states. */ private static final class FloatingToolbarVisibilityHelper { private final FloatingToolbar mToolbar; private boolean mHideRequested; private boolean mMoving; private boolean mOutOfBounds; private boolean mWindowFocused = true; private boolean mActive; public FloatingToolbarVisibilityHelper(FloatingToolbar toolbar) { mToolbar = Preconditions.checkNotNull(toolbar); } public void activate() { mHideRequested = false; mMoving = false; mOutOfBounds = false; mWindowFocused = true; mActive = true; } public void deactivate() { mActive = false; mToolbar.dismiss(); } public void setHideRequested(boolean hide) { mHideRequested = hide; } public void setMoving(boolean moving) { mMoving = moving; } public void setOutOfBounds(boolean outOfBounds) { mOutOfBounds = outOfBounds; } public void setWindowFocused(boolean windowFocused) { mWindowFocused = windowFocused; } public void updateToolbarVisibility() { if (!mActive) { return; } if (mHideRequested || mMoving || mOutOfBounds || !mWindowFocused) { mToolbar.hide(); } else { mToolbar.show(); } } } }