1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui; 16 17 import static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE; 18 import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE; 19 import static com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.animation.AnimatorSet; 24 import android.animation.ObjectAnimator; 25 import android.content.Context; 26 import android.provider.Settings; 27 import android.util.AttributeSet; 28 import android.view.Gravity; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.ViewOutlineProvider; 32 import android.view.ViewTreeObserver; 33 import android.widget.LinearLayout; 34 35 import com.android.systemui.tuner.TunerService; 36 import com.android.systemui.tuner.TunerService.Tunable; 37 import com.android.systemui.util.leak.RotationUtils; 38 39 /** 40 * Layout for placing two containers at a specific physical position on the device, relative to the 41 * device's hardware, regardless of screen rotation. 42 */ 43 public class HardwareUiLayout extends MultiListLayout implements Tunable { 44 45 private static final String EDGE_BLEED = "sysui_hwui_edge_bleed"; 46 private static final String ROUNDED_DIVIDER = "sysui_hwui_rounded_divider"; 47 private final int[] mTmp2 = new int[2]; 48 private ViewGroup mList; 49 private ViewGroup mSeparatedView; 50 private int mOldHeight; 51 private boolean mAnimating; 52 private AnimatorSet mAnimation; 53 private View mDivision; 54 private HardwareBgDrawable mListBackground; 55 private HardwareBgDrawable mSeparatedViewBackground; 56 private Animator mAnimator; 57 private boolean mCollapse; 58 private int mEndPoint; 59 private boolean mEdgeBleed; 60 private boolean mRoundedDivider; 61 private boolean mRotatedBackground; 62 private boolean mSwapOrientation = true; 63 HardwareUiLayout(Context context, AttributeSet attrs)64 public HardwareUiLayout(Context context, AttributeSet attrs) { 65 super(context, attrs); 66 // Manually re-initialize mRotation to portrait-mode, since this view must always 67 // be constructed in portrait mode and rotated into the correct initial position. 68 mRotation = ROTATION_NONE; 69 updateSettings(); 70 } 71 72 @Override getSeparatedView()73 protected ViewGroup getSeparatedView() { 74 return findViewById(com.android.systemui.R.id.separated_button); 75 } 76 77 @Override getListView()78 protected ViewGroup getListView() { 79 return findViewById(android.R.id.list); 80 } 81 82 @Override onAttachedToWindow()83 protected void onAttachedToWindow() { 84 super.onAttachedToWindow(); 85 updateSettings(); 86 Dependency.get(TunerService.class).addTunable(this, EDGE_BLEED, ROUNDED_DIVIDER); 87 getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener); 88 } 89 90 @Override onDetachedFromWindow()91 protected void onDetachedFromWindow() { 92 super.onDetachedFromWindow(); 93 getViewTreeObserver().removeOnComputeInternalInsetsListener(mInsetsListener); 94 Dependency.get(TunerService.class).removeTunable(this); 95 } 96 97 @Override onTuningChanged(String key, String newValue)98 public void onTuningChanged(String key, String newValue) { 99 updateSettings(); 100 } 101 updateSettings()102 private void updateSettings() { 103 mEdgeBleed = Settings.Secure.getInt(getContext().getContentResolver(), 104 EDGE_BLEED, 0) != 0; 105 mRoundedDivider = Settings.Secure.getInt(getContext().getContentResolver(), 106 ROUNDED_DIVIDER, 0) != 0; 107 updateEdgeMargin(mEdgeBleed ? 0 : getEdgePadding()); 108 mListBackground = new HardwareBgDrawable(mRoundedDivider, !mEdgeBleed, getContext()); 109 mSeparatedViewBackground = new HardwareBgDrawable(mRoundedDivider, !mEdgeBleed, 110 getContext()); 111 if (mList != null) { 112 mList.setBackground(mListBackground); 113 mSeparatedView.setBackground(mSeparatedViewBackground); 114 requestLayout(); 115 } 116 } 117 updateEdgeMargin(int edge)118 private void updateEdgeMargin(int edge) { 119 if (mList != null) { 120 MarginLayoutParams params = (MarginLayoutParams) mList.getLayoutParams(); 121 if (mRotation == ROTATION_LANDSCAPE) { 122 params.topMargin = edge; 123 } else if (mRotation == ROTATION_SEASCAPE) { 124 params.bottomMargin = edge; 125 } else { 126 params.rightMargin = edge; 127 } 128 mList.setLayoutParams(params); 129 } 130 131 if (mSeparatedView != null) { 132 MarginLayoutParams params = (MarginLayoutParams) mSeparatedView.getLayoutParams(); 133 if (mRotation == ROTATION_LANDSCAPE) { 134 params.topMargin = edge; 135 } else if (mRotation == ROTATION_SEASCAPE) { 136 params.bottomMargin = edge; 137 } else { 138 params.rightMargin = edge; 139 } 140 mSeparatedView.setLayoutParams(params); 141 } 142 } 143 getEdgePadding()144 private int getEdgePadding() { 145 return getContext().getResources().getDimensionPixelSize(R.dimen.edge_margin); 146 } 147 148 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)149 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 150 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 151 if (mList == null) { 152 if (getChildCount() != 0) { 153 mList = getListView(); 154 mList.setBackground(mListBackground); 155 mSeparatedView = getSeparatedView(); 156 mSeparatedView.setBackground(mSeparatedViewBackground); 157 updateEdgeMargin(mEdgeBleed ? 0 : getEdgePadding()); 158 mOldHeight = mList.getMeasuredHeight(); 159 160 // Must be called to initialize view rotation correctly. 161 // Requires LayoutParams, hence why this isn't called during the constructor. 162 updateRotation(); 163 } else { 164 return; 165 } 166 } 167 int newHeight = mList.getMeasuredHeight(); 168 if (newHeight != mOldHeight) { 169 animateChild(mOldHeight, newHeight); 170 } 171 172 post(() -> updatePaddingAndGravityIfTooTall()); 173 post(() -> updatePosition()); 174 } 175 setSwapOrientation(boolean swapOrientation)176 public void setSwapOrientation(boolean swapOrientation) { 177 mSwapOrientation = swapOrientation; 178 } 179 updateRotation()180 private void updateRotation() { 181 int rotation = RotationUtils.getRotation(getContext()); 182 if (rotation != mRotation) { 183 rotate(mRotation, rotation); 184 mRotation = rotation; 185 } 186 } 187 188 /** 189 * Requires LayoutParams to be set to work correctly, and therefore must be run after after 190 * the HardwareUILayout has been added to the view hierarchy. 191 */ rotate(int from, int to)192 protected void rotate(int from, int to) { 193 super.rotate(from, to); 194 if (from != ROTATION_NONE && to != ROTATION_NONE) { 195 // Rather than handling this confusing case, just do 2 rotations. 196 rotate(from, ROTATION_NONE); 197 rotate(ROTATION_NONE, to); 198 return; 199 } 200 if (from == ROTATION_LANDSCAPE || to == ROTATION_SEASCAPE) { 201 rotateRight(); 202 } else { 203 rotateLeft(); 204 } 205 if (mAdapter.hasSeparatedItems()) { 206 if (from == ROTATION_SEASCAPE || to == ROTATION_SEASCAPE) { 207 // Separated view has top margin, so seascape separated view need special rotation, 208 // not a full left or right rotation. 209 swapLeftAndTop(mSeparatedView); 210 } else if (from == ROTATION_LANDSCAPE) { 211 rotateRight(mSeparatedView); 212 } else { 213 rotateLeft(mSeparatedView); 214 } 215 } 216 if (to != ROTATION_NONE) { 217 if (mList instanceof LinearLayout) { 218 mRotatedBackground = true; 219 mListBackground.setRotatedBackground(true); 220 mSeparatedViewBackground.setRotatedBackground(true); 221 LinearLayout linearLayout = (LinearLayout) mList; 222 if (mSwapOrientation) { 223 linearLayout.setOrientation(LinearLayout.HORIZONTAL); 224 setOrientation(LinearLayout.HORIZONTAL); 225 } 226 swapDimens(mList); 227 swapDimens(mSeparatedView); 228 } 229 } else { 230 if (mList instanceof LinearLayout) { 231 mRotatedBackground = false; 232 mListBackground.setRotatedBackground(false); 233 mSeparatedViewBackground.setRotatedBackground(false); 234 LinearLayout linearLayout = (LinearLayout) mList; 235 if (mSwapOrientation) { 236 linearLayout.setOrientation(LinearLayout.VERTICAL); 237 setOrientation(LinearLayout.VERTICAL); 238 } 239 swapDimens(mList); 240 swapDimens(mSeparatedView); 241 } 242 } 243 } 244 245 @Override onUpdateList()246 public void onUpdateList() { 247 super.onUpdateList(); 248 249 for (int i = 0; i < mAdapter.getCount(); i++) { 250 ViewGroup parent; 251 boolean separated = mAdapter.shouldBeSeparated(i); 252 if (separated) { 253 parent = getSeparatedView(); 254 } else { 255 parent = getListView(); 256 } 257 View v = mAdapter.getView(i, null, parent); 258 parent.addView(v); 259 } 260 } 261 rotateRight()262 private void rotateRight() { 263 rotateRight(this); 264 rotateRight(mList); 265 swapDimens(this); 266 267 LayoutParams p = (LayoutParams) mList.getLayoutParams(); 268 p.gravity = rotateGravityRight(p.gravity); 269 mList.setLayoutParams(p); 270 271 LayoutParams separatedViewLayoutParams = (LayoutParams) mSeparatedView.getLayoutParams(); 272 separatedViewLayoutParams.gravity = rotateGravityRight(separatedViewLayoutParams.gravity); 273 mSeparatedView.setLayoutParams(separatedViewLayoutParams); 274 275 setGravity(rotateGravityRight(getGravity())); 276 } 277 swapDimens(View v)278 private void swapDimens(View v) { 279 ViewGroup.LayoutParams params = v.getLayoutParams(); 280 int h = params.width; 281 params.width = params.height; 282 params.height = h; 283 v.setLayoutParams(params); 284 } 285 rotateGravityRight(int gravity)286 private int rotateGravityRight(int gravity) { 287 int retGravity = 0; 288 int layoutDirection = getLayoutDirection(); 289 final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); 290 final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; 291 292 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 293 case Gravity.CENTER_HORIZONTAL: 294 retGravity |= Gravity.CENTER_VERTICAL; 295 break; 296 case Gravity.RIGHT: 297 retGravity |= Gravity.BOTTOM; 298 break; 299 case Gravity.LEFT: 300 default: 301 retGravity |= Gravity.TOP; 302 break; 303 } 304 305 switch (verticalGravity) { 306 case Gravity.CENTER_VERTICAL: 307 retGravity |= Gravity.CENTER_HORIZONTAL; 308 break; 309 case Gravity.BOTTOM: 310 retGravity |= Gravity.LEFT; 311 break; 312 case Gravity.TOP: 313 default: 314 retGravity |= Gravity.RIGHT; 315 break; 316 } 317 return retGravity; 318 } 319 rotateLeft()320 private void rotateLeft() { 321 rotateLeft(this); 322 rotateLeft(mList); 323 swapDimens(this); 324 325 LayoutParams p = (LayoutParams) mList.getLayoutParams(); 326 p.gravity = rotateGravityLeft(p.gravity); 327 mList.setLayoutParams(p); 328 329 LayoutParams separatedViewLayoutParams = (LayoutParams) mSeparatedView.getLayoutParams(); 330 separatedViewLayoutParams.gravity = rotateGravityLeft(separatedViewLayoutParams.gravity); 331 mSeparatedView.setLayoutParams(separatedViewLayoutParams); 332 333 setGravity(rotateGravityLeft(getGravity())); 334 } 335 rotateGravityLeft(int gravity)336 private int rotateGravityLeft(int gravity) { 337 if (gravity == -1) { 338 gravity = Gravity.TOP | Gravity.START; 339 } 340 int retGravity = 0; 341 int layoutDirection = getLayoutDirection(); 342 final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); 343 final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; 344 345 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 346 case Gravity.CENTER_HORIZONTAL: 347 retGravity |= Gravity.CENTER_VERTICAL; 348 break; 349 case Gravity.RIGHT: 350 retGravity |= Gravity.TOP; 351 break; 352 case Gravity.LEFT: 353 default: 354 retGravity |= Gravity.BOTTOM; 355 break; 356 } 357 358 switch (verticalGravity) { 359 case Gravity.CENTER_VERTICAL: 360 retGravity |= Gravity.CENTER_HORIZONTAL; 361 break; 362 case Gravity.BOTTOM: 363 retGravity |= Gravity.RIGHT; 364 break; 365 case Gravity.TOP: 366 default: 367 retGravity |= Gravity.LEFT; 368 break; 369 } 370 return retGravity; 371 } 372 rotateLeft(View v)373 private void rotateLeft(View v) { 374 v.setPadding(v.getPaddingTop(), v.getPaddingRight(), v.getPaddingBottom(), 375 v.getPaddingLeft()); 376 MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams(); 377 params.setMargins(params.topMargin, params.rightMargin, params.bottomMargin, 378 params.leftMargin); 379 v.setLayoutParams(params); 380 } 381 rotateRight(View v)382 private void rotateRight(View v) { 383 v.setPadding(v.getPaddingBottom(), v.getPaddingLeft(), v.getPaddingTop(), 384 v.getPaddingRight()); 385 MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams(); 386 params.setMargins(params.bottomMargin, params.leftMargin, params.topMargin, 387 params.rightMargin); 388 v.setLayoutParams(params); 389 } 390 swapLeftAndTop(View v)391 private void swapLeftAndTop(View v) { 392 v.setPadding(v.getPaddingTop(), v.getPaddingLeft(), v.getPaddingBottom(), 393 v.getPaddingRight()); 394 MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams(); 395 params.setMargins(params.topMargin, params.leftMargin, params.bottomMargin, 396 params.rightMargin); 397 v.setLayoutParams(params); 398 } 399 400 @Override onLayout(boolean changed, int left, int top, int right, int bottom)401 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 402 super.onLayout(changed, left, top, right, bottom); 403 404 post(() -> updatePosition()); 405 406 } 407 animateChild(int oldHeight, int newHeight)408 private void animateChild(int oldHeight, int newHeight) { 409 if (true) return; 410 if (mAnimating) { 411 mAnimation.cancel(); 412 } 413 mAnimating = true; 414 mAnimation = new AnimatorSet(); 415 mAnimation.addListener(new AnimatorListenerAdapter() { 416 @Override 417 public void onAnimationEnd(Animator animation) { 418 mAnimating = false; 419 } 420 }); 421 int fromTop = mList.getTop(); 422 int fromBottom = mList.getBottom(); 423 int toTop = fromTop - ((newHeight - oldHeight) / 2); 424 int toBottom = fromBottom + ((newHeight - oldHeight) / 2); 425 ObjectAnimator top = ObjectAnimator.ofInt(mList, "top", fromTop, toTop); 426 top.addUpdateListener(animation -> mListBackground.invalidateSelf()); 427 mAnimation.playTogether(top, 428 ObjectAnimator.ofInt(mList, "bottom", fromBottom, toBottom)); 429 } 430 setDivisionView(View v)431 public void setDivisionView(View v) { 432 mDivision = v; 433 if (mDivision != null) { 434 mDivision.addOnLayoutChangeListener( 435 (v1, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> 436 updatePosition()); 437 } 438 updatePosition(); 439 } 440 updatePosition()441 private void updatePosition() { 442 if (mList == null) return; 443 // If got separated button, setRotatedBackground to false, 444 // all items won't get white background. 445 boolean separated = mAdapter.hasSeparatedItems(); 446 mListBackground.setRotatedBackground(separated); 447 mSeparatedViewBackground.setRotatedBackground(separated); 448 if (mDivision != null && mDivision.getVisibility() == VISIBLE) { 449 int index = mRotatedBackground ? 0 : 1; 450 mDivision.getLocationOnScreen(mTmp2); 451 float trans = mRotatedBackground ? mDivision.getTranslationX() 452 : mDivision.getTranslationY(); 453 int viewTop = (int) (mTmp2[index] + trans); 454 mList.getLocationOnScreen(mTmp2); 455 viewTop -= mTmp2[index]; 456 setCutPoint(viewTop); 457 } else { 458 setCutPoint(mList.getMeasuredHeight()); 459 } 460 } 461 setCutPoint(int point)462 private void setCutPoint(int point) { 463 int curPoint = mListBackground.getCutPoint(); 464 if (curPoint == point) return; 465 if (getAlpha() == 0 || curPoint == 0) { 466 mListBackground.setCutPoint(point); 467 return; 468 } 469 if (mAnimator != null) { 470 if (mEndPoint == point) { 471 return; 472 } 473 mAnimator.cancel(); 474 } 475 mEndPoint = point; 476 mAnimator = ObjectAnimator.ofInt(mListBackground, "cutPoint", curPoint, point); 477 if (mCollapse) { 478 mAnimator.setStartDelay(300); 479 mCollapse = false; 480 } 481 mAnimator.start(); 482 } 483 484 // If current power menu height larger then screen height, remove padding to break power menu 485 // alignment and set menu center vertical within the screen. updatePaddingAndGravityIfTooTall()486 private void updatePaddingAndGravityIfTooTall() { 487 int defaultTopPadding; 488 int viewsTotalHeight; 489 int separatedViewTopMargin; 490 int screenHeight; 491 int totalHeight; 492 int targetGravity; 493 boolean separated = mAdapter.hasSeparatedItems(); 494 MarginLayoutParams params = (MarginLayoutParams) mSeparatedView.getLayoutParams(); 495 switch (RotationUtils.getRotation(getContext())) { 496 case RotationUtils.ROTATION_LANDSCAPE: 497 defaultTopPadding = getPaddingLeft(); 498 viewsTotalHeight = mList.getMeasuredWidth() + mSeparatedView.getMeasuredWidth(); 499 separatedViewTopMargin = separated ? params.leftMargin : 0; 500 screenHeight = getMeasuredWidth(); 501 targetGravity = Gravity.CENTER_HORIZONTAL|Gravity.TOP; 502 break; 503 case RotationUtils.ROTATION_SEASCAPE: 504 defaultTopPadding = getPaddingRight(); 505 viewsTotalHeight = mList.getMeasuredWidth() + mSeparatedView.getMeasuredWidth(); 506 separatedViewTopMargin = separated ? params.leftMargin : 0; 507 screenHeight = getMeasuredWidth(); 508 targetGravity = Gravity.CENTER_HORIZONTAL|Gravity.BOTTOM; 509 break; 510 default: // Portrait 511 defaultTopPadding = getPaddingTop(); 512 viewsTotalHeight = mList.getMeasuredHeight() + mSeparatedView.getMeasuredHeight(); 513 separatedViewTopMargin = separated ? params.topMargin : 0; 514 screenHeight = getMeasuredHeight(); 515 targetGravity = Gravity.CENTER_VERTICAL|Gravity.RIGHT; 516 break; 517 } 518 totalHeight = defaultTopPadding + viewsTotalHeight + separatedViewTopMargin; 519 if (totalHeight >= screenHeight) { 520 setPadding(0, 0, 0, 0); 521 setGravity(targetGravity); 522 } 523 } 524 525 @Override getOutlineProvider()526 public ViewOutlineProvider getOutlineProvider() { 527 return super.getOutlineProvider(); 528 } 529 setCollapse()530 public void setCollapse() { 531 mCollapse = true; 532 } 533 534 private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener = inoutInfo -> { 535 if (mHasOutsideTouch || (mList == null)) { 536 inoutInfo.setTouchableInsets( 537 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); 538 return; 539 } 540 inoutInfo.setTouchableInsets( 541 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT); 542 inoutInfo.contentInsets.set(mList.getLeft(), mList.getTop(), 543 0, getBottom() - mList.getBottom()); 544 }; 545 getAnimationDistance()546 private float getAnimationDistance() { 547 return getContext().getResources().getDimension( 548 com.android.systemui.R.dimen.global_actions_panel_width) / 2; 549 } 550 551 @Override getAnimationOffsetX()552 public float getAnimationOffsetX() { 553 if (RotationUtils.getRotation(mContext) == ROTATION_NONE) { 554 return getAnimationDistance(); 555 } 556 return 0; 557 } 558 559 @Override getAnimationOffsetY()560 public float getAnimationOffsetY() { 561 switch (RotationUtils.getRotation(getContext())) { 562 case RotationUtils.ROTATION_LANDSCAPE: 563 return -getAnimationDistance(); 564 case RotationUtils.ROTATION_SEASCAPE: 565 return getAnimationDistance(); 566 default: // Portrait 567 return 0; 568 } 569 } 570 }