• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
20 import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
21 import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
22 
23 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CURSOR_HOVER_STATES_ENABLED;
24 
25 import android.animation.Animator;
26 import android.animation.AnimatorListenerAdapter;
27 import android.animation.ObjectAnimator;
28 import android.content.Context;
29 import android.graphics.Canvas;
30 import android.graphics.Paint;
31 import android.graphics.Rect;
32 import android.os.Bundle;
33 import android.provider.DeviceConfig;
34 import android.util.AttributeSet;
35 import android.util.Property;
36 import android.view.GestureDetector;
37 import android.view.InsetsController;
38 import android.view.InsetsSource;
39 import android.view.InsetsState;
40 import android.view.MotionEvent;
41 import android.view.PointerIcon;
42 import android.view.SurfaceControlViewHost;
43 import android.view.VelocityTracker;
44 import android.view.View;
45 import android.view.ViewConfiguration;
46 import android.view.ViewGroup;
47 import android.view.WindowInsets;
48 import android.view.WindowManager;
49 import android.view.accessibility.AccessibilityNodeInfo;
50 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
51 import android.widget.FrameLayout;
52 
53 import androidx.annotation.NonNull;
54 import androidx.annotation.Nullable;
55 
56 import com.android.internal.annotations.VisibleForTesting;
57 import com.android.internal.protolog.ProtoLog;
58 import com.android.wm.shell.R;
59 import com.android.wm.shell.protolog.ShellProtoLogGroup;
60 import com.android.wm.shell.shared.animation.Interpolators;
61 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
62 
63 /**
64  * Divider for multi window splits.
65  */
66 public class DividerView extends FrameLayout implements View.OnTouchListener {
67     public static final long TOUCH_ANIMATION_DURATION = 150;
68     public static final long TOUCH_RELEASE_ANIMATION_DURATION = 200;
69 
70     private final Paint mPaint = new Paint();
71     private final Rect mBackgroundRect = new Rect();
72     private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
73 
74     private SplitLayout mSplitLayout;
75     private SplitWindowManager mSplitWindowManager;
76     private SurfaceControlViewHost mViewHost;
77     private DividerHandleView mHandle;
78     private DividerRoundedCorner mCorners;
79     private int mTouchElevation;
80 
81     private VelocityTracker mVelocityTracker;
82     private boolean mMoving;
83     private int mStartPos;
84     private GestureDetector mDoubleTapDetector;
85     private boolean mInteractive;
86     private boolean mHideHandle;
87     private boolean mSetTouchRegion = true;
88     private int mLastDraggingPosition;
89     private int mHandleRegionWidth;
90     private int mHandleRegionHeight;
91 
92     /**
93      * This is not the visible bounds you see on screen, but the actual behind-the-scenes window
94      * bounds, which is larger.
95      */
96     private final Rect mDividerBounds = new Rect();
97     private final Rect mTempRect = new Rect();
98     private FrameLayout mDividerBar;
99 
100     static final Property<DividerView, Integer> DIVIDER_HEIGHT_PROPERTY =
101             new Property<DividerView, Integer>(Integer.class, "height") {
102                 @Override
103                 public Integer get(DividerView object) {
104                     return object.mDividerBar.getLayoutParams().height;
105                 }
106 
107                 @Override
108                 public void set(DividerView object, Integer value) {
109                     ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)
110                             object.mDividerBar.getLayoutParams();
111                     lp.height = value;
112                     object.mDividerBar.setLayoutParams(lp);
113                 }
114             };
115 
116     private AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() {
117         @Override
118         public void onAnimationEnd(Animator animation) {
119             mSetTouchRegion = true;
120         }
121 
122         @Override
123         public void onAnimationCancel(Animator animation) {
124             mSetTouchRegion = true;
125         }
126     };
127 
128     final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() {
129         @Override
130         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
131             super.onInitializeAccessibilityNodeInfo(host, info);
132             final DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm;
133             if (mSplitLayout.isLeftRightSplit()) {
134                 info.addAction(new AccessibilityAction(R.id.action_move_tl_full,
135                         mContext.getString(R.string.accessibility_action_divider_left_full)));
136                 if (snapAlgorithm.isFirstSplitTargetAvailable()) {
137                     info.addAction(new AccessibilityAction(R.id.action_move_tl_70,
138                             mContext.getString(R.string.accessibility_action_divider_left_70)));
139                 }
140                 if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) {
141                     // Only show the middle target if there are more than 1 split target
142                     info.addAction(new AccessibilityAction(R.id.action_move_tl_50,
143                             mContext.getString(R.string.accessibility_action_divider_left_50)));
144                 }
145                 if (snapAlgorithm.isLastSplitTargetAvailable()) {
146                     info.addAction(new AccessibilityAction(R.id.action_move_tl_30,
147                             mContext.getString(R.string.accessibility_action_divider_left_30)));
148                 }
149                 info.addAction(new AccessibilityAction(R.id.action_move_rb_full,
150                         mContext.getString(R.string.accessibility_action_divider_right_full)));
151                 info.addAction(new AccessibilityAction(R.id.action_swap_apps,
152                         mContext.getString(R.string.accessibility_action_divider_swap_horizontal)));
153             } else {
154                 info.addAction(new AccessibilityAction(R.id.action_move_tl_full,
155                         mContext.getString(R.string.accessibility_action_divider_top_full)));
156                 if (snapAlgorithm.isFirstSplitTargetAvailable()) {
157                     info.addAction(new AccessibilityAction(R.id.action_move_tl_70,
158                             mContext.getString(R.string.accessibility_action_divider_top_70)));
159                 }
160                 if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) {
161                     // Only show the middle target if there are more than 1 split target
162                     info.addAction(new AccessibilityAction(R.id.action_move_tl_50,
163                             mContext.getString(R.string.accessibility_action_divider_top_50)));
164                 }
165                 if (snapAlgorithm.isLastSplitTargetAvailable()) {
166                     info.addAction(new AccessibilityAction(R.id.action_move_tl_30,
167                             mContext.getString(R.string.accessibility_action_divider_top_30)));
168                 }
169                 info.addAction(new AccessibilityAction(R.id.action_move_rb_full,
170                         mContext.getString(R.string.accessibility_action_divider_bottom_full)));
171                 info.addAction(new AccessibilityAction(R.id.action_swap_apps,
172                         mContext.getString(R.string.accessibility_action_divider_swap_vertical)));
173             }
174         }
175 
176         @Override
177         public boolean performAccessibilityAction(@NonNull View host, int action,
178                 @Nullable Bundle args) {
179             if (action == R.id.action_swap_apps) {
180                 mSplitLayout.onDoubleTappedDivider();
181                 return true;
182             }
183 
184             DividerSnapAlgorithm.SnapTarget nextTarget = null;
185             DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm;
186             if (action == R.id.action_move_tl_full) {
187                 nextTarget = snapAlgorithm.getDismissEndTarget();
188             } else if (action == R.id.action_move_tl_70) {
189                 nextTarget = snapAlgorithm.getLastSplitTarget();
190             } else if (action == R.id.action_move_tl_50) {
191                 nextTarget = snapAlgorithm.getMiddleTarget();
192             } else if (action == R.id.action_move_tl_30) {
193                 nextTarget = snapAlgorithm.getFirstSplitTarget();
194             } else if (action == R.id.action_move_rb_full) {
195                 nextTarget = snapAlgorithm.getDismissStartTarget();
196             }
197             if (nextTarget != null) {
198                 mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), nextTarget);
199                 return true;
200             }
201             return super.performAccessibilityAction(host, action, args);
202         }
203     };
204 
DividerView(@onNull Context context)205     public DividerView(@NonNull Context context) {
206         super(context);
207     }
208 
DividerView(@onNull Context context, @Nullable AttributeSet attrs)209     public DividerView(@NonNull Context context,
210             @Nullable AttributeSet attrs) {
211         super(context, attrs);
212     }
213 
DividerView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)214     public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
215         super(context, attrs, defStyleAttr);
216     }
217 
DividerView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)218     public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
219             int defStyleRes) {
220         super(context, attrs, defStyleAttr, defStyleRes);
221     }
222 
223     /** Sets up essential dependencies of the divider bar. */
setup(SplitLayout layout, SplitWindowManager splitWindowManager, SurfaceControlViewHost viewHost, InsetsState insetsState)224     public void setup(SplitLayout layout, SplitWindowManager splitWindowManager,
225             SurfaceControlViewHost viewHost, InsetsState insetsState) {
226         mSplitLayout = layout;
227         mSplitWindowManager = splitWindowManager;
228         mViewHost = viewHost;
229         layout.getDividerBounds(mDividerBounds);
230         onInsetsChanged(insetsState, false /* animate */);
231 
232         final boolean isLeftRightSplit = mSplitLayout.isLeftRightSplit();
233         mHandle.setIsLeftRightSplit(isLeftRightSplit);
234         mCorners.setIsLeftRightSplit(isLeftRightSplit);
235 
236         mHandleRegionWidth = getResources().getDimensionPixelSize(isLeftRightSplit
237                 ? R.dimen.split_divider_handle_region_height
238                 : R.dimen.split_divider_handle_region_width);
239         mHandleRegionHeight = getResources().getDimensionPixelSize(isLeftRightSplit
240                 ? R.dimen.split_divider_handle_region_width
241                 : DesktopModeStatus.canEnterDesktopMode(mContext)
242                         ? R.dimen.desktop_mode_portrait_split_divider_handle_region_height
243                         : R.dimen.split_divider_handle_region_height);
244     }
245 
onInsetsChanged(InsetsState insetsState, boolean animate)246     void onInsetsChanged(InsetsState insetsState, boolean animate) {
247         mSplitLayout.getDividerBounds(mTempRect);
248         // Only insets the divider bar with task bar when it's expanded so that the rounded corners
249         // will be drawn against task bar.
250         // But there is no need to do it when IME showing because there are no rounded corners at
251         // the bottom. This also avoids the problem of task bar height not changing when IME
252         // floating.
253         if (!insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME, WindowInsets.Type.ime())) {
254             for (int i = insetsState.sourceSize() - 1; i >= 0; i--) {
255                 final InsetsSource source = insetsState.sourceAt(i);
256                 if (source.getType() == WindowInsets.Type.navigationBars()
257                         && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)) {
258                     mTempRect.inset(source.calculateVisibleInsets(mTempRect));
259                 }
260             }
261         }
262 
263         if (!mTempRect.equals(mDividerBounds)) {
264             if (animate) {
265                 ObjectAnimator animator = ObjectAnimator.ofInt(this,
266                         DIVIDER_HEIGHT_PROPERTY, mDividerBounds.height(), mTempRect.height());
267                 animator.setInterpolator(InsetsController.RESIZE_INTERPOLATOR);
268                 animator.setDuration(InsetsController.ANIMATION_DURATION_RESIZE);
269                 animator.addListener(mAnimatorListener);
270                 animator.start();
271             } else {
272                 DIVIDER_HEIGHT_PROPERTY.set(this, mTempRect.height());
273                 mSetTouchRegion = true;
274             }
275             mDividerBounds.set(mTempRect);
276         }
277     }
278 
279     @Override
onFinishInflate()280     protected void onFinishInflate() {
281         super.onFinishInflate();
282         mDividerBar = findViewById(R.id.divider_bar);
283         mHandle = findViewById(R.id.docked_divider_handle);
284         mCorners = findViewById(R.id.docked_divider_rounded_corner);
285         mTouchElevation = getResources().getDimensionPixelSize(
286                 R.dimen.docked_stack_divider_lift_elevation);
287         mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener());
288         mInteractive = true;
289         mHideHandle = false;
290         setOnTouchListener(this);
291         mHandle.setAccessibilityDelegate(mHandleDelegate);
292         setWillNotDraw(false);
293         mPaint.setColor(getResources().getColor(R.color.split_divider_background, null));
294         mPaint.setAntiAlias(true);
295         mPaint.setStyle(Paint.Style.FILL);
296     }
297 
298     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)299     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
300         super.onLayout(changed, left, top, right, bottom);
301         if (mSetTouchRegion) {
302             int startX = (mDividerBounds.width() - mHandleRegionWidth) / 2;
303             int startY = (mDividerBounds.height() - mHandleRegionHeight) / 2;
304             mTempRect.set(startX, startY, startX + mHandleRegionWidth,
305                     startY + mHandleRegionHeight);
306             mSplitWindowManager.setTouchRegion(mTempRect);
307             mSetTouchRegion = false;
308         }
309 
310         if (changed) {
311             boolean isHorizontalSplit = mSplitLayout.isLeftRightSplit();
312             int dividerSize = getResources().getDimensionPixelSize(R.dimen.split_divider_bar_width);
313             left = isHorizontalSplit ? (getWidth() - dividerSize) / 2 : 0;
314             top = isHorizontalSplit ? 0 : (getHeight() - dividerSize) / 2;
315             right = isHorizontalSplit ? left + dividerSize : getWidth();
316             bottom = isHorizontalSplit ? getHeight() : top + dividerSize;
317             mBackgroundRect.set(left, top, right, bottom);
318         }
319     }
320 
321     @Override
onResolvePointerIcon(MotionEvent event, int pointerIndex)322     public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
323         return PointerIcon.getSystemIcon(getContext(),
324                 mSplitLayout.isLeftRightSplit() ? TYPE_HORIZONTAL_DOUBLE_ARROW
325                         : TYPE_VERTICAL_DOUBLE_ARROW);
326     }
327 
328     @Override
onTouch(View v, MotionEvent event)329     public boolean onTouch(View v, MotionEvent event) {
330         if (mSplitLayout == null || !mInteractive) {
331             return false;
332         }
333 
334         if (mDoubleTapDetector.onTouchEvent(event)) {
335             return true;
336         }
337 
338         // Convert to use screen-based coordinates to prevent lost track of motion events while
339         // moving divider bar and calculating dragging velocity.
340         event.setLocation(event.getRawX(), event.getRawY());
341         final int action = event.getAction() & MotionEvent.ACTION_MASK;
342         final boolean isLeftRightSplit = mSplitLayout.isLeftRightSplit();
343         final int touchPos = (int) (isLeftRightSplit ? event.getX() : event.getY());
344         switch (action) {
345             case MotionEvent.ACTION_DOWN:
346                 mVelocityTracker = VelocityTracker.obtain();
347                 mVelocityTracker.addMovement(event);
348                 setTouching();
349                 mStartPos = touchPos;
350                 mMoving = false;
351                 mSplitLayout.onStartDragging();
352                 break;
353             case MotionEvent.ACTION_MOVE:
354                 mVelocityTracker.addMovement(event);
355                 if (!mMoving && Math.abs(touchPos - mStartPos) > mTouchSlop) {
356                     mStartPos = touchPos;
357                     mMoving = true;
358                 }
359                 if (mMoving) {
360                     final int position = mSplitLayout.getDividerPosition() + touchPos - mStartPos;
361                     mLastDraggingPosition = position;
362                     mSplitLayout.updateDividerBounds(position, true /* shouldUseParallaxEffect */);
363                 }
364                 break;
365             case MotionEvent.ACTION_UP:
366             case MotionEvent.ACTION_CANCEL:
367                 releaseTouching();
368                 if (!mMoving) {
369                     mSplitLayout.onDraggingCancelled();
370                     break;
371                 }
372 
373                 mVelocityTracker.addMovement(event);
374                 mVelocityTracker.computeCurrentVelocity(1000 /* units */);
375                 final float velocity = isLeftRightSplit
376                         ? mVelocityTracker.getXVelocity()
377                         : mVelocityTracker.getYVelocity();
378                 final int position = mSplitLayout.getDividerPosition() + touchPos - mStartPos;
379                 final DividerSnapAlgorithm.SnapTarget snapTarget =
380                         mSplitLayout.findSnapTarget(position, velocity, false /* hardDismiss */);
381                 mSplitLayout.snapToTarget(position, snapTarget);
382                 mMoving = false;
383                 break;
384         }
385 
386         return true;
387     }
388 
setTouching()389     private void setTouching() {
390         setSlippery(false);
391         mHandle.setTouching(true, true);
392         // Lift handle as well so it doesn't get behind the background, even though it doesn't
393         // cast shadow.
394         mHandle.animate()
395                 .setInterpolator(Interpolators.TOUCH_RESPONSE)
396                 .setDuration(TOUCH_ANIMATION_DURATION)
397                 .translationZ(mTouchElevation)
398                 .start();
399     }
400 
releaseTouching()401     private void releaseTouching() {
402         setSlippery(true);
403         mHandle.setTouching(false, true);
404         mHandle.animate()
405                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
406                 .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
407                 .translationZ(0)
408                 .start();
409     }
410 
setSlippery(boolean slippery)411     private void setSlippery(boolean slippery) {
412         if (mViewHost == null) {
413             return;
414         }
415 
416         final WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams();
417         final boolean isSlippery = (lp.flags & FLAG_SLIPPERY) != 0;
418         if (isSlippery == slippery) {
419             return;
420         }
421 
422         if (slippery) {
423             lp.flags |= FLAG_SLIPPERY;
424         } else {
425             lp.flags &= ~FLAG_SLIPPERY;
426         }
427         mViewHost.relayout(lp);
428     }
429 
430     @Override
onHoverEvent(MotionEvent event)431     public boolean onHoverEvent(MotionEvent event) {
432         if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, CURSOR_HOVER_STATES_ENABLED,
433                 /* defaultValue = */ false)) {
434             return false;
435         }
436 
437         if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
438             setHovering();
439             return true;
440         } else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
441             releaseHovering();
442             return true;
443         }
444         return false;
445     }
446 
447     @VisibleForTesting
setHovering()448     void setHovering() {
449         mHandle.setHovering(true, true);
450         mHandle.animate()
451                 .setInterpolator(Interpolators.TOUCH_RESPONSE)
452                 .setDuration(TOUCH_ANIMATION_DURATION)
453                 .translationZ(mTouchElevation)
454                 .start();
455     }
456 
457     @Override
onDraw(@onNull Canvas canvas)458     protected void onDraw(@NonNull Canvas canvas) {
459         canvas.drawRect(mBackgroundRect, mPaint);
460     }
461 
462     @VisibleForTesting
releaseHovering()463     void releaseHovering() {
464         mHandle.setHovering(false, true);
465         mHandle.animate()
466                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
467                 .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
468                 .translationZ(0)
469                 .start();
470     }
471 
472     /**
473      * Set divider should interactive to user or not.
474      *
475      * @param interactive divider interactive.
476      * @param hideHandle divider handle hidden or not, only work when interactive is false.
477      * @param from caller from where.
478      */
setInteractive(boolean interactive, boolean hideHandle, String from)479     void setInteractive(boolean interactive, boolean hideHandle, String from) {
480         if (interactive == mInteractive) return;
481         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
482                 "Set divider bar %s hide handle=%b from %s",
483                 interactive ? "interactive" : "non-interactive", hideHandle, from);
484         mInteractive = interactive;
485         mHideHandle = hideHandle;
486         if (!mInteractive && mHideHandle && mMoving) {
487             final int position = mSplitLayout.getDividerPosition();
488             mSplitLayout.flingDividerPosition(
489                     mLastDraggingPosition,
490                     position,
491                     mSplitLayout.FLING_RESIZE_DURATION,
492                     Interpolators.FAST_OUT_SLOW_IN,
493                     () -> mSplitLayout.setDividerPosition(position, true /* applyLayoutChange */));
494             mMoving = false;
495         }
496         releaseTouching();
497         mHandle.setVisibility(!mInteractive && mHideHandle ? View.INVISIBLE : View.VISIBLE);
498     }
499 
isInteractive()500     boolean isInteractive() {
501         return mInteractive;
502     }
503 
isHandleHidden()504     boolean isHandleHidden() {
505         return mHideHandle;
506     }
507 
508     /** Returns true if the divider is currently being physically controlled by the user. */
isMoving()509     boolean isMoving() {
510         return mMoving;
511     }
512 
513     private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener {
514         @Override
onDoubleTap(MotionEvent e)515         public boolean onDoubleTap(MotionEvent e) {
516             if (mSplitLayout != null) {
517                 mSplitLayout.onDoubleTappedDivider();
518             }
519             return true;
520         }
521 
522         @Override
onDoubleTapEvent(@onNull MotionEvent e)523         public boolean onDoubleTapEvent(@NonNull MotionEvent e) {
524             return true;
525         }
526     }
527 }
528