1 /* 2 * Copyright (C) 2024 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 package com.android.quickstep.orientation 17 18 import android.annotation.SuppressLint 19 import android.content.res.Resources 20 import android.graphics.Point 21 import android.graphics.PointF 22 import android.graphics.Rect 23 import android.util.Pair 24 import android.view.Gravity 25 import android.view.Surface 26 import android.view.View 27 import android.view.View.MeasureSpec 28 import android.widget.FrameLayout 29 import androidx.core.util.component1 30 import androidx.core.util.component2 31 import com.android.launcher3.DeviceProfile 32 import com.android.launcher3.Flags 33 import com.android.launcher3.R 34 import com.android.launcher3.Utilities 35 import com.android.launcher3.logger.LauncherAtom 36 import com.android.launcher3.touch.SingleAxisSwipeDetector 37 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT 38 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT 39 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED 40 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN 41 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds 42 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption 43 import com.android.launcher3.views.BaseDragLayer 44 import com.android.quickstep.views.IconAppChipView 45 46 class SeascapePagedViewHandler : LandscapePagedViewHandler() { rotateInsetsnull47 override fun rotateInsets(insets: Rect, outInsets: Rect) { 48 outInsets.set(insets.top, insets.right, insets.bottom, insets.left) 49 } 50 51 override val secondaryTranslationDirectionFactor: Int = -1 52 getSplitTranslationDirectionFactornull53 override fun getSplitTranslationDirectionFactor( 54 stagePosition: Int, 55 deviceProfile: DeviceProfile 56 ): Int = if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) -1 else 1 57 58 override fun getRecentsRtlSetting(resources: Resources): Boolean = Utilities.isRtl(resources) 59 60 override val degreesRotated: Float = 270f 61 62 override val rotation: Int = Surface.ROTATION_270 63 64 override fun adjustFloatingIconStartVelocity(velocity: PointF) = 65 velocity.set(velocity.y, -velocity.x) 66 67 override fun getTaskMenuX( 68 x: Float, 69 thumbnailView: View, 70 deviceProfile: DeviceProfile, 71 taskInsetMargin: Float, 72 taskViewIcon: View 73 ): Float = x + taskInsetMargin 74 75 override fun getTaskMenuY( 76 y: Float, 77 thumbnailView: View, 78 stagePosition: Int, 79 taskMenuView: View, 80 taskInsetMargin: Float, 81 taskViewIcon: View 82 ): Float { 83 if (Flags.enableOverviewIconMenu()) { 84 return y 85 } 86 val lp = taskMenuView.layoutParams as BaseDragLayer.LayoutParams 87 val taskMenuWidth = lp.width 88 return if (stagePosition == STAGE_POSITION_UNDEFINED) { 89 y + taskInsetMargin + (thumbnailView.measuredHeight + taskMenuWidth) / 2f 90 } else { 91 y + taskMenuWidth + taskInsetMargin 92 } 93 } 94 getTaskMenuHeightnull95 override fun getTaskMenuHeight( 96 taskInsetMargin: Float, 97 deviceProfile: DeviceProfile, 98 taskMenuX: Float, 99 taskMenuY: Float 100 ): Int = (deviceProfile.availableWidthPx - taskInsetMargin - taskMenuX).toInt() 101 102 override fun setSplitTaskSwipeRect( 103 dp: DeviceProfile, 104 outRect: Rect, 105 splitInfo: SplitBounds, 106 desiredStagePosition: Int 107 ) { 108 val topLeftTaskPercent: Float 109 val dividerBarPercent: Float 110 if (splitInfo.appsStackedVertically) { 111 topLeftTaskPercent = splitInfo.topTaskPercent 112 dividerBarPercent = splitInfo.dividerHeightPercent 113 } else { 114 topLeftTaskPercent = splitInfo.leftTaskPercent 115 dividerBarPercent = splitInfo.dividerWidthPercent 116 } 117 118 // In seascape, the primary thumbnail is counterintuitively placed at the physical bottom of 119 // the screen. This is to preserve consistency when the user rotates: From the user's POV, 120 // the primary should always be on the left. 121 if (desiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) { 122 outRect.top += (outRect.height() * (1 - topLeftTaskPercent)).toInt() 123 } else { 124 outRect.bottom -= (outRect.height() * (topLeftTaskPercent + dividerBarPercent)).toInt() 125 } 126 } 127 getDwbLayoutTranslationsnull128 override fun getDwbLayoutTranslations( 129 taskViewWidth: Int, 130 taskViewHeight: Int, 131 splitBounds: SplitBounds?, 132 deviceProfile: DeviceProfile, 133 thumbnailViews: Array<View>, 134 desiredTaskId: Int, 135 banner: View 136 ): Pair<Float, Float> { 137 val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams 138 val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL 139 140 val bannerParams = banner.layoutParams as FrameLayout.LayoutParams 141 bannerParams.gravity = Gravity.BOTTOM or if (isRtl) Gravity.END else Gravity.START 142 banner.pivotX = 0f 143 banner.pivotY = 0f 144 banner.rotation = degreesRotated 145 146 val translationX: Float = (taskViewWidth - banner.height).toFloat() 147 if (splitBounds == null) { 148 // Single, fullscreen case 149 bannerParams.width = taskViewHeight - snapshotParams.topMargin 150 return Pair(translationX, banner.height.toFloat()) 151 } 152 153 // Set correct width and translations 154 val translationY: Float 155 if (desiredTaskId == splitBounds.leftTopTaskId) { 156 bannerParams.width = thumbnailViews[0].measuredHeight 157 val bottomRightTaskPlusDividerPercent = 158 if (splitBounds.appsStackedVertically) { 159 1f - splitBounds.topTaskPercent 160 } else { 161 1f - splitBounds.leftTaskPercent 162 } 163 translationY = 164 banner.height - 165 (taskViewHeight - snapshotParams.topMargin) * bottomRightTaskPlusDividerPercent 166 } else { 167 bannerParams.width = thumbnailViews[1].measuredHeight 168 translationY = banner.height.toFloat() 169 } 170 171 return Pair(translationX, translationY) 172 } 173 getDistanceToBottomOfRectnull174 override fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int = 175 dp.widthPx - rect.right 176 177 override fun getSplitPositionOptions(dp: DeviceProfile): List<SplitPositionOption> = 178 // Add "right" option which is actually the top 179 listOf( 180 SplitPositionOption( 181 R.drawable.ic_split_horizontal, 182 R.string.recent_task_option_split_screen, 183 STAGE_POSITION_BOTTOM_OR_RIGHT, 184 STAGE_TYPE_MAIN 185 ) 186 ) 187 188 override fun setSplitInstructionsParams( 189 out: View, 190 dp: DeviceProfile, 191 splitInstructionsHeight: Int, 192 splitInstructionsWidth: Int 193 ) { 194 out.pivotX = 0f 195 out.pivotY = splitInstructionsHeight.toFloat() 196 out.rotation = degreesRotated 197 val distanceToEdge = 198 out.resources.getDimensionPixelSize( 199 R.dimen.split_instructions_bottom_margin_phone_landscape 200 ) 201 // Adjust for any insets on the right edge 202 val insetCorrectionX = dp.insets.right 203 // Center the view in case of unbalanced insets on top or bottom of screen 204 val insetCorrectionY = (dp.insets.bottom - dp.insets.top) / 2 205 out.translationX = (splitInstructionsWidth - distanceToEdge + insetCorrectionX).toFloat() 206 out.translationY = 207 (-splitInstructionsHeight + splitInstructionsWidth) / 2f + insetCorrectionY 208 // Setting gravity to RIGHT instead of the lint-recommended END because we always want this 209 // view to be screen-right when phone is in seascape, regardless of the RtL setting. 210 val lp = out.layoutParams as FrameLayout.LayoutParams 211 lp.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL 212 out.layoutParams = lp 213 } 214 setTaskIconParamsnull215 override fun setTaskIconParams( 216 iconParams: FrameLayout.LayoutParams, 217 taskIconMargin: Int, 218 taskIconHeight: Int, 219 thumbnailTopMargin: Int, 220 isRtl: Boolean 221 ) { 222 iconParams.gravity = 223 if (isRtl) { 224 Gravity.END or Gravity.CENTER_VERTICAL 225 } else { 226 Gravity.START or Gravity.CENTER_VERTICAL 227 } 228 iconParams.setMargins(-taskIconHeight - taskIconMargin / 2, thumbnailTopMargin / 2, 0, 0) 229 } 230 setIconAppChipChildrenParamsnull231 override fun setIconAppChipChildrenParams( 232 iconParams: FrameLayout.LayoutParams, 233 chipChildMarginStart: Int 234 ) { 235 iconParams.setMargins(0, 0, 0, 0) 236 iconParams.marginStart = chipChildMarginStart 237 iconParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL 238 } 239 setIconAppChipMenuParamsnull240 override fun setIconAppChipMenuParams( 241 iconAppChipView: IconAppChipView, 242 iconMenuParams: FrameLayout.LayoutParams, 243 iconMenuMargin: Int, 244 thumbnailTopMargin: Int 245 ) { 246 val isRtl = iconAppChipView.layoutDirection == View.LAYOUT_DIRECTION_RTL 247 val iconCenter = iconAppChipView.getHeight() / 2f 248 249 if (isRtl) { 250 iconMenuParams.gravity = Gravity.TOP or Gravity.END 251 iconMenuParams.topMargin = iconMenuMargin 252 iconMenuParams.marginEnd = thumbnailTopMargin 253 // Use half menu height to place the pivot within the X/Y center of icon in the menu. 254 iconAppChipView.pivotX = iconMenuParams.width / 2f 255 iconAppChipView.pivotY = iconMenuParams.width / 2f 256 } else { 257 iconMenuParams.gravity = Gravity.BOTTOM or Gravity.START 258 iconMenuParams.topMargin = 0 259 iconMenuParams.marginEnd = 0 260 iconAppChipView.pivotX = iconCenter 261 iconAppChipView.pivotY = iconCenter - iconMenuMargin 262 } 263 iconMenuParams.marginStart = 0 264 iconMenuParams.bottomMargin = 0 265 iconAppChipView.setSplitTranslationY(0f) 266 iconAppChipView.setRotation(degreesRotated) 267 } 268 measureGroupedTaskViewThumbnailBoundsnull269 override fun measureGroupedTaskViewThumbnailBounds( 270 primarySnapshot: View, 271 secondarySnapshot: View, 272 parentWidth: Int, 273 parentHeight: Int, 274 splitBoundsConfig: SplitBounds, 275 dp: DeviceProfile, 276 isRtl: Boolean 277 ) { 278 val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams 279 val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams 280 281 // Swap the margins that are set in TaskView#setRecentsOrientedState() 282 secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx 283 primaryParams.topMargin = 0 284 285 // Measure and layout the thumbnails bottom up, since the primary is on the visual left 286 // (portrait bottom) and secondary is on the right (portrait top) 287 val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx 288 val totalThumbnailHeight = parentHeight - spaceAboveSnapshot 289 val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig) 290 291 val (taskViewFirst, taskViewSecond) = 292 getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight) 293 secondarySnapshot.translationY = 0f 294 primarySnapshot.translationY = 295 (taskViewSecond.y + spaceAboveSnapshot + dividerBar).toFloat() 296 primarySnapshot.measure( 297 MeasureSpec.makeMeasureSpec(taskViewFirst.x, MeasureSpec.EXACTLY), 298 MeasureSpec.makeMeasureSpec(taskViewFirst.y, MeasureSpec.EXACTLY) 299 ) 300 secondarySnapshot.measure( 301 MeasureSpec.makeMeasureSpec(taskViewSecond.x, MeasureSpec.EXACTLY), 302 MeasureSpec.makeMeasureSpec(taskViewSecond.y, MeasureSpec.EXACTLY) 303 ) 304 } 305 getGroupedTaskViewSizesnull306 override fun getGroupedTaskViewSizes( 307 dp: DeviceProfile, 308 splitBoundsConfig: SplitBounds, 309 parentWidth: Int, 310 parentHeight: Int 311 ): Pair<Point, Point> { 312 // Measure and layout the thumbnails bottom up, since the primary is on the visual left 313 // (portrait bottom) and secondary is on the right (portrait top) 314 val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx 315 val totalThumbnailHeight = parentHeight - spaceAboveSnapshot 316 val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig) 317 318 val taskPercent = 319 if (splitBoundsConfig.appsStackedVertically) { 320 splitBoundsConfig.topTaskPercent 321 } else { 322 splitBoundsConfig.leftTaskPercent 323 } 324 val firstTaskViewSize = Point(parentWidth, (totalThumbnailHeight * taskPercent).toInt()) 325 val secondTaskViewSize = 326 Point(parentWidth, totalThumbnailHeight - firstTaskViewSize.y - dividerBar) 327 return Pair(firstTaskViewSize, secondTaskViewSize) 328 } 329 330 /* ---------- The following are only used by TaskViewTouchHandler. ---------- */ 331 override val upDownSwipeDirection: SingleAxisSwipeDetector.Direction = 332 SingleAxisSwipeDetector.HORIZONTAL 333 getUpDirectionnull334 override fun getUpDirection(isRtl: Boolean): Int = 335 if (isRtl) SingleAxisSwipeDetector.DIRECTION_POSITIVE 336 else SingleAxisSwipeDetector.DIRECTION_NEGATIVE 337 338 override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean = 339 if (isRtl) displacement > 0 else displacement < 0 340 341 override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1 342 /* -------------------- */ 343 344 override fun getSplitIconsPosition( 345 taskIconHeight: Int, 346 primarySnapshotHeight: Int, 347 totalThumbnailHeight: Int, 348 isRtl: Boolean, 349 overviewTaskMarginPx: Int, 350 dividerSize: Int, 351 ): SplitIconPositions { 352 return if (Flags.enableOverviewIconMenu()) { 353 if (isRtl) { 354 SplitIconPositions( 355 topLeftY = totalThumbnailHeight - primarySnapshotHeight, 356 bottomRightY = 0 357 ) 358 } else { 359 SplitIconPositions( 360 topLeftY = 0, 361 bottomRightY = -(primarySnapshotHeight + dividerSize) 362 ) 363 } 364 } else { 365 // In seascape, the icons are initially placed at the bottom start of the 366 // display (portrait locked). The values defined here are used to translate the icons 367 // from the bottom to the almost-center of the screen using the bottom margin. 368 // The primary snapshot is placed at the bottom, thus we translate the icons using 369 // the size of the primary snapshot minus the icon size for the top-left icon. 370 SplitIconPositions( 371 topLeftY = primarySnapshotHeight - taskIconHeight, 372 bottomRightY = primarySnapshotHeight + dividerSize 373 ) 374 } 375 } 376 377 /** 378 * Updates icon view gravity and translation for split tasks 379 * 380 * @param iconView View to be updated 381 * @param translationY the translationY that should be applied 382 * @param isRtl Whether the layout direction is RTL (or false for LTR). 383 */ 384 @SuppressLint("RtlHardcoded") updateSplitIconsPositionnull385 override fun updateSplitIconsPosition(iconView: View, translationY: Int, isRtl: Boolean) { 386 val layoutParams = iconView.layoutParams as FrameLayout.LayoutParams 387 388 if (Flags.enableOverviewIconMenu()) { 389 val appChipView = iconView as IconAppChipView 390 layoutParams.gravity = 391 if (isRtl) Gravity.TOP or Gravity.END else Gravity.BOTTOM or Gravity.START 392 appChipView.layoutParams = layoutParams 393 appChipView.setSplitTranslationX(0f) 394 appChipView.setSplitTranslationY(translationY.toFloat()) 395 } else { 396 layoutParams.gravity = Gravity.BOTTOM or Gravity.LEFT 397 iconView.translationX = 0f 398 iconView.translationY = 0f 399 layoutParams.bottomMargin = translationY 400 iconView.layoutParams = layoutParams 401 } 402 } 403 404 @Override getHandlerTypeForLoggingnull405 override fun getHandlerTypeForLogging(): LauncherAtom.TaskSwitcherContainer.OrientationHandler = 406 LauncherAtom.TaskSwitcherContainer.OrientationHandler.SEASCAPE 407 } 408