1 /* 2 * Copyright (C) 2019 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.launcher3.touch; 18 19 import static android.view.Gravity.BOTTOM; 20 import static android.view.Gravity.CENTER_HORIZONTAL; 21 import static android.view.Gravity.END; 22 import static android.view.Gravity.START; 23 import static android.view.Gravity.TOP; 24 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 25 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 26 27 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; 28 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; 29 import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL; 30 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; 31 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; 32 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN; 33 34 import android.content.res.Resources; 35 import android.graphics.Matrix; 36 import android.graphics.PointF; 37 import android.graphics.Rect; 38 import android.graphics.RectF; 39 import android.graphics.drawable.ShapeDrawable; 40 import android.util.FloatProperty; 41 import android.util.Pair; 42 import android.view.MotionEvent; 43 import android.view.Surface; 44 import android.view.VelocityTracker; 45 import android.view.View; 46 import android.view.accessibility.AccessibilityEvent; 47 import android.widget.FrameLayout; 48 import android.widget.LinearLayout; 49 50 import com.android.launcher3.DeviceProfile; 51 import com.android.launcher3.R; 52 import com.android.launcher3.Utilities; 53 import com.android.launcher3.util.SplitConfigurationOptions; 54 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; 55 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; 56 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; 57 58 import java.util.ArrayList; 59 import java.util.List; 60 61 public class PortraitPagedViewHandler implements PagedOrientationHandler { 62 63 private final Matrix mTmpMatrix = new Matrix(); 64 private final RectF mTmpRectF = new RectF(); 65 66 @Override getPrimaryValue(T x, T y)67 public <T> T getPrimaryValue(T x, T y) { 68 return x; 69 } 70 71 @Override getSecondaryValue(T x, T y)72 public <T> T getSecondaryValue(T x, T y) { 73 return y; 74 } 75 76 @Override getPrimaryValue(int x, int y)77 public int getPrimaryValue(int x, int y) { 78 return x; 79 } 80 81 @Override getSecondaryValue(int x, int y)82 public int getSecondaryValue(int x, int y) { 83 return y; 84 } 85 86 @Override getPrimaryValue(float x, float y)87 public float getPrimaryValue(float x, float y) { 88 return x; 89 } 90 91 @Override getSecondaryValue(float x, float y)92 public float getSecondaryValue(float x, float y) { 93 return y; 94 } 95 96 @Override isLayoutNaturalToLauncher()97 public boolean isLayoutNaturalToLauncher() { 98 return true; 99 } 100 101 @Override adjustFloatingIconStartVelocity(PointF velocity)102 public void adjustFloatingIconStartVelocity(PointF velocity) { 103 //no-op 104 } 105 106 @Override fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile)107 public void fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile) { 108 if (outStartRect.left > deviceProfile.widthPx) { 109 outStartRect.offsetTo(0, outStartRect.top); 110 } else if (outStartRect.left < -deviceProfile.widthPx) { 111 outStartRect.offsetTo(0, outStartRect.top); 112 } 113 } 114 115 @Override setPrimary(T target, Int2DAction<T> action, int param)116 public <T> void setPrimary(T target, Int2DAction<T> action, int param) { 117 action.call(target, param, 0); 118 } 119 120 @Override setPrimary(T target, Float2DAction<T> action, float param)121 public <T> void setPrimary(T target, Float2DAction<T> action, float param) { 122 action.call(target, param, 0); 123 } 124 125 @Override setSecondary(T target, Float2DAction<T> action, float param)126 public <T> void setSecondary(T target, Float2DAction<T> action, float param) { 127 action.call(target, 0, param); 128 } 129 130 @Override set(T target, Int2DAction<T> action, int primaryParam, int secondaryParam)131 public <T> void set(T target, Int2DAction<T> action, int primaryParam, 132 int secondaryParam) { 133 action.call(target, primaryParam, secondaryParam); 134 } 135 136 @Override getPrimaryDirection(MotionEvent event, int pointerIndex)137 public float getPrimaryDirection(MotionEvent event, int pointerIndex) { 138 return event.getX(pointerIndex); 139 } 140 141 @Override getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId)142 public float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId) { 143 return velocityTracker.getXVelocity(pointerId); 144 } 145 146 @Override getMeasuredSize(View view)147 public int getMeasuredSize(View view) { 148 return view.getMeasuredWidth(); 149 } 150 151 @Override getPrimarySize(View view)152 public int getPrimarySize(View view) { 153 return view.getWidth(); 154 } 155 156 @Override getPrimarySize(RectF rect)157 public float getPrimarySize(RectF rect) { 158 return rect.width(); 159 } 160 161 @Override getStart(RectF rect)162 public float getStart(RectF rect) { 163 return rect.left; 164 } 165 166 @Override getEnd(RectF rect)167 public float getEnd(RectF rect) { 168 return rect.right; 169 } 170 171 @Override getClearAllSidePadding(View view, boolean isRtl)172 public int getClearAllSidePadding(View view, boolean isRtl) { 173 return (isRtl ? view.getPaddingRight() : - view.getPaddingLeft()) / 2; 174 } 175 176 @Override getSecondaryDimension(View view)177 public int getSecondaryDimension(View view) { 178 return view.getHeight(); 179 } 180 181 @Override getPrimaryViewTranslate()182 public FloatProperty<View> getPrimaryViewTranslate() { 183 return VIEW_TRANSLATE_X; 184 } 185 186 @Override getSecondaryViewTranslate()187 public FloatProperty<View> getSecondaryViewTranslate() { 188 return VIEW_TRANSLATE_Y; 189 } 190 191 @Override getPrimaryScroll(View view)192 public int getPrimaryScroll(View view) { 193 return view.getScrollX(); 194 } 195 196 @Override getPrimaryScale(View view)197 public float getPrimaryScale(View view) { 198 return view.getScaleX(); 199 } 200 201 @Override setMaxScroll(AccessibilityEvent event, int maxScroll)202 public void setMaxScroll(AccessibilityEvent event, int maxScroll) { 203 event.setMaxScrollX(maxScroll); 204 } 205 206 @Override getRecentsRtlSetting(Resources resources)207 public boolean getRecentsRtlSetting(Resources resources) { 208 return !Utilities.isRtl(resources); 209 } 210 211 @Override getDegreesRotated()212 public float getDegreesRotated() { 213 return 0; 214 } 215 216 @Override getRotation()217 public int getRotation() { 218 return Surface.ROTATION_0; 219 } 220 221 @Override setPrimaryScale(View view, float scale)222 public void setPrimaryScale(View view, float scale) { 223 view.setScaleX(scale); 224 } 225 226 @Override setSecondaryScale(View view, float scale)227 public void setSecondaryScale(View view, float scale) { 228 view.setScaleY(scale); 229 } 230 231 @Override getChildStart(View view)232 public int getChildStart(View view) { 233 return view.getLeft(); 234 } 235 236 @Override getCenterForPage(View view, Rect insets)237 public int getCenterForPage(View view, Rect insets) { 238 return (view.getPaddingTop() + view.getMeasuredHeight() + insets.top 239 - insets.bottom - view.getPaddingBottom()) / 2; 240 } 241 242 @Override getScrollOffsetStart(View view, Rect insets)243 public int getScrollOffsetStart(View view, Rect insets) { 244 return insets.left + view.getPaddingLeft(); 245 } 246 247 @Override getScrollOffsetEnd(View view, Rect insets)248 public int getScrollOffsetEnd(View view, Rect insets) { 249 return view.getWidth() - view.getPaddingRight() - insets.right; 250 } 251 getSecondaryTranslationDirectionFactor()252 public int getSecondaryTranslationDirectionFactor() { 253 return -1; 254 } 255 256 @Override getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile)257 public int getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile) { 258 if (deviceProfile.isLandscape && stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) { 259 return -1; 260 } else { 261 return 1; 262 } 263 } 264 265 @Override getTaskMenuX(float x, View thumbnailView, DeviceProfile deviceProfile, float taskInsetMargin)266 public float getTaskMenuX(float x, View thumbnailView, 267 DeviceProfile deviceProfile, float taskInsetMargin) { 268 if (deviceProfile.isLandscape) { 269 return x + taskInsetMargin 270 + (thumbnailView.getMeasuredWidth() - thumbnailView.getMeasuredHeight()) / 2f; 271 } else { 272 return x + taskInsetMargin; 273 } 274 } 275 276 @Override getTaskMenuY(float y, View thumbnailView, int stagePosition, View taskMenuView, float taskInsetMargin)277 public float getTaskMenuY(float y, View thumbnailView, int stagePosition, 278 View taskMenuView, float taskInsetMargin) { 279 return y + taskInsetMargin; 280 } 281 282 @Override getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile, @StagePosition int stagePosition)283 public int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile, 284 @StagePosition int stagePosition) { 285 return deviceProfile.isLandscape && !deviceProfile.isTablet 286 ? thumbnailView.getMeasuredHeight() 287 : thumbnailView.getMeasuredWidth(); 288 } 289 290 @Override setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile, LinearLayout taskMenuLayout, int dividerSpacing, ShapeDrawable dividerDrawable)291 public void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile, 292 LinearLayout taskMenuLayout, int dividerSpacing, 293 ShapeDrawable dividerDrawable) { 294 taskMenuLayout.setOrientation(LinearLayout.VERTICAL); 295 dividerDrawable.setIntrinsicHeight(dividerSpacing); 296 taskMenuLayout.setDividerDrawable(dividerDrawable); 297 } 298 299 @Override setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp, LinearLayout viewGroup, DeviceProfile deviceProfile)300 public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp, 301 LinearLayout viewGroup, DeviceProfile deviceProfile) { 302 viewGroup.setOrientation(LinearLayout.HORIZONTAL); 303 lp.width = LinearLayout.LayoutParams.MATCH_PARENT; 304 lp.height = WRAP_CONTENT; 305 } 306 307 @Override getDwbLayoutTranslations(int taskViewWidth, int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile, View[] thumbnailViews, int desiredTaskId, View banner)308 public Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth, 309 int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile, 310 View[] thumbnailViews, int desiredTaskId, View banner) { 311 float translationX = 0; 312 float translationY = 0; 313 FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams(); 314 banner.setPivotX(0); 315 banner.setPivotY(0); 316 banner.setRotation(getDegreesRotated()); 317 if (splitBounds == null) { 318 // Single, fullscreen case 319 bannerParams.width = MATCH_PARENT; 320 bannerParams.gravity = BOTTOM | CENTER_HORIZONTAL; 321 return new Pair<>(translationX, translationY); 322 } 323 324 bannerParams.gravity = BOTTOM | ((deviceProfile.isLandscape) ? START : CENTER_HORIZONTAL); 325 326 // Set correct width 327 if (desiredTaskId == splitBounds.leftTopTaskId) { 328 bannerParams.width = thumbnailViews[0].getMeasuredWidth(); 329 } else { 330 bannerParams.width = thumbnailViews[1].getMeasuredWidth(); 331 } 332 333 // Set translations 334 if (deviceProfile.isLandscape) { 335 if (desiredTaskId == splitBounds.rightBottomTaskId) { 336 float leftTopTaskPercent = splitBounds.appsStackedVertically 337 ? splitBounds.topTaskPercent 338 : splitBounds.leftTaskPercent; 339 float dividerThicknessPercent = splitBounds.appsStackedVertically 340 ? splitBounds.dividerHeightPercent 341 : splitBounds.dividerWidthPercent; 342 translationX = ((taskViewWidth * leftTopTaskPercent) 343 + (taskViewWidth * dividerThicknessPercent)); 344 } 345 } else { 346 if (desiredTaskId == splitBounds.leftTopTaskId) { 347 FrameLayout.LayoutParams snapshotParams = 348 (FrameLayout.LayoutParams) thumbnailViews[0] 349 .getLayoutParams(); 350 float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically 351 ? (1f - splitBounds.topTaskPercent) 352 : (1f - splitBounds.leftTaskPercent); 353 translationY = -((taskViewHeight - snapshotParams.topMargin) 354 * bottomRightTaskPlusDividerPercent); 355 } 356 } 357 return new Pair<>(translationX, translationY); 358 } 359 360 /* ---------- The following are only used by TaskViewTouchHandler. ---------- */ 361 362 @Override getUpDownSwipeDirection()363 public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() { 364 return VERTICAL; 365 } 366 367 @Override getUpDirection(boolean isRtl)368 public int getUpDirection(boolean isRtl) { 369 // Ignore rtl since it only affects X value displacement, Y displacement doesn't change 370 return SingleAxisSwipeDetector.DIRECTION_POSITIVE; 371 } 372 373 @Override isGoingUp(float displacement, boolean isRtl)374 public boolean isGoingUp(float displacement, boolean isRtl) { 375 // Ignore rtl since it only affects X value displacement, Y displacement doesn't change 376 return displacement < 0; 377 } 378 379 @Override getTaskDragDisplacementFactor(boolean isRtl)380 public int getTaskDragDisplacementFactor(boolean isRtl) { 381 // Ignore rtl since it only affects X value displacement, Y displacement doesn't change 382 return 1; 383 } 384 385 /* -------------------- */ 386 387 @Override getChildBounds(View child, int childStart, int pageCenter, boolean layoutChild)388 public ChildBounds getChildBounds(View child, int childStart, int pageCenter, 389 boolean layoutChild) { 390 final int childWidth = child.getMeasuredWidth(); 391 final int childRight = childStart + childWidth; 392 final int childHeight = child.getMeasuredHeight(); 393 final int childTop = pageCenter - childHeight / 2; 394 if (layoutChild) { 395 child.layout(childStart, childTop, childRight, childTop + childHeight); 396 } 397 return new ChildBounds(childWidth, childHeight, childRight, childTop); 398 } 399 400 @Override getDistanceToBottomOfRect(DeviceProfile dp, Rect rect)401 public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) { 402 return dp.heightPx - rect.bottom; 403 } 404 405 @Override getSplitPositionOptions(DeviceProfile dp)406 public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) { 407 if (dp.isTablet) { 408 return Utilities.getSplitPositionOptions(dp); 409 } 410 411 List<SplitPositionOption> options = new ArrayList<>(); 412 if (dp.isSeascape()) { 413 options.add(new SplitPositionOption( 414 R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen, 415 STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN)); 416 } else if (dp.isLandscape) { 417 options.add(new SplitPositionOption( 418 R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen, 419 STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN)); 420 } else { 421 // Only add top option 422 options.add(new SplitPositionOption( 423 R.drawable.ic_split_vertical, R.string.recent_task_option_split_screen, 424 STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN)); 425 } 426 return options; 427 } 428 429 @Override getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset, DeviceProfile dp, @StagePosition int stagePosition, Rect out)430 public void getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset, 431 DeviceProfile dp, @StagePosition int stagePosition, Rect out) { 432 int screenWidth = dp.widthPx; 433 int screenHeight = dp.heightPx; 434 boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; 435 int insetSizeAdjustment = getPlaceholderSizeAdjustment(dp, pinToRight); 436 437 out.set(0, 0, screenWidth, placeholderHeight + insetSizeAdjustment); 438 if (!dp.isLandscape) { 439 // portrait, phone or tablet - spans width of screen, nothing else to do 440 out.inset(placeholderInset, 0); 441 442 // Adjust the top to account for content off screen. This will help to animate the view 443 // in with rounded corners. 444 int totalHeight = (int) (1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset) 445 / screenWidth); 446 out.top -= (totalHeight - placeholderHeight); 447 return; 448 } 449 450 // Now we rotate the portrait rect depending on what side we want pinned 451 452 float postRotateScale = (float) screenHeight / screenWidth; 453 mTmpMatrix.reset(); 454 mTmpMatrix.postRotate(pinToRight ? 90 : 270); 455 mTmpMatrix.postTranslate(pinToRight ? screenWidth : 0, pinToRight ? 0 : screenWidth); 456 // The placeholder height stays constant after rotation, so we don't change width scale 457 mTmpMatrix.postScale(1, postRotateScale); 458 459 mTmpRectF.set(out); 460 mTmpMatrix.mapRect(mTmpRectF); 461 mTmpRectF.inset(0, placeholderInset); 462 mTmpRectF.roundOut(out); 463 464 // Adjust the top to account for content off screen. This will help to animate the view in 465 // with rounded corners. 466 int totalWidth = (int) (1.0f * screenWidth / 2 * (screenHeight - 2 * placeholderInset) 467 / screenHeight); 468 int width = out.width(); 469 if (pinToRight) { 470 out.right += totalWidth - width; 471 } else { 472 out.left -= totalWidth - width; 473 } 474 } 475 476 @Override updateSplitIconParams(View out, float onScreenRectCenterX, float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY, int drawableWidth, int drawableHeight, DeviceProfile dp, @StagePosition int stagePosition)477 public void updateSplitIconParams(View out, float onScreenRectCenterX, 478 float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY, 479 int drawableWidth, int drawableHeight, DeviceProfile dp, 480 @StagePosition int stagePosition) { 481 boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; 482 float insetAdjustment = getPlaceholderSizeAdjustment(dp, pinToRight) / 2f; 483 if (!dp.isLandscape) { 484 out.setX(onScreenRectCenterX / fullscreenScaleX 485 - 1.0f * drawableWidth / 2); 486 out.setY((onScreenRectCenterY + insetAdjustment) / fullscreenScaleY 487 - 1.0f * drawableHeight / 2); 488 } else { 489 if (pinToRight) { 490 out.setX((onScreenRectCenterX - insetAdjustment) / fullscreenScaleX 491 - 1.0f * drawableWidth / 2); 492 } else { 493 out.setX((onScreenRectCenterX + insetAdjustment) / fullscreenScaleX 494 - 1.0f * drawableWidth / 2); 495 } 496 out.setY(onScreenRectCenterY / fullscreenScaleY 497 - 1.0f * drawableHeight / 2); 498 } 499 } 500 501 /** 502 * The split placeholder comes with a default inset to buffer the icon from the top of the 503 * screen. But if the device already has a large inset (from cutouts etc), use that instead. 504 */ getPlaceholderSizeAdjustment(DeviceProfile dp, boolean pinToRight)505 private int getPlaceholderSizeAdjustment(DeviceProfile dp, boolean pinToRight) { 506 int insetThickness; 507 if (!dp.isLandscape) { 508 insetThickness = dp.getInsets().top; 509 } else { 510 insetThickness = pinToRight ? dp.getInsets().right : dp.getInsets().left; 511 } 512 return Math.max(insetThickness - dp.splitPlaceholderInset, 0); 513 } 514 515 @Override setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight, int splitInstructionsWidth)516 public void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight, 517 int splitInstructionsWidth) { 518 out.setPivotX(0); 519 out.setPivotY(splitInstructionsHeight); 520 out.setRotation(getDegreesRotated()); 521 int distanceToEdge; 522 if (dp.isPhone) { 523 if (dp.isLandscape) { 524 distanceToEdge = out.getResources().getDimensionPixelSize( 525 R.dimen.split_instructions_bottom_margin_phone_landscape); 526 } else { 527 distanceToEdge = out.getResources().getDimensionPixelSize( 528 R.dimen.split_instructions_bottom_margin_phone_portrait); 529 } 530 } else { 531 distanceToEdge = dp.getOverviewActionsClaimedSpaceBelow(); 532 } 533 534 // Center the view in case of unbalanced insets on left or right of screen 535 int insetCorrectionX = (dp.getInsets().right - dp.getInsets().left) / 2; 536 // Adjust for any insets on the bottom edge 537 int insetCorrectionY = dp.getInsets().bottom; 538 out.setTranslationX(insetCorrectionX); 539 out.setTranslationY(-distanceToEdge + insetCorrectionY); 540 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) out.getLayoutParams(); 541 lp.gravity = CENTER_HORIZONTAL | BOTTOM; 542 out.setLayoutParams(lp); 543 } 544 545 @Override getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp, @StagePosition int stagePosition, Rect out1, Rect out2)546 public void getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp, 547 @StagePosition int stagePosition, Rect out1, Rect out2) { 548 int screenHeight = dp.heightPx; 549 int screenWidth = dp.widthPx; 550 out1.set(0, 0, screenWidth, screenHeight / 2 - splitDividerSize); 551 out2.set(0, screenHeight / 2 + splitDividerSize, screenWidth, screenHeight); 552 if (!dp.isLandscape) { 553 // Portrait - the window bounds are always top and bottom half 554 return; 555 } 556 557 // Now we rotate the portrait rect depending on what side we want pinned 558 boolean pinToRight = stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT; 559 float postRotateScale = (float) screenHeight / screenWidth; 560 561 mTmpMatrix.reset(); 562 mTmpMatrix.postRotate(pinToRight ? 90 : 270); 563 mTmpMatrix.postTranslate(pinToRight ? screenHeight : 0, pinToRight ? 0 : screenWidth); 564 mTmpMatrix.postScale(1 / postRotateScale, postRotateScale); 565 566 mTmpRectF.set(out1); 567 mTmpMatrix.mapRect(mTmpRectF); 568 mTmpRectF.roundOut(out1); 569 570 mTmpRectF.set(out2); 571 mTmpMatrix.mapRect(mTmpRectF); 572 mTmpRectF.roundOut(out2); 573 } 574 575 @Override setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, SplitBounds splitInfo, int desiredStagePosition)576 public void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, 577 SplitBounds splitInfo, int desiredStagePosition) { 578 float topLeftTaskPercent = splitInfo.appsStackedVertically 579 ? splitInfo.topTaskPercent 580 : splitInfo.leftTaskPercent; 581 float dividerBarPercent = splitInfo.appsStackedVertically 582 ? splitInfo.dividerHeightPercent 583 : splitInfo.dividerWidthPercent; 584 585 float scale = (float) outRect.height() / dp.availableHeightPx; 586 float topTaskHeight = dp.availableHeightPx * topLeftTaskPercent; 587 float scaledTopTaskHeight = topTaskHeight * scale; 588 float dividerHeight = dp.availableHeightPx * dividerBarPercent; 589 float scaledDividerHeight = dividerHeight * scale; 590 591 if (desiredStagePosition == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) { 592 if (splitInfo.appsStackedVertically) { 593 outRect.bottom = Math.round(outRect.top + scaledTopTaskHeight); 594 } else { 595 outRect.right = outRect.left + Math.round(outRect.width() * topLeftTaskPercent); 596 } 597 } else { 598 if (splitInfo.appsStackedVertically) { 599 outRect.top += Math.round(scaledTopTaskHeight + scaledDividerHeight); 600 } else { 601 outRect.left += Math.round(outRect.width() 602 * (topLeftTaskPercent + dividerBarPercent)); 603 } 604 } 605 } 606 607 @Override measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot, int parentWidth, int parentHeight, SplitBounds splitBoundsConfig, DeviceProfile dp, boolean isRtl)608 public void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot, 609 int parentWidth, int parentHeight, SplitBounds splitBoundsConfig, 610 DeviceProfile dp, boolean isRtl) { 611 int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx; 612 int totalThumbnailHeight = parentHeight - spaceAboveSnapshot; 613 int dividerBar = Math.round(splitBoundsConfig.appsStackedVertically 614 ? splitBoundsConfig.dividerHeightPercent * dp.availableHeightPx 615 : splitBoundsConfig.dividerWidthPercent * parentWidth); 616 int primarySnapshotHeight; 617 int primarySnapshotWidth; 618 int secondarySnapshotHeight; 619 int secondarySnapshotWidth; 620 float taskPercent = splitBoundsConfig.appsStackedVertically ? 621 splitBoundsConfig.topTaskPercent : splitBoundsConfig.leftTaskPercent; 622 if (dp.isLandscape) { 623 primarySnapshotHeight = totalThumbnailHeight; 624 primarySnapshotWidth = Math.round(parentWidth * taskPercent); 625 626 secondarySnapshotHeight = totalThumbnailHeight; 627 secondarySnapshotWidth = parentWidth - primarySnapshotWidth - dividerBar; 628 int translationX = primarySnapshotWidth + dividerBar; 629 if (isRtl) { 630 primarySnapshot.setTranslationX(-translationX); 631 secondarySnapshot.setTranslationX(0); 632 } else { 633 secondarySnapshot.setTranslationX(translationX); 634 primarySnapshot.setTranslationX(0); 635 } 636 secondarySnapshot.setTranslationY(spaceAboveSnapshot); 637 638 // Reset unused translations 639 primarySnapshot.setTranslationY(0); 640 } else { 641 float scale = (float) totalThumbnailHeight / dp.availableHeightPx; 642 float topTaskHeight = dp.availableHeightPx * taskPercent; 643 float finalDividerHeight = dividerBar * scale; 644 float scaledTopTaskHeight = topTaskHeight * scale; 645 primarySnapshotWidth = parentWidth; 646 primarySnapshotHeight = Math.round(scaledTopTaskHeight); 647 648 secondarySnapshotWidth = parentWidth; 649 secondarySnapshotHeight = Math.round(totalThumbnailHeight - primarySnapshotHeight 650 - finalDividerHeight); 651 float translationY = primarySnapshotHeight + spaceAboveSnapshot + finalDividerHeight; 652 secondarySnapshot.setTranslationY(translationY); 653 654 FrameLayout.LayoutParams primaryParams = 655 (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams(); 656 FrameLayout.LayoutParams secondaryParams = 657 (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams(); 658 secondaryParams.topMargin = 0; 659 primaryParams.topMargin = spaceAboveSnapshot; 660 661 // Reset unused translations 662 primarySnapshot.setTranslationY(0); 663 secondarySnapshot.setTranslationX(0); 664 primarySnapshot.setTranslationX(0); 665 } 666 primarySnapshot.measure( 667 View.MeasureSpec.makeMeasureSpec(primarySnapshotWidth, View.MeasureSpec.EXACTLY), 668 View.MeasureSpec.makeMeasureSpec(primarySnapshotHeight, View.MeasureSpec.EXACTLY)); 669 secondarySnapshot.measure( 670 View.MeasureSpec.makeMeasureSpec(secondarySnapshotWidth, View.MeasureSpec.EXACTLY), 671 View.MeasureSpec.makeMeasureSpec(secondarySnapshotHeight, 672 View.MeasureSpec.EXACTLY)); 673 primarySnapshot.setScaleX(1); 674 secondarySnapshot.setScaleX(1); 675 primarySnapshot.setScaleY(1); 676 secondarySnapshot.setScaleY(1); 677 } 678 679 @Override setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl)680 public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin, 681 int taskIconHeight, int thumbnailTopMargin, boolean isRtl) { 682 iconParams.gravity = TOP | CENTER_HORIZONTAL; 683 // Reset margins, since they may have been set on rotation 684 iconParams.leftMargin = iconParams.rightMargin = 0; 685 iconParams.topMargin = iconParams.bottomMargin = 0; 686 } 687 688 @Override setSplitIconParams(View primaryIconView, View secondaryIconView, int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight, int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl, DeviceProfile deviceProfile, SplitBounds splitConfig)689 public void setSplitIconParams(View primaryIconView, View secondaryIconView, 690 int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight, 691 int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl, 692 DeviceProfile deviceProfile, SplitBounds splitConfig) { 693 FrameLayout.LayoutParams primaryIconParams = 694 (FrameLayout.LayoutParams) primaryIconView.getLayoutParams(); 695 FrameLayout.LayoutParams secondaryIconParams = 696 new FrameLayout.LayoutParams(primaryIconParams); 697 698 if (deviceProfile.isLandscape) { 699 // We calculate the "midpoint" of the thumbnail area, and place the icons there. 700 // This is the place where the thumbnail area splits by default, in a near-50/50 split. 701 // It is usually not exactly 50/50, due to insets/screen cutouts. 702 int fullscreenInsetThickness = deviceProfile.isSeascape() 703 ? deviceProfile.getInsets().right 704 : deviceProfile.getInsets().left; 705 int fullscreenMidpointFromBottom = ((deviceProfile.widthPx 706 - fullscreenInsetThickness) / 2); 707 float midpointFromEndPct = (float) fullscreenMidpointFromBottom 708 / deviceProfile.widthPx; 709 float insetPct = (float) fullscreenInsetThickness / deviceProfile.widthPx; 710 int spaceAboveSnapshots = 0; 711 int overviewThumbnailAreaThickness = groupedTaskViewWidth - spaceAboveSnapshots; 712 int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness 713 * midpointFromEndPct); 714 int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct); 715 716 if (deviceProfile.isSeascape()) { 717 primaryIconParams.gravity = TOP | (isRtl ? END : START); 718 secondaryIconParams.gravity = TOP | (isRtl ? END : START); 719 if (splitConfig.initiatedFromSeascape) { 720 // if the split was initiated from seascape, 721 // the task on the right (secondary) is slightly larger 722 primaryIconView.setTranslationX(bottomToMidpointOffset - taskIconHeight); 723 secondaryIconView.setTranslationX(bottomToMidpointOffset); 724 } else { 725 // if not, 726 // the task on the left (primary) is slightly larger 727 primaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset 728 - taskIconHeight); 729 secondaryIconView.setTranslationX(bottomToMidpointOffset + insetOffset); 730 } 731 } else { 732 primaryIconParams.gravity = TOP | (isRtl ? START : END); 733 secondaryIconParams.gravity = TOP | (isRtl ? START : END); 734 if (!splitConfig.initiatedFromSeascape) { 735 // if the split was initiated from landscape, 736 // the task on the left (primary) is slightly larger 737 primaryIconView.setTranslationX(-bottomToMidpointOffset); 738 secondaryIconView.setTranslationX(-bottomToMidpointOffset + taskIconHeight); 739 } else { 740 // if not, 741 // the task on the right (secondary) is slightly larger 742 primaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset); 743 secondaryIconView.setTranslationX(-bottomToMidpointOffset - insetOffset 744 + taskIconHeight); 745 } 746 } 747 } else { 748 primaryIconParams.gravity = TOP | CENTER_HORIZONTAL; 749 // shifts icon half a width left (height is used here since icons are square) 750 primaryIconView.setTranslationX(-(taskIconHeight / 2f)); 751 secondaryIconParams.gravity = TOP | CENTER_HORIZONTAL; 752 secondaryIconView.setTranslationX(taskIconHeight / 2f); 753 } 754 primaryIconView.setTranslationY(0); 755 secondaryIconView.setTranslationY(0); 756 757 primaryIconView.setLayoutParams(primaryIconParams); 758 secondaryIconView.setLayoutParams(secondaryIconParams); 759 } 760 761 @Override getDefaultSplitPosition(DeviceProfile deviceProfile)762 public int getDefaultSplitPosition(DeviceProfile deviceProfile) { 763 if (!deviceProfile.isTablet) { 764 throw new IllegalStateException("Default position available only for large screens"); 765 } 766 if (deviceProfile.isLandscape) { 767 return STAGE_POSITION_BOTTOM_OR_RIGHT; 768 } else { 769 return STAGE_POSITION_TOP_OR_LEFT; 770 } 771 } 772 773 @Override getSplitSelectTaskOffset(FloatProperty primary, FloatProperty secondary, DeviceProfile deviceProfile)774 public Pair<FloatProperty, FloatProperty> getSplitSelectTaskOffset(FloatProperty primary, 775 FloatProperty secondary, DeviceProfile deviceProfile) { 776 if (deviceProfile.isLandscape) { // or seascape 777 return new Pair<>(primary, secondary); 778 } else { 779 return new Pair<>(secondary, primary); 780 } 781 } 782 783 @Override getFloatingTaskOffscreenTranslationTarget(View floatingTask, RectF onScreenRect, @StagePosition int stagePosition, DeviceProfile dp)784 public float getFloatingTaskOffscreenTranslationTarget(View floatingTask, RectF onScreenRect, 785 @StagePosition int stagePosition, DeviceProfile dp) { 786 if (dp.isLandscape) { 787 float currentTranslationX = floatingTask.getTranslationX(); 788 return stagePosition == STAGE_POSITION_TOP_OR_LEFT 789 ? currentTranslationX - onScreenRect.width() 790 : currentTranslationX + onScreenRect.width(); 791 } else { 792 float currentTranslationY = floatingTask.getTranslationY(); 793 return currentTranslationY - onScreenRect.height(); 794 } 795 } 796 797 @Override setFloatingTaskPrimaryTranslation(View floatingTask, float translation, DeviceProfile dp)798 public void setFloatingTaskPrimaryTranslation(View floatingTask, float translation, 799 DeviceProfile dp) { 800 if (dp.isLandscape) { 801 floatingTask.setTranslationX(translation); 802 } else { 803 floatingTask.setTranslationY(translation); 804 } 805 806 } 807 808 @Override getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp)809 public Float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp) { 810 return dp.isLandscape 811 ? floatingTask.getTranslationX() 812 : floatingTask.getTranslationY(); 813 } 814 } 815