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_VERTICAL; 21 import static android.view.Gravity.END; 22 import static android.view.Gravity.RIGHT; 23 import static android.view.Gravity.START; 24 25 import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL; 26 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; 27 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED; 28 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN; 29 30 import android.content.res.Resources; 31 import android.graphics.PointF; 32 import android.graphics.Rect; 33 import android.util.Pair; 34 import android.view.Surface; 35 import android.view.View; 36 import android.widget.FrameLayout; 37 38 import com.android.launcher3.DeviceProfile; 39 import com.android.launcher3.R; 40 import com.android.launcher3.Utilities; 41 import com.android.launcher3.util.SplitConfigurationOptions; 42 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; 43 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; 44 import com.android.launcher3.views.BaseDragLayer; 45 46 import java.util.Collections; 47 import java.util.List; 48 49 public class SeascapePagedViewHandler extends LandscapePagedViewHandler { 50 51 @Override getSecondaryTranslationDirectionFactor()52 public int getSecondaryTranslationDirectionFactor() { 53 return -1; 54 } 55 56 @Override getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile)57 public int getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile) { 58 if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) { 59 return -1; 60 } else { 61 return 1; 62 } 63 } 64 65 @Override getRecentsRtlSetting(Resources resources)66 public boolean getRecentsRtlSetting(Resources resources) { 67 return Utilities.isRtl(resources); 68 } 69 70 @Override getDegreesRotated()71 public float getDegreesRotated() { 72 return 270; 73 } 74 75 @Override getRotation()76 public int getRotation() { 77 return Surface.ROTATION_270; 78 } 79 80 @Override adjustFloatingIconStartVelocity(PointF velocity)81 public void adjustFloatingIconStartVelocity(PointF velocity) { 82 float oldX = velocity.x; 83 float oldY = velocity.y; 84 velocity.set(oldY, -oldX); 85 } 86 87 @Override getTaskMenuX(float x, View thumbnailView, DeviceProfile deviceProfile, float taskInsetMargin)88 public float getTaskMenuX(float x, View thumbnailView, 89 DeviceProfile deviceProfile, float taskInsetMargin) { 90 return x + taskInsetMargin; 91 } 92 93 @Override getTaskMenuY(float y, View thumbnailView, int stagePosition, View taskMenuView, float taskInsetMargin)94 public float getTaskMenuY(float y, View thumbnailView, int stagePosition, 95 View taskMenuView, float taskInsetMargin) { 96 BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskMenuView.getLayoutParams(); 97 int taskMenuWidth = lp.width; 98 if (stagePosition == STAGE_POSITION_UNDEFINED) { 99 return y + taskInsetMargin 100 + (thumbnailView.getMeasuredHeight() + taskMenuWidth) / 2f; 101 } else { 102 return y + taskMenuWidth + taskInsetMargin; 103 } 104 } 105 106 @Override setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, SplitBounds splitInfo, int desiredStagePosition)107 public void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, SplitBounds splitInfo, 108 int desiredStagePosition) { 109 float topLeftTaskPercent = splitInfo.appsStackedVertically 110 ? splitInfo.topTaskPercent 111 : splitInfo.leftTaskPercent; 112 float dividerBarPercent = splitInfo.appsStackedVertically 113 ? splitInfo.dividerHeightPercent 114 : splitInfo.dividerWidthPercent; 115 116 // In seascape, the primary thumbnail is counterintuitively placed at the physical bottom of 117 // the screen. This is to preserve consistency when the user rotates: From the user's POV, 118 // the primary should always be on the left. 119 if (desiredStagePosition == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) { 120 outRect.top += (int) (outRect.height() * ((1 - topLeftTaskPercent))); 121 } else { 122 outRect.bottom -= (int) (outRect.height() * (topLeftTaskPercent + dividerBarPercent)); 123 } 124 } 125 126 @Override getDwbLayoutTranslations(int taskViewWidth, int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile, View[] thumbnailViews, int desiredTaskId, View banner)127 public Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth, 128 int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile, 129 View[] thumbnailViews, int desiredTaskId, View banner) { 130 boolean isRtl = banner.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 131 float translationX = 0; 132 float translationY = 0; 133 FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams(); 134 banner.setPivotX(0); 135 banner.setPivotY(0); 136 banner.setRotation(getDegreesRotated()); 137 translationX = taskViewWidth - banner.getHeight(); 138 FrameLayout.LayoutParams snapshotParams = 139 (FrameLayout.LayoutParams) thumbnailViews[0] 140 .getLayoutParams(); 141 bannerParams.gravity = BOTTOM | (isRtl ? END : START); 142 143 if (splitBounds == null) { 144 // Single, fullscreen case 145 bannerParams.width = taskViewHeight - snapshotParams.topMargin; 146 translationY = banner.getHeight(); 147 return new Pair<>(translationX, translationY); 148 } 149 150 // Set correct width 151 if (desiredTaskId == splitBounds.leftTopTaskId) { 152 bannerParams.width = thumbnailViews[0].getMeasuredHeight(); 153 } else { 154 bannerParams.width = thumbnailViews[1].getMeasuredHeight(); 155 } 156 157 // Set translations 158 if (desiredTaskId == splitBounds.rightBottomTaskId) { 159 translationY = banner.getHeight(); 160 } 161 if (desiredTaskId == splitBounds.leftTopTaskId) { 162 float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically 163 ? (1f - splitBounds.topTaskPercent) 164 : (1f - splitBounds.leftTaskPercent); 165 translationY = banner.getHeight() 166 - ((taskViewHeight - snapshotParams.topMargin) 167 * bottomRightTaskPlusDividerPercent); 168 } 169 return new Pair<>(translationX, translationY); 170 } 171 172 @Override getDistanceToBottomOfRect(DeviceProfile dp, Rect rect)173 public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) { 174 return dp.widthPx - rect.right; 175 } 176 177 @Override getSplitPositionOptions(DeviceProfile dp)178 public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) { 179 // Add "right" option which is actually the top 180 return Collections.singletonList(new SplitPositionOption( 181 R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen, 182 STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN)); 183 } 184 185 @Override setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight, int splitInstructionsWidth)186 public void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight, 187 int splitInstructionsWidth) { 188 out.setPivotX(0); 189 out.setPivotY(splitInstructionsHeight); 190 out.setRotation(getDegreesRotated()); 191 int distanceToEdge = out.getResources().getDimensionPixelSize( 192 R.dimen.split_instructions_bottom_margin_phone_landscape); 193 // Adjust for any insets on the right edge 194 int insetCorrectionX = dp.getInsets().right; 195 // Center the view in case of unbalanced insets on top or bottom of screen 196 int insetCorrectionY = (dp.getInsets().bottom - dp.getInsets().top) / 2; 197 out.setTranslationX(splitInstructionsWidth - distanceToEdge + insetCorrectionX); 198 out.setTranslationY(((-splitInstructionsHeight + splitInstructionsWidth) / 2f) 199 + insetCorrectionY); 200 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) out.getLayoutParams(); 201 // Setting gravity to RIGHT instead of the lint-recommended END because we always want this 202 // view to be screen-right when phone is in seascape, regardless of the RtL setting. 203 lp.gravity = RIGHT | CENTER_VERTICAL; 204 out.setLayoutParams(lp); 205 } 206 207 @Override setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl)208 public void setTaskIconParams(FrameLayout.LayoutParams iconParams, 209 int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl) { 210 iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL; 211 iconParams.leftMargin = -taskIconHeight - taskIconMargin / 2; 212 iconParams.rightMargin = 0; 213 iconParams.topMargin = thumbnailTopMargin / 2; 214 iconParams.bottomMargin = 0; 215 } 216 217 @Override setSplitIconParams(View primaryIconView, View secondaryIconView, int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight, int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl, DeviceProfile deviceProfile, SplitBounds splitConfig)218 public void setSplitIconParams(View primaryIconView, View secondaryIconView, 219 int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight, 220 int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl, 221 DeviceProfile deviceProfile, SplitBounds splitConfig) { 222 super.setSplitIconParams(primaryIconView, secondaryIconView, taskIconHeight, 223 primarySnapshotWidth, primarySnapshotHeight, groupedTaskViewHeight, 224 groupedTaskViewWidth, isRtl, deviceProfile, splitConfig); 225 FrameLayout.LayoutParams primaryIconParams = 226 (FrameLayout.LayoutParams) primaryIconView.getLayoutParams(); 227 FrameLayout.LayoutParams secondaryIconParams = 228 (FrameLayout.LayoutParams) secondaryIconView.getLayoutParams(); 229 230 // We calculate the "midpoint" of the thumbnail area, and place the icons there. 231 // This is the place where the thumbnail area splits by default, in a near-50/50 split. 232 // It is usually not exactly 50/50, due to insets/screen cutouts. 233 int fullscreenInsetThickness = deviceProfile.getInsets().top 234 - deviceProfile.getInsets().bottom; 235 int fullscreenMidpointFromBottom = ((deviceProfile.heightPx 236 - fullscreenInsetThickness) / 2); 237 float midpointFromBottomPct = (float) fullscreenMidpointFromBottom / deviceProfile.heightPx; 238 float insetPct = (float) fullscreenInsetThickness / deviceProfile.heightPx; 239 int spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx; 240 int overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots; 241 int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness * midpointFromBottomPct); 242 int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct); 243 244 primaryIconParams.gravity = BOTTOM | (isRtl ? END : START); 245 secondaryIconParams.gravity = BOTTOM | (isRtl ? END : START); 246 primaryIconView.setTranslationX(0); 247 secondaryIconView.setTranslationX(0); 248 if (splitConfig.initiatedFromSeascape) { 249 // if the split was initiated from seascape, 250 // the task on the right (secondary) is slightly larger 251 primaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset 252 + taskIconHeight); 253 secondaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset); 254 } else { 255 // if not, 256 // the task on the left (primary) is slightly larger 257 primaryIconView.setTranslationY(-bottomToMidpointOffset + taskIconHeight); 258 secondaryIconView.setTranslationY(-bottomToMidpointOffset); 259 } 260 261 primaryIconView.setLayoutParams(primaryIconParams); 262 secondaryIconView.setLayoutParams(secondaryIconParams); 263 } 264 265 @Override measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot, int parentWidth, int parentHeight, SplitBounds splitBoundsConfig, DeviceProfile dp, boolean isRtl)266 public void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot, 267 int parentWidth, int parentHeight, SplitBounds splitBoundsConfig, DeviceProfile dp, 268 boolean isRtl) { 269 FrameLayout.LayoutParams primaryParams = 270 (FrameLayout.LayoutParams) primarySnapshot.getLayoutParams(); 271 FrameLayout.LayoutParams secondaryParams = 272 (FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams(); 273 274 // Swap the margins that are set in TaskView#setRecentsOrientedState() 275 secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx; 276 primaryParams.topMargin = 0; 277 278 // Measure and layout the thumbnails bottom up, since the primary is on the visual left 279 // (portrait bottom) and secondary is on the right (portrait top) 280 int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx; 281 int totalThumbnailHeight = parentHeight - spaceAboveSnapshot; 282 int dividerBar = Math.round(totalThumbnailHeight * (splitBoundsConfig.appsStackedVertically 283 ? splitBoundsConfig.dividerHeightPercent 284 : splitBoundsConfig.dividerWidthPercent)); 285 int primarySnapshotHeight; 286 int primarySnapshotWidth; 287 int secondarySnapshotHeight; 288 int secondarySnapshotWidth; 289 290 float taskPercent = splitBoundsConfig.appsStackedVertically ? 291 splitBoundsConfig.topTaskPercent : splitBoundsConfig.leftTaskPercent; 292 primarySnapshotWidth = parentWidth; 293 primarySnapshotHeight = (int) (totalThumbnailHeight * (taskPercent)); 294 295 secondarySnapshotWidth = parentWidth; 296 secondarySnapshotHeight = totalThumbnailHeight - primarySnapshotHeight - dividerBar; 297 secondarySnapshot.setTranslationY(0); 298 primarySnapshot.setTranslationY(secondarySnapshotHeight + spaceAboveSnapshot + dividerBar); 299 primarySnapshot.measure( 300 View.MeasureSpec.makeMeasureSpec(primarySnapshotWidth, View.MeasureSpec.EXACTLY), 301 View.MeasureSpec.makeMeasureSpec(primarySnapshotHeight, View.MeasureSpec.EXACTLY)); 302 secondarySnapshot.measure( 303 View.MeasureSpec.makeMeasureSpec(secondarySnapshotWidth, View.MeasureSpec.EXACTLY), 304 View.MeasureSpec.makeMeasureSpec(secondarySnapshotHeight, 305 View.MeasureSpec.EXACTLY)); 306 } 307 308 /* ---------- The following are only used by TaskViewTouchHandler. ---------- */ 309 310 @Override getUpDownSwipeDirection()311 public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() { 312 return HORIZONTAL; 313 } 314 315 @Override getUpDirection(boolean isRtl)316 public int getUpDirection(boolean isRtl) { 317 return isRtl ? SingleAxisSwipeDetector.DIRECTION_POSITIVE 318 : SingleAxisSwipeDetector.DIRECTION_NEGATIVE; 319 } 320 321 @Override isGoingUp(float displacement, boolean isRtl)322 public boolean isGoingUp(float displacement, boolean isRtl) { 323 return isRtl ? displacement > 0 : displacement < 0; 324 } 325 326 @Override getTaskDragDisplacementFactor(boolean isRtl)327 public int getTaskDragDisplacementFactor(boolean isRtl) { 328 return isRtl ? -1 : 1; 329 } 330 331 /* -------------------- */ 332 } 333