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