1 /*
2  * Copyright (C) 2020 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 
17 package com.android.wm.shell.common.split;
18 
19 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
20 import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.ObjectAnimator;
25 import android.content.Context;
26 import android.graphics.Rect;
27 import android.util.AttributeSet;
28 import android.util.Property;
29 import android.view.GestureDetector;
30 import android.view.InsetsController;
31 import android.view.InsetsSource;
32 import android.view.InsetsState;
33 import android.view.MotionEvent;
34 import android.view.SurfaceControlViewHost;
35 import android.view.VelocityTracker;
36 import android.view.View;
37 import android.view.ViewConfiguration;
38 import android.view.ViewGroup;
39 import android.view.WindowManager;
40 import android.widget.FrameLayout;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 
45 import com.android.internal.policy.DividerSnapAlgorithm;
46 import com.android.wm.shell.R;
47 import com.android.wm.shell.animation.Interpolators;
48 
49 /**
50  * Divider for multi window splits.
51  */
52 public class DividerView extends FrameLayout implements View.OnTouchListener {
53     public static final long TOUCH_ANIMATION_DURATION = 150;
54     public static final long TOUCH_RELEASE_ANIMATION_DURATION = 200;
55 
56     /** The task bar expanded height. Used to determine whether to insets divider bounds or not. */
57     private float mExpandedTaskBarHeight;
58 
59     private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
60 
61     private SplitLayout mSplitLayout;
62     private SplitWindowManager mSplitWindowManager;
63     private SurfaceControlViewHost mViewHost;
64     private DividerHandleView mHandle;
65     private View mBackground;
66     private int mTouchElevation;
67 
68     private VelocityTracker mVelocityTracker;
69     private boolean mMoving;
70     private int mStartPos;
71     private GestureDetector mDoubleTapDetector;
72     private boolean mInteractive;
73     private boolean mSetTouchRegion = true;
74 
75     /**
76      * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with
77      * insets.
78      */
79     private final Rect mDividerBounds = new Rect();
80     private final Rect mTempRect = new Rect();
81     private FrameLayout mDividerBar;
82 
83 
84     static final Property<DividerView, Integer> DIVIDER_HEIGHT_PROPERTY =
85             new Property<DividerView, Integer>(Integer.class, "height") {
86                 @Override
87                 public Integer get(DividerView object) {
88                     return object.mDividerBar.getLayoutParams().height;
89                 }
90 
91                 @Override
92                 public void set(DividerView object, Integer value) {
93                     ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)
94                             object.mDividerBar.getLayoutParams();
95                     lp.height = value;
96                     object.mDividerBar.setLayoutParams(lp);
97                 }
98             };
99 
100     private AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() {
101         @Override
102         public void onAnimationEnd(Animator animation) {
103             mSetTouchRegion = true;
104         }
105 
106         @Override
107         public void onAnimationCancel(Animator animation) {
108             mSetTouchRegion = true;
109         }
110     };
111 
DividerView(@onNull Context context)112     public DividerView(@NonNull Context context) {
113         super(context);
114     }
115 
DividerView(@onNull Context context, @Nullable AttributeSet attrs)116     public DividerView(@NonNull Context context,
117             @Nullable AttributeSet attrs) {
118         super(context, attrs);
119     }
120 
DividerView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)121     public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
122         super(context, attrs, defStyleAttr);
123     }
124 
DividerView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)125     public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
126             int defStyleRes) {
127         super(context, attrs, defStyleAttr, defStyleRes);
128     }
129 
130     /** Sets up essential dependencies of the divider bar. */
setup( SplitLayout layout, SplitWindowManager splitWindowManager, SurfaceControlViewHost viewHost, InsetsState insetsState)131     public void setup(
132             SplitLayout layout,
133             SplitWindowManager splitWindowManager,
134             SurfaceControlViewHost viewHost,
135             InsetsState insetsState) {
136         mSplitLayout = layout;
137         mSplitWindowManager = splitWindowManager;
138         mViewHost = viewHost;
139         mDividerBounds.set(layout.getDividerBounds());
140         onInsetsChanged(insetsState, false /* animate */);
141     }
142 
onInsetsChanged(InsetsState insetsState, boolean animate)143     void onInsetsChanged(InsetsState insetsState, boolean animate) {
144         mTempRect.set(mSplitLayout.getDividerBounds());
145         final InsetsSource taskBarInsetsSource =
146                 insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
147         // Only insets the divider bar with task bar when it's expanded so that the rounded corners
148         // will be drawn against task bar.
149         if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
150             mTempRect.inset(taskBarInsetsSource.calculateVisibleInsets(mTempRect));
151         }
152 
153         if (!mTempRect.equals(mDividerBounds)) {
154             if (animate) {
155                 ObjectAnimator animator = ObjectAnimator.ofInt(this,
156                         DIVIDER_HEIGHT_PROPERTY, mDividerBounds.height(), mTempRect.height());
157                 animator.setInterpolator(InsetsController.RESIZE_INTERPOLATOR);
158                 animator.setDuration(InsetsController.ANIMATION_DURATION_RESIZE);
159                 animator.addListener(mAnimatorListener);
160                 animator.start();
161             } else {
162                 DIVIDER_HEIGHT_PROPERTY.set(this, mTempRect.height());
163                 mSetTouchRegion = true;
164             }
165             mDividerBounds.set(mTempRect);
166         }
167     }
168 
169     @Override
onFinishInflate()170     protected void onFinishInflate() {
171         super.onFinishInflate();
172         mDividerBar = findViewById(R.id.divider_bar);
173         mHandle = findViewById(R.id.docked_divider_handle);
174         mBackground = findViewById(R.id.docked_divider_background);
175         mExpandedTaskBarHeight = getResources().getDimensionPixelSize(
176                 com.android.internal.R.dimen.taskbar_frame_height);
177         mTouchElevation = getResources().getDimensionPixelSize(
178                 R.dimen.docked_stack_divider_lift_elevation);
179         mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener());
180         mInteractive = true;
181         setOnTouchListener(this);
182     }
183 
184     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)185     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
186         super.onLayout(changed, left, top, right, bottom);
187         if (mSetTouchRegion) {
188             mTempRect.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(),
189                     mHandle.getBottom());
190             mSplitWindowManager.setTouchRegion(mTempRect);
191             mSetTouchRegion = false;
192         }
193     }
194 
195     @Override
onTouch(View v, MotionEvent event)196     public boolean onTouch(View v, MotionEvent event) {
197         if (mSplitLayout == null || !mInteractive) {
198             return false;
199         }
200 
201         if (mDoubleTapDetector.onTouchEvent(event)) {
202             return true;
203         }
204 
205         // Convert to use screen-based coordinates to prevent lost track of motion events while
206         // moving divider bar and calculating dragging velocity.
207         event.setLocation(event.getRawX(), event.getRawY());
208         final int action = event.getAction() & MotionEvent.ACTION_MASK;
209         final boolean isLandscape = isLandscape();
210         final int touchPos = (int) (isLandscape ? event.getX() : event.getY());
211         switch (action) {
212             case MotionEvent.ACTION_DOWN:
213                 mVelocityTracker = VelocityTracker.obtain();
214                 mVelocityTracker.addMovement(event);
215                 setTouching();
216                 mStartPos = touchPos;
217                 mMoving = false;
218                 break;
219             case MotionEvent.ACTION_MOVE:
220                 mVelocityTracker.addMovement(event);
221                 if (!mMoving && Math.abs(touchPos - mStartPos) > mTouchSlop) {
222                     mStartPos = touchPos;
223                     mMoving = true;
224                 }
225                 if (mMoving) {
226                     final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos;
227                     mSplitLayout.updateDivideBounds(position);
228                 }
229                 break;
230             case MotionEvent.ACTION_UP:
231             case MotionEvent.ACTION_CANCEL:
232                 releaseTouching();
233                 if (!mMoving) break;
234 
235                 mVelocityTracker.addMovement(event);
236                 mVelocityTracker.computeCurrentVelocity(1000 /* units */);
237                 final float velocity = isLandscape
238                         ? mVelocityTracker.getXVelocity()
239                         : mVelocityTracker.getYVelocity();
240                 final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos;
241                 final DividerSnapAlgorithm.SnapTarget snapTarget =
242                         mSplitLayout.findSnapTarget(position, velocity, false /* hardDismiss */);
243                 mSplitLayout.snapToTarget(position, snapTarget);
244                 mMoving = false;
245                 break;
246         }
247 
248         return true;
249     }
250 
setTouching()251     private void setTouching() {
252         setSlippery(false);
253         mHandle.setTouching(true, true);
254         // Lift handle as well so it doesn't get behind the background, even though it doesn't
255         // cast shadow.
256         mHandle.animate()
257                 .setInterpolator(Interpolators.TOUCH_RESPONSE)
258                 .setDuration(TOUCH_ANIMATION_DURATION)
259                 .translationZ(mTouchElevation)
260                 .start();
261     }
262 
releaseTouching()263     private void releaseTouching() {
264         setSlippery(true);
265         mHandle.setTouching(false, true);
266         mHandle.animate()
267                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
268                 .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
269                 .translationZ(0)
270                 .start();
271     }
272 
setSlippery(boolean slippery)273     private void setSlippery(boolean slippery) {
274         if (mViewHost == null) {
275             return;
276         }
277 
278         final WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams();
279         final boolean isSlippery = (lp.flags & FLAG_SLIPPERY) != 0;
280         if (isSlippery == slippery) {
281             return;
282         }
283 
284         if (slippery) {
285             lp.flags |= FLAG_SLIPPERY;
286         } else {
287             lp.flags &= ~FLAG_SLIPPERY;
288         }
289         mViewHost.relayout(lp);
290     }
291 
setInteractive(boolean interactive)292     void setInteractive(boolean interactive) {
293         if (interactive == mInteractive) return;
294         mInteractive = interactive;
295         releaseTouching();
296         mHandle.setVisibility(mInteractive ? View.VISIBLE : View.INVISIBLE);
297     }
298 
isLandscape()299     private boolean isLandscape() {
300         return getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE;
301     }
302 
303     private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener {
304         @Override
onDoubleTap(MotionEvent e)305         public boolean onDoubleTap(MotionEvent e) {
306             if (mSplitLayout != null) {
307                 mSplitLayout.onDoubleTappedDivider();
308             }
309             return true;
310         }
311     }
312 }
313