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