DividerView.java revision 81fe2d1f0adc9e752d7f1a410d66af6a326fd6e2
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.systemui.stackdivider;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.animation.ValueAnimator.AnimatorUpdateListener;
23import android.annotation.Nullable;
24import android.content.Context;
25import android.content.res.Configuration;
26import android.graphics.Rect;
27import android.graphics.Region.Op;
28import android.hardware.display.DisplayManager;
29import android.util.AttributeSet;
30import android.view.Display;
31import android.view.DisplayInfo;
32import android.view.MotionEvent;
33import android.view.PointerIcon;
34import android.view.VelocityTracker;
35import android.view.View;
36import android.view.View.OnTouchListener;
37import android.view.ViewTreeObserver.InternalInsetsInfo;
38import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
39import android.view.WindowInsets;
40import android.view.WindowManager;
41import android.view.animation.AnimationUtils;
42import android.view.animation.Interpolator;
43import android.view.animation.PathInterpolator;
44import android.widget.FrameLayout;
45import android.widget.ImageButton;
46
47import com.android.systemui.R;
48import com.android.systemui.stackdivider.DividerSnapAlgorithm.SnapTarget;
49import com.android.systemui.statusbar.FlingAnimationUtils;
50
51import static android.view.PointerIcon.STYLE_HORIZONTAL_DOUBLE_ARROW;
52import static android.view.PointerIcon.STYLE_VERTICAL_DOUBLE_ARROW;
53
54/**
55 * Docked stack divider.
56 */
57public class DividerView extends FrameLayout implements OnTouchListener,
58        OnComputeInternalInsetsListener {
59
60    private static final String TAG = "DividerView";
61
62    private static final int TASK_POSITION_SAME = Integer.MAX_VALUE;
63
64    private ImageButton mHandle;
65    private View mBackground;
66    private int mStartX;
67    private int mStartY;
68    private int mStartPosition;
69    private int mDockSide;
70    private final int[] mTempInt2 = new int[2];
71
72    private int mDividerInsets;
73    private int mDisplayWidth;
74    private int mDisplayHeight;
75    private int mDividerWindowWidth;
76    private int mDividerSize;
77    private int mTouchElevation;
78
79    private final Rect mDockedRect = new Rect();
80    private final Rect mDockedTaskRect = new Rect();
81    private final Rect mOtherTaskRect = new Rect();
82    private final Rect mOtherRect = new Rect();
83    private final Rect mDockedInsetRect = new Rect();
84    private final Rect mOtherInsetRect = new Rect();
85    private final Rect mLastResizeRect = new Rect();
86    private final WindowManagerProxy mWindowManagerProxy = WindowManagerProxy.getInstance();
87    private Interpolator mFastOutSlowInInterpolator;
88    private final Interpolator mTouchResponseInterpolator =
89            new PathInterpolator(0.3f, 0f, 0.1f, 1f);
90    private DividerWindowManager mWindowManager;
91    private VelocityTracker mVelocityTracker;
92    private FlingAnimationUtils mFlingAnimationUtils;
93    private DividerSnapAlgorithm mSnapAlgorithm;
94    private final Rect mStableInsets = new Rect();
95
96    public DividerView(Context context) {
97        super(context);
98    }
99
100    public DividerView(Context context, @Nullable AttributeSet attrs) {
101        super(context, attrs);
102    }
103
104    public DividerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
105        super(context, attrs, defStyleAttr);
106    }
107
108    public DividerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
109            int defStyleRes) {
110        super(context, attrs, defStyleAttr, defStyleRes);
111    }
112
113    @Override
114    protected void onFinishInflate() {
115        super.onFinishInflate();
116        mHandle = (ImageButton) findViewById(R.id.docked_divider_handle);
117        mBackground = findViewById(R.id.docked_divider_background);
118        mHandle.setOnTouchListener(this);
119        mDividerWindowWidth = getResources().getDimensionPixelSize(
120                com.android.internal.R.dimen.docked_stack_divider_thickness);
121        mDividerInsets = getResources().getDimensionPixelSize(
122                com.android.internal.R.dimen.docked_stack_divider_insets);
123        mDividerSize = mDividerWindowWidth - 2 * mDividerInsets;
124        mTouchElevation = getResources().getDimensionPixelSize(
125                R.dimen.docked_stack_divider_lift_elevation);
126        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
127                android.R.interpolator.fast_out_slow_in);
128        mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.3f);
129        updateDisplayInfo();
130        boolean landscape = getResources().getConfiguration().orientation
131                == Configuration.ORIENTATION_LANDSCAPE;
132        mHandle.setPointerIcon(PointerIcon.getSystemIcon(getContext(),
133                landscape ? STYLE_HORIZONTAL_DOUBLE_ARROW : STYLE_VERTICAL_DOUBLE_ARROW));
134        getViewTreeObserver().addOnComputeInternalInsetsListener(this);
135    }
136
137    @Override
138    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
139        mStableInsets.set(insets.getStableInsetLeft(), insets.getStableInsetTop(),
140                insets.getStableInsetRight(), insets.getStableInsetBottom());
141        return super.onApplyWindowInsets(insets);
142    }
143
144    public void setWindowManager(DividerWindowManager windowManager) {
145        mWindowManager = windowManager;
146    }
147
148    public WindowManagerProxy getWindowManagerProxy() {
149        return mWindowManagerProxy;
150    }
151
152    public boolean startDragging() {
153        mDockSide = mWindowManagerProxy.getDockSide();
154        mSnapAlgorithm = new DividerSnapAlgorithm(getContext(), mFlingAnimationUtils, mDisplayWidth,
155                mDisplayHeight, mDividerSize, isHorizontalDivision(), mStableInsets);
156        if (mDockSide != WindowManager.DOCKED_INVALID) {
157            mWindowManagerProxy.setResizing(true);
158            mWindowManager.setSlippery(false);
159            liftBackground();
160            return true;
161        } else {
162            return false;
163        }
164    }
165
166    public void stopDragging(int position, float velocity) {
167        fling(position, velocity);
168        mWindowManager.setSlippery(true);
169        releaseBackground();
170    }
171
172    public DividerSnapAlgorithm getSnapAlgorithm() {
173        return mSnapAlgorithm;
174    }
175
176    @Override
177    public boolean onTouch(View v, MotionEvent event) {
178        convertToScreenCoordinates(event);
179        final int action = event.getAction() & MotionEvent.ACTION_MASK;
180        switch (action) {
181            case MotionEvent.ACTION_DOWN:
182                mVelocityTracker = VelocityTracker.obtain();
183                mVelocityTracker.addMovement(event);
184                mStartX = (int) event.getX();
185                mStartY = (int) event.getY();
186                getLocationOnScreen(mTempInt2);
187                boolean result = startDragging();
188                if (isHorizontalDivision()) {
189                    mStartPosition = mTempInt2[1] + mDividerInsets;
190                } else {
191                    mStartPosition = mTempInt2[0] + mDividerInsets;
192                }
193                return result;
194            case MotionEvent.ACTION_MOVE:
195                mVelocityTracker.addMovement(event);
196                int x = (int) event.getX();
197                int y = (int) event.getY();
198                if (mDockSide != WindowManager.DOCKED_INVALID) {
199                    int position = calculatePosition(x, y);
200                    SnapTarget snapTarget = mSnapAlgorithm.calculateSnapTarget(position,
201                            0 /* velocity */);
202                    resizeStack(calculatePosition(x, y), snapTarget.position);
203                }
204                break;
205            case MotionEvent.ACTION_UP:
206            case MotionEvent.ACTION_CANCEL:
207                mVelocityTracker.addMovement(event);
208
209                x = (int) event.getRawX();
210                y = (int) event.getRawY();
211
212                mVelocityTracker.computeCurrentVelocity(1000);
213                int position = calculatePosition(x, y);
214                stopDragging(position, isHorizontalDivision() ? mVelocityTracker.getYVelocity()
215                        : mVelocityTracker.getXVelocity());
216                break;
217        }
218        return true;
219    }
220
221    private void convertToScreenCoordinates(MotionEvent event) {
222        event.setLocation(event.getRawX(), event.getRawY());
223    }
224
225    private void fling(int position, float velocity) {
226        final SnapTarget snapTarget = mSnapAlgorithm.calculateSnapTarget(position, velocity);
227
228        ValueAnimator anim = ValueAnimator.ofInt(position, snapTarget.position);
229        anim.addUpdateListener(new AnimatorUpdateListener() {
230            @Override
231            public void onAnimationUpdate(ValueAnimator animation) {
232                resizeStack((Integer) animation.getAnimatedValue(),
233                        animation.getAnimatedFraction() == 1f
234                                ? TASK_POSITION_SAME
235                                : snapTarget.position);
236            }
237        });
238        anim.addListener(new AnimatorListenerAdapter() {
239            @Override
240            public void onAnimationEnd(Animator animation) {
241                commitSnapFlags(snapTarget);
242                mWindowManagerProxy.setResizing(false);
243                mDockSide = WindowManager.DOCKED_INVALID;
244            }
245        });
246        mFlingAnimationUtils.apply(anim, position, snapTarget.position, velocity);
247        anim.start();
248    }
249
250    private void commitSnapFlags(SnapTarget target) {
251        if (target.flag == SnapTarget.FLAG_NONE) {
252            return;
253        }
254        boolean dismissOrMaximize;
255        if (target.flag == SnapTarget.FLAG_DISMISS_START) {
256            dismissOrMaximize = mDockSide == WindowManager.DOCKED_LEFT
257                    || mDockSide == WindowManager.DOCKED_TOP;
258        } else {
259            dismissOrMaximize = mDockSide == WindowManager.DOCKED_RIGHT
260                    || mDockSide == WindowManager.DOCKED_BOTTOM;
261        }
262        if (dismissOrMaximize) {
263            mWindowManagerProxy.dismissDockedStack();
264        } else {
265            mWindowManagerProxy.maximizeDockedStack();
266        }
267    }
268
269    private void liftBackground() {
270        if (isHorizontalDivision()) {
271            mBackground.animate().scaleY(1.4f);
272        } else {
273            mBackground.animate().scaleX(1.4f);
274        }
275        mBackground.animate()
276                .setInterpolator(mTouchResponseInterpolator)
277                .setDuration(150)
278                .translationZ(mTouchElevation);
279
280        // Lift handle as well so it doesn't get behind the background, even though it doesn't
281        // cast shadow.
282        mHandle.animate()
283                .setInterpolator(mTouchResponseInterpolator)
284                .setDuration(150)
285                .translationZ(mTouchElevation);
286    }
287
288    private void releaseBackground() {
289        mBackground.animate()
290                .setInterpolator(mFastOutSlowInInterpolator)
291                .setDuration(200)
292                .translationZ(0)
293                .scaleX(1f)
294                .scaleY(1f);
295        mHandle.animate()
296                .setInterpolator(mFastOutSlowInInterpolator)
297                .setDuration(200)
298                .translationZ(0);
299    }
300
301    @Override
302    protected void onConfigurationChanged(Configuration newConfig) {
303        super.onConfigurationChanged(newConfig);
304        updateDisplayInfo();
305    }
306
307    private void updateDisplayInfo() {
308        final DisplayManager displayManager =
309                (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
310        Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
311        final DisplayInfo info = new DisplayInfo();
312        display.getDisplayInfo(info);
313        mDisplayWidth = info.logicalWidth;
314        mDisplayHeight = info.logicalHeight;
315    }
316
317    private int calculatePosition(int touchX, int touchY) {
318        return isHorizontalDivision() ? calculateYPosition(touchY) : calculateXPosition(touchX);
319    }
320
321    public boolean isHorizontalDivision() {
322        return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
323    }
324
325    private int calculateXPosition(int touchX) {
326        return mStartPosition + touchX - mStartX;
327    }
328
329    private int calculateYPosition(int touchY) {
330        return mStartPosition + touchY - mStartY;
331    }
332
333    public void calculateBoundsForPosition(int position, int dockSide, Rect outRect) {
334        outRect.set(0, 0, mDisplayWidth, mDisplayHeight);
335        switch (dockSide) {
336            case WindowManager.DOCKED_LEFT:
337                outRect.right = position;
338                break;
339            case WindowManager.DOCKED_TOP:
340                outRect.bottom = position;
341                break;
342            case WindowManager.DOCKED_RIGHT:
343                outRect.left = position + mDividerWindowWidth - 2 * mDividerInsets;
344                break;
345            case WindowManager.DOCKED_BOTTOM:
346                outRect.top = position + mDividerWindowWidth - 2 * mDividerInsets;
347                break;
348        }
349        if (outRect.left > outRect.right) {
350            outRect.left = outRect.right;
351        }
352        if (outRect.top > outRect.bottom) {
353            outRect.top = outRect.bottom;
354        }
355        if (outRect.right < outRect.left) {
356            outRect.right = outRect.left;
357        }
358        if (outRect.bottom < outRect.top) {
359            outRect.bottom = outRect.top;
360        }
361    }
362
363    private int invertDockSide(int dockSide) {
364        switch (dockSide) {
365            case WindowManager.DOCKED_LEFT:
366                return WindowManager.DOCKED_RIGHT;
367            case WindowManager.DOCKED_TOP:
368                return WindowManager.DOCKED_BOTTOM;
369            case WindowManager.DOCKED_RIGHT:
370                return WindowManager.DOCKED_LEFT;
371            case WindowManager.DOCKED_BOTTOM:
372                return WindowManager.DOCKED_TOP;
373            default:
374                return WindowManager.DOCKED_INVALID;
375        }
376    }
377
378    private void alignTopLeft(Rect containingRect, Rect rect) {
379        int width = rect.width();
380        int height = rect.height();
381        rect.set(containingRect.left, containingRect.top,
382                containingRect.left + width, containingRect.top + height);
383    }
384
385    private void alignBottomRight(Rect containingRect, Rect rect) {
386        int width = rect.width();
387        int height = rect.height();
388        rect.set(containingRect.right - width, containingRect.bottom - height,
389                containingRect.right, containingRect.bottom);
390    }
391
392    public void resizeStack(int position, int taskPosition) {
393        calculateBoundsForPosition(position, mDockSide, mDockedRect);
394
395        if (mDockedRect.equals(mLastResizeRect)) {
396            return;
397        }
398
399        // Make sure shadows are updated
400        mBackground.invalidate();
401
402        mLastResizeRect.set(mDockedRect);
403        if (taskPosition != TASK_POSITION_SAME) {
404            calculateBoundsForPosition(position, invertDockSide(mDockSide), mOtherRect);
405            calculateBoundsForPosition(taskPosition, mDockSide, mDockedTaskRect);
406            calculateBoundsForPosition(taskPosition, invertDockSide(mDockSide), mOtherTaskRect);
407            alignTopLeft(mDockedRect, mDockedTaskRect);
408            alignTopLeft(mOtherRect, mOtherTaskRect);
409            mDockedInsetRect.set(mDockedTaskRect);
410            mOtherInsetRect.set(mOtherTaskRect);
411            if (mDockSide == WindowManager.DOCKED_LEFT || mDockSide == WindowManager.DOCKED_TOP) {
412                alignTopLeft(mDockedRect, mDockedInsetRect);
413                alignBottomRight(mOtherRect, mOtherInsetRect);
414            } else {
415                alignBottomRight(mDockedRect, mDockedInsetRect);
416                alignTopLeft(mOtherRect, mOtherInsetRect);
417            }
418            mWindowManagerProxy.resizeDockedStack(mDockedRect, mDockedTaskRect, mDockedInsetRect,
419                    mOtherTaskRect, mOtherInsetRect);
420        } else {
421            mWindowManagerProxy.resizeDockedStack(mDockedRect, null, null, null, null);
422        }
423    }
424
425    @Override
426    public void onComputeInternalInsets(InternalInsetsInfo inoutInfo) {
427        inoutInfo.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
428        inoutInfo.touchableRegion.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(),
429                mHandle.getBottom());
430        inoutInfo.touchableRegion.op(mBackground.getLeft(), mBackground.getTop(),
431                mBackground.getRight(), mBackground.getBottom(), Op.UNION);
432    }
433}
434