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