/* * 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.annotation.NonNull; import android.content.Context; import android.graphics.Point; 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 android.view.WindowManager; 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 final class FloatingActionMode extends ActionMode { private static final int MAX_HIDE_DURATION = 3000; private static final int MOVING_HIDE_DELAY = 50; @NonNull private final Context mContext; @NonNull private final ActionMode.Callback2 mCallback; @NonNull private final MenuBuilder mMenu; @NonNull private final Rect mContentRect; @NonNull private final Rect mContentRectOnScreen; @NonNull private final Rect mPreviousContentRectOnScreen; @NonNull private final int[] mViewPositionOnScreen; @NonNull private final int[] mPreviousViewPositionOnScreen; @NonNull private final int[] mRootViewPositionOnScreen; @NonNull private final Rect mViewRectOnScreen; @NonNull private final Rect mPreviousViewRectOnScreen; @NonNull private final Rect mScreenRect; @NonNull private final View mOriginatingView; @NonNull private final Point mDisplaySize; private final int mBottomAllowance; private final Runnable mMovingOff = new Runnable() { public void run() { if (isViewStillActive()) { mFloatingToolbarVisibilityHelper.setMoving(false); mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); } } }; private final Runnable mHideOff = new Runnable() { public void run() { if (isViewStillActive()) { mFloatingToolbarVisibilityHelper.setHideRequested(false); mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); } } }; @NonNull private FloatingToolbar mFloatingToolbar; @NonNull private FloatingToolbarVisibilityHelper mFloatingToolbarVisibilityHelper; public FloatingActionMode( Context context, ActionMode.Callback2 callback, View originatingView, FloatingToolbar floatingToolbar) { 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); mDisplaySize = new Point(); setFloatingToolbar(Preconditions.checkNotNull(floatingToolbar)); } private void setFloatingToolbar(FloatingToolbar floatingToolbar) { mFloatingToolbar = floatingToolbar .setMenu(mMenu) .setOnMenuItemClickListener(item -> 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() { mCallback.onPrepareActionMode(this, mMenu); invalidateContentRect(); // Will re-layout and show the toolbar if necessary. } @Override public void invalidateContentRect() { mCallback.onGetContentRect(this, mOriginatingView, mContentRect); repositionToolbar(); } public void updateViewLocationInWindow() { 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() { 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() { mContext.getSystemService(WindowManager.class) .getDefaultDisplay().getRealSize(mDisplaySize); mScreenRect.set(0, 0, mDisplaySize.x, mDisplaySize.y); 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) { 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) { mFloatingToolbarVisibilityHelper.setWindowFocused(hasWindowFocus); mFloatingToolbarVisibilityHelper.updateToolbarVisibility(); } @Override public void finish() { 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); } private void reset() { mFloatingToolbar.dismiss(); mFloatingToolbarVisibilityHelper.deactivate(); mOriginatingView.removeCallbacks(mMovingOff); mOriginatingView.removeCallbacks(mHideOff); } private boolean isViewStillActive() { return mOriginatingView.getWindowVisibility() == View.VISIBLE && mOriginatingView.isShown(); } /** * A helper for showing/hiding the floating toolbar depending on certain states. */ private static final class FloatingToolbarVisibilityHelper { private static final long MIN_SHOW_DURATION_FOR_MOVE_HIDE = 500; private final FloatingToolbar mToolbar; private boolean mHideRequested; private boolean mMoving; private boolean mOutOfBounds; private boolean mWindowFocused = true; private boolean mActive; private long mLastShowTime; 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) { // Avoid unintended flickering by allowing the toolbar to show long enough before // triggering the 'moving' flag - which signals a hide. final boolean showingLongEnough = System.currentTimeMillis() - mLastShowTime > MIN_SHOW_DURATION_FOR_MOVE_HIDE; if (!moving || showingLongEnough) { 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(); mLastShowTime = System.currentTimeMillis(); } } } }