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