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