Merge "Converting orientation handler classes to kotlin" into main

This commit is contained in:
Jordan Silva
2024-03-14 10:03:07 +00:00
committed by Android (Google) Code Review
10 changed files with 1476 additions and 1387 deletions
@@ -133,7 +133,7 @@ public final class RecentsViewStateController extends
// Create transition animations to split select
RecentsPagedOrientationHandler orientationHandler =
((RecentsView) mLauncher.getOverviewPanel()).getPagedOrientationHandler();
Pair<FloatProperty, FloatProperty> taskViewsFloat =
Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
orientationHandler.getSplitSelectTaskOffset(
TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
mLauncher.getDeviceProfile());
@@ -49,6 +49,7 @@ import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.quickstep.RecentsActivity;
import com.android.quickstep.views.ClearAllButton;
import com.android.quickstep.views.RecentsView;
/**
* State controller for fallback recents activity
@@ -125,7 +126,7 @@ public class FallbackRecentsStateController implements StateHandler<RecentsState
setter.add(pa.buildAnim());
}
Pair<FloatProperty, FloatProperty> taskViewsFloat =
Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
mRecentsView.getPagedOrientationHandler().getSplitSelectTaskOffset(
TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
mActivity.getDeviceProfile());
@@ -1,704 +0,0 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.orientation;
import static android.view.Gravity.BOTTOM;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.view.Gravity.END;
import static android.view.Gravity.LEFT;
import static android.view.Gravity.START;
import static android.view.Gravity.TOP;
import static android.view.View.LAYOUT_DIRECTION_RTL;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static com.android.launcher3.Flags.enableOverviewIconMenu;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.ShapeDrawable;
import android.util.FloatProperty;
import android.util.Pair;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.VelocityTracker;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.touch.SingleAxisSwipeDetector;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
import com.android.launcher3.views.BaseDragLayer;
import com.android.quickstep.views.IconAppChipView;
import java.util.Collections;
import java.util.List;
public class LandscapePagedViewHandler implements RecentsPagedOrientationHandler {
@Override
public <T> T getPrimaryValue(T x, T y) {
return y;
}
@Override
public <T> T getSecondaryValue(T x, T y) {
return x;
}
@Override
public int getPrimaryValue(int x, int y) {
return y;
}
@Override
public int getSecondaryValue(int x, int y) {
return x;
}
@Override
public float getPrimaryValue(float x, float y) {
return y;
}
@Override
public float getSecondaryValue(float x, float y) {
return x;
}
@Override
public boolean isLayoutNaturalToLauncher() {
return false;
}
@Override
public void adjustFloatingIconStartVelocity(PointF velocity) {
float oldX = velocity.x;
float oldY = velocity.y;
velocity.set(-oldY, oldX);
}
@Override
public void fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile) {
// We don't need to check the "top" value here because the startRect is in the orientation
// of the app, not of the fixed portrait launcher.
if (outStartRect.left > deviceProfile.heightPx) {
outStartRect.offsetTo(0, outStartRect.top);
} else if (outStartRect.left < -deviceProfile.heightPx) {
outStartRect.offsetTo(0, outStartRect.top);
}
}
@Override
public <T> void setPrimary(T target, Int2DAction<T> action, int param) {
action.call(target, 0, param);
}
@Override
public <T> void setPrimary(T target, Float2DAction<T> action, float param) {
action.call(target, 0, param);
}
@Override
public <T> void setSecondary(T target, Float2DAction<T> action, float param) {
action.call(target, param, 0);
}
@Override
public <T> void set(T target, Int2DAction<T> action, int primaryParam,
int secondaryParam) {
action.call(target, secondaryParam, primaryParam);
}
@Override
public float getPrimaryDirection(MotionEvent event, int pointerIndex) {
return event.getY(pointerIndex);
}
@Override
public float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId) {
return velocityTracker.getYVelocity(pointerId);
}
@Override
public int getMeasuredSize(View view) {
return view.getMeasuredHeight();
}
@Override
public int getPrimarySize(View view) {
return view.getHeight();
}
@Override
public float getPrimarySize(RectF rect) {
return rect.height();
}
@Override
public float getStart(RectF rect) {
return rect.top;
}
@Override
public float getEnd(RectF rect) {
return rect.bottom;
}
@Override
public int getClearAllSidePadding(View view, boolean isRtl) {
return (isRtl ? view.getPaddingBottom() : - view.getPaddingTop()) / 2;
}
@Override
public int getSecondaryDimension(View view) {
return view.getWidth();
}
@Override
public FloatProperty<View> getPrimaryViewTranslate() {
return VIEW_TRANSLATE_Y;
}
@Override
public FloatProperty<View> getSecondaryViewTranslate() {
return VIEW_TRANSLATE_X;
}
@Override
public int getPrimaryScroll(View view) {
return view.getScrollY();
}
@Override
public float getPrimaryScale(View view) {
return view.getScaleY();
}
@Override
public void setMaxScroll(AccessibilityEvent event, int maxScroll) {
event.setMaxScrollY(maxScroll);
}
@Override
public boolean getRecentsRtlSetting(Resources resources) {
return !Utilities.isRtl(resources);
}
@Override
public float getDegreesRotated() {
return 90;
}
@Override
public int getRotation() {
return Surface.ROTATION_90;
}
@Override
public void setPrimaryScale(View view, float scale) {
view.setScaleY(scale);
}
@Override
public void setSecondaryScale(View view, float scale) {
view.setScaleX(scale);
}
@Override
public int getChildStart(View view) {
return view.getTop();
}
@Override
public int getCenterForPage(View view, Rect insets) {
return (view.getPaddingLeft() + view.getMeasuredWidth() + insets.left
- insets.right - view.getPaddingRight()) / 2;
}
@Override
public int getScrollOffsetStart(View view, Rect insets) {
return insets.top + view.getPaddingTop();
}
@Override
public int getScrollOffsetEnd(View view, Rect insets) {
return view.getHeight() - view.getPaddingBottom() - insets.bottom;
}
public int getSecondaryTranslationDirectionFactor() {
return 1;
}
@Override
public int getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile) {
if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
return -1;
} else {
return 1;
}
}
@Override
public float getTaskMenuX(float x, View thumbnailView,
DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) {
return thumbnailView.getMeasuredWidth() + x - taskInsetMargin;
}
@Override
public float getTaskMenuY(float y, View thumbnailView, int stagePosition,
View taskMenuView, float taskInsetMargin, View taskViewIcon) {
BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskMenuView.getLayoutParams();
int taskMenuWidth = lp.width;
if (stagePosition == STAGE_POSITION_UNDEFINED) {
return y + taskInsetMargin
+ (thumbnailView.getMeasuredHeight() - taskMenuWidth) / 2f;
} else {
return y + taskInsetMargin;
}
}
@Override
public int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile,
@StagePosition int stagePosition) {
if (enableOverviewIconMenu()) {
return thumbnailView.getResources().getDimensionPixelSize(
R.dimen.task_thumbnail_icon_menu_expanded_width);
}
if (stagePosition == SplitConfigurationOptions.STAGE_POSITION_UNDEFINED) {
return thumbnailView.getMeasuredWidth();
} else {
return thumbnailView.getMeasuredHeight();
}
}
@Override
public int getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile,
float taskMenuX, float taskMenuY) {
return (int) (taskMenuX - taskInsetMargin);
}
@Override
public void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile,
LinearLayout taskMenuLayout, int dividerSpacing,
ShapeDrawable dividerDrawable) {
taskMenuLayout.setOrientation(LinearLayout.VERTICAL);
dividerDrawable.setIntrinsicHeight(dividerSpacing);
taskMenuLayout.setDividerDrawable(dividerDrawable);
}
@Override
public void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp,
LinearLayout viewGroup, DeviceProfile deviceProfile) {
// Phone fake landscape
viewGroup.setOrientation(LinearLayout.HORIZONTAL);
lp.width = MATCH_PARENT;
lp.height = WRAP_CONTENT;
}
@Override
public Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth,
int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile,
View[] thumbnailViews, int desiredTaskId, View banner) {
boolean isRtl = banner.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
float translationX = 0;
float translationY = 0;
FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams();
banner.setPivotX(0);
banner.setPivotY(0);
banner.setRotation(getDegreesRotated());
translationX = banner.getHeight();
FrameLayout.LayoutParams snapshotParams =
(FrameLayout.LayoutParams) thumbnailViews[0]
.getLayoutParams();
bannerParams.gravity = TOP | (isRtl ? END : START);
if (splitBounds == null) {
// Single, fullscreen case
bannerParams.width = taskViewHeight - snapshotParams.topMargin;
return new Pair<>(translationX, Integer.valueOf(snapshotParams.topMargin).floatValue());
}
// Set correct width
if (desiredTaskId == splitBounds.leftTopTaskId) {
bannerParams.width = thumbnailViews[0].getMeasuredHeight();
} else {
bannerParams.width = thumbnailViews[1].getMeasuredHeight();
}
// Set translations
if (desiredTaskId == splitBounds.rightBottomTaskId) {
float topLeftTaskPlusDividerPercent = splitBounds.appsStackedVertically
? (splitBounds.topTaskPercent + splitBounds.dividerHeightPercent)
: (splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent);
translationY = snapshotParams.topMargin
+ ((taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent);
}
if (desiredTaskId == splitBounds.leftTopTaskId) {
translationY = snapshotParams.topMargin;
}
return new Pair<>(translationX, translationY);
}
/* ---------- The following are only used by TaskViewTouchHandler. ---------- */
@Override
public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() {
return HORIZONTAL;
}
@Override
public int getUpDirection(boolean isRtl) {
return isRtl ? SingleAxisSwipeDetector.DIRECTION_NEGATIVE
: SingleAxisSwipeDetector.DIRECTION_POSITIVE;
}
@Override
public boolean isGoingUp(float displacement, boolean isRtl) {
return isRtl ? displacement < 0 : displacement > 0;
}
@Override
public int getTaskDragDisplacementFactor(boolean isRtl) {
return isRtl ? 1 : -1;
}
/* -------------------- */
@Override
public ChildBounds getChildBounds(View child, int childStart, int pageCenter,
boolean layoutChild) {
final int childHeight = child.getMeasuredHeight();
final int childBottom = childStart + childHeight;
final int childWidth = child.getMeasuredWidth();
final int childLeft = pageCenter - childWidth/ 2;
if (layoutChild) {
child.layout(childLeft, childStart, childLeft + childWidth, childBottom);
}
return new ChildBounds(childHeight, childWidth, childBottom, childLeft);
}
@SuppressWarnings("SuspiciousNameCombination")
@Override
public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
return rect.left;
}
@Override
public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
// Add "left" side of phone which is actually the top
return Collections.singletonList(new SplitPositionOption(
R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen,
STAGE_POSITION_TOP_OR_LEFT, STAGE_TYPE_MAIN));
}
@Override
public void getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset,
DeviceProfile dp, @StagePosition int stagePosition, Rect out) {
// In fake land/seascape, the placeholder always needs to go to the "top" of the device,
// which is the same bounds as 0 rotation.
int width = dp.widthPx;
int insetSizeAdjustment = getPlaceholderSizeAdjustment(dp);
out.set(0, 0, width, placeholderHeight + insetSizeAdjustment);
out.inset(placeholderInset, 0);
// Adjust the top to account for content off screen. This will help to animate the view in
// with rounded corners.
int screenWidth = dp.widthPx;
int screenHeight = dp.heightPx;
int totalHeight = (int) (1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset)
/ screenWidth);
out.top -= (totalHeight - placeholderHeight);
}
@Override
public void updateSplitIconParams(View out, float onScreenRectCenterX,
float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY,
int drawableWidth, int drawableHeight, DeviceProfile dp,
@StagePosition int stagePosition) {
float insetAdjustment = getPlaceholderSizeAdjustment(dp) / 2f;
out.setX(onScreenRectCenterX / fullscreenScaleX
- 1.0f * drawableWidth / 2);
out.setY((onScreenRectCenterY + insetAdjustment) / fullscreenScaleY
- 1.0f * drawableHeight / 2);
}
/**
* The split placeholder comes with a default inset to buffer the icon from the top of the
* screen. But if the device already has a large inset (from cutouts etc), use that instead.
*/
private int getPlaceholderSizeAdjustment(DeviceProfile dp) {
return Math.max(dp.getInsets().top - dp.splitPlaceholderInset, 0);
}
@Override
public void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight,
int splitInstructionsWidth) {
out.setPivotX(0);
out.setPivotY(splitInstructionsHeight);
out.setRotation(getDegreesRotated());
int distanceToEdge = out.getResources().getDimensionPixelSize(
R.dimen.split_instructions_bottom_margin_phone_landscape);
// Adjust for any insets on the left edge
int insetCorrectionX = dp.getInsets().left;
// Center the view in case of unbalanced insets on top or bottom of screen
int insetCorrectionY = (dp.getInsets().bottom - dp.getInsets().top) / 2;
out.setTranslationX(distanceToEdge - insetCorrectionX);
out.setTranslationY(((-splitInstructionsHeight - splitInstructionsWidth) / 2f)
+ insetCorrectionY);
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) out.getLayoutParams();
// Setting gravity to LEFT instead of the lint-recommended START because we always want this
// view to be screen-left when phone is in landscape, regardless of the RtL setting.
lp.gravity = LEFT | CENTER_VERTICAL;
out.setLayoutParams(lp);
}
@Override
public void getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp,
@StagePosition int stagePosition, Rect out1, Rect out2) {
// In fake land/seascape, the window bounds are always top and bottom half
int screenHeight = dp.heightPx;
int screenWidth = dp.widthPx;
out1.set(0, 0, screenWidth, screenHeight / 2 - splitDividerSize);
out2.set(0, screenHeight / 2 + splitDividerSize, screenWidth, screenHeight);
}
@Override
public void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect,
SplitBounds splitInfo, int desiredStagePosition) {
float topLeftTaskPercent = splitInfo.appsStackedVertically
? splitInfo.topTaskPercent
: splitInfo.leftTaskPercent;
float dividerBarPercent = splitInfo.appsStackedVertically
? splitInfo.dividerHeightPercent
: splitInfo.dividerWidthPercent;
if (desiredStagePosition == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) {
outRect.bottom = outRect.top + (int) (outRect.height() * topLeftTaskPercent);
} else {
outRect.top += (int) (outRect.height() * (topLeftTaskPercent + dividerBarPercent));
}
}
@Override
public void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot,
int parentWidth, int parentHeight, SplitBounds splitBoundsConfig, DeviceProfile dp,
boolean isRtl) {
FrameLayout.LayoutParams primaryParams =
(FrameLayout.LayoutParams) primarySnapshot.getLayoutParams();
FrameLayout.LayoutParams secondaryParams =
(FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams();
// Swap the margins that are set in TaskView#setRecentsOrientedState()
secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx;
primaryParams.topMargin = 0;
// Measure and layout the thumbnails bottom up, since the primary is on the visual left
// (portrait bottom) and secondary is on the right (portrait top)
int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
int dividerBar = Math.round(totalThumbnailHeight * (splitBoundsConfig.appsStackedVertically
? splitBoundsConfig.dividerHeightPercent
: splitBoundsConfig.dividerWidthPercent));
Pair<Point, Point> taskViewSizes =
getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight);
int translationY = taskViewSizes.first.y + spaceAboveSnapshot + dividerBar;
primarySnapshot.setTranslationY(spaceAboveSnapshot);
secondarySnapshot.setTranslationY(translationY - spaceAboveSnapshot);
primarySnapshot.measure(
View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY)
);
secondarySnapshot.measure(
View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.x, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.y, View.MeasureSpec.EXACTLY)
);
}
@Override
public Pair<Point, Point> getGroupedTaskViewSizes(
DeviceProfile dp,
SplitBounds splitBoundsConfig,
int parentWidth,
int parentHeight) {
int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
int dividerBar = Math.round(totalThumbnailHeight * (splitBoundsConfig.appsStackedVertically
? splitBoundsConfig.dividerHeightPercent
: splitBoundsConfig.dividerWidthPercent));
float taskPercent = splitBoundsConfig.appsStackedVertically
? splitBoundsConfig.topTaskPercent
: splitBoundsConfig.leftTaskPercent;
Point firstTaskViewSize = new Point(
parentWidth,
(int) (totalThumbnailHeight * taskPercent)
);
Point secondTaskViewSize = new Point(
parentWidth,
totalThumbnailHeight - firstTaskViewSize.y - dividerBar
);
return new Pair<>(firstTaskViewSize, secondTaskViewSize);
}
@Override
public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin,
int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL;
iconParams.rightMargin = -taskIconHeight - taskIconMargin / 2;
iconParams.leftMargin = 0;
iconParams.topMargin = thumbnailTopMargin / 2;
iconParams.bottomMargin = 0;
}
@Override
public void setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams,
int chipChildMarginStart) {
iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
iconParams.setMarginStart(chipChildMarginStart);
iconParams.topMargin = 0;
}
@Override
public void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) {
boolean isRtl = iconAppChipView.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
iconMenuParams.gravity = (isRtl ? START : END) | (isRtl ? BOTTOM : TOP);
iconMenuParams.setMarginStart(isRtl ? iconMenuMargin : 0);
iconMenuParams.topMargin = iconMenuMargin;
iconMenuParams.bottomMargin = isRtl ? iconMenuMargin : 0;
iconMenuParams.setMarginEnd(iconMenuMargin);
iconAppChipView.setPivotX(isRtl ? iconMenuParams.width - (iconMenuParams.height / 2f)
: iconMenuParams.width / 2f);
iconAppChipView.setPivotY(
isRtl ? (iconMenuParams.height / 2f) : iconMenuParams.width / 2f);
iconAppChipView.setSplitTranslationY(0);
iconAppChipView.setRotation(getDegreesRotated());
}
@Override
public void setSplitIconParams(View primaryIconView, View secondaryIconView,
int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
DeviceProfile deviceProfile, SplitBounds splitConfig) {
FrameLayout.LayoutParams primaryIconParams =
(FrameLayout.LayoutParams) primaryIconView.getLayoutParams();
FrameLayout.LayoutParams secondaryIconParams = enableOverviewIconMenu()
? (FrameLayout.LayoutParams) secondaryIconView.getLayoutParams()
: new FrameLayout.LayoutParams(primaryIconParams);
// We calculate the "midpoint" of the thumbnail area, and place the icons there.
// This is the place where the thumbnail area splits by default, in a near-50/50 split.
// It is usually not exactly 50/50, due to insets/screen cutouts.
int fullscreenInsetThickness = deviceProfile.getInsets().top
- deviceProfile.getInsets().bottom;
int fullscreenMidpointFromBottom = ((deviceProfile.heightPx - fullscreenInsetThickness)
/ 2);
float midpointFromBottomPct = (float) fullscreenMidpointFromBottom / deviceProfile.heightPx;
float insetPct = (float) fullscreenInsetThickness / deviceProfile.heightPx;
int spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx;
int overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots;
int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness * midpointFromBottomPct);
int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct);
if (enableOverviewIconMenu()) {
primaryIconParams.gravity = isRtl ? BOTTOM | START : TOP | END;
secondaryIconParams.gravity = isRtl ? BOTTOM | START : TOP | END;
} else {
primaryIconParams.gravity = BOTTOM | (isRtl ? START : END);
secondaryIconParams.gravity = BOTTOM | (isRtl ? START : END);
}
primaryIconView.setTranslationX(0);
secondaryIconView.setTranslationX(0);
if (enableOverviewIconMenu()) {
IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
if (primaryIconView.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight);
primaryAppChipView.setSplitTranslationY(0);
} else {
int secondarySnapshotHeight = groupedTaskViewHeight - primarySnapshotHeight;
primaryAppChipView.setSplitTranslationY(secondarySnapshotHeight);
}
} else if (splitConfig.initiatedFromSeascape) {
// if the split was initiated from seascape,
// the task on the right (secondary) is slightly larger
primaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset);
secondaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset
+ taskIconHeight);
} else {
// if not,
// the task on the left (primary) is slightly larger
primaryIconView.setTranslationY(-bottomToMidpointOffset);
secondaryIconView.setTranslationY(-bottomToMidpointOffset + taskIconHeight);
}
primaryIconView.setLayoutParams(primaryIconParams);
secondaryIconView.setLayoutParams(secondaryIconParams);
}
@Override
public int getDefaultSplitPosition(DeviceProfile deviceProfile) {
throw new IllegalStateException("Default position not available in fake landscape");
}
@Override
public Pair<FloatProperty, FloatProperty> getSplitSelectTaskOffset(FloatProperty primary,
FloatProperty secondary, DeviceProfile deviceProfile) {
return new Pair<>(primary, secondary);
}
@Override
public float getFloatingTaskOffscreenTranslationTarget(View floatingTask, RectF onScreenRect,
@StagePosition int stagePosition, DeviceProfile dp) {
float currentTranslationY = floatingTask.getTranslationY();
return currentTranslationY - onScreenRect.height();
}
@Override
public void setFloatingTaskPrimaryTranslation(View floatingTask, float translation,
DeviceProfile dp) {
floatingTask.setTranslationY(translation);
}
@Override
public Float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp) {
return floatingTask.getTranslationY();
}
}
@@ -0,0 +1,659 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.orientation
import android.content.res.Resources
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.ShapeDrawable
import android.util.FloatProperty
import android.util.Pair
import android.view.Gravity
import android.view.MotionEvent
import android.view.Surface
import android.view.VelocityTracker
import android.view.View
import android.view.View.MeasureSpec
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.core.util.component1
import androidx.core.util.component2
import com.android.launcher3.DeviceProfile
import com.android.launcher3.Flags
import com.android.launcher3.LauncherAnimUtils
import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.touch.PagedOrientationHandler.ChildBounds
import com.android.launcher3.touch.PagedOrientationHandler.Float2DAction
import com.android.launcher3.touch.PagedOrientationHandler.Int2DAction
import com.android.launcher3.touch.SingleAxisSwipeDetector
import com.android.launcher3.util.SplitConfigurationOptions.*
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption
import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
import com.android.launcher3.views.BaseDragLayer
import com.android.quickstep.views.IconAppChipView
import kotlin.math.max
open class LandscapePagedViewHandler : RecentsPagedOrientationHandler {
override fun <T> getPrimaryValue(x: T, y: T): T = y
override fun <T> getSecondaryValue(x: T, y: T): T = x
override fun getPrimaryValue(x: Int, y: Int): Int = y
override fun getSecondaryValue(x: Int, y: Int): Int = x
override fun getPrimaryValue(x: Float, y: Float): Float = y
override fun getSecondaryValue(x: Float, y: Float): Float = x
override val isLayoutNaturalToLauncher: Boolean = false
override fun adjustFloatingIconStartVelocity(velocity: PointF) {
val oldX = velocity.x
val oldY = velocity.y
velocity.set(-oldY, oldX)
}
override fun fixBoundsForHomeAnimStartRect(outStartRect: RectF, deviceProfile: DeviceProfile) {
// We don't need to check the "top" value here because the startRect is in the orientation
// of the app, not of the fixed portrait launcher.
if (outStartRect.left > deviceProfile.heightPx) {
outStartRect.offsetTo(0f, outStartRect.top)
} else if (outStartRect.left < -deviceProfile.heightPx) {
outStartRect.offsetTo(0f, outStartRect.top)
}
}
override fun <T> setPrimary(target: T, action: Int2DAction<T>, param: Int) =
action.call(target, 0, param)
override fun <T> setPrimary(target: T, action: Float2DAction<T>, param: Float) =
action.call(target, 0f, param)
override fun <T> setSecondary(target: T, action: Float2DAction<T>, param: Float) =
action.call(target, param, 0f)
override fun <T> set(
target: T,
action: Int2DAction<T>,
primaryParam: Int,
secondaryParam: Int
) = action.call(target, secondaryParam, primaryParam)
override fun getPrimaryDirection(event: MotionEvent, pointerIndex: Int): Float =
event.getY(pointerIndex)
override fun getPrimaryVelocity(velocityTracker: VelocityTracker, pointerId: Int): Float =
velocityTracker.getYVelocity(pointerId)
override fun getMeasuredSize(view: View): Int = view.measuredHeight
override fun getPrimarySize(view: View): Int = view.height
override fun getPrimarySize(rect: RectF): Float = rect.height()
override fun getStart(rect: RectF): Float = rect.top
override fun getEnd(rect: RectF): Float = rect.bottom
override fun getClearAllSidePadding(view: View, isRtl: Boolean): Int =
if (isRtl) view.paddingBottom / 2 else -view.paddingTop / 2
override fun getSecondaryDimension(view: View): Int = view.width
override val primaryViewTranslate: FloatProperty<View> = LauncherAnimUtils.VIEW_TRANSLATE_Y
override val secondaryViewTranslate: FloatProperty<View> = LauncherAnimUtils.VIEW_TRANSLATE_X
override fun getPrimaryScroll(view: View): Int = view.scrollY
override fun getPrimaryScale(view: View): Float = view.scaleY
override fun setMaxScroll(event: AccessibilityEvent, maxScroll: Int) {
event.maxScrollY = maxScroll
}
override fun getRecentsRtlSetting(resources: Resources): Boolean = !Utilities.isRtl(resources)
override val degreesRotated: Float = 90f
override val rotation: Int = Surface.ROTATION_90
override fun setPrimaryScale(view: View, scale: Float) {
view.scaleY = scale
}
override fun setSecondaryScale(view: View, scale: Float) {
view.scaleX = scale
}
override fun getChildStart(view: View): Int = view.top
override fun getCenterForPage(view: View, insets: Rect): Int =
(view.paddingLeft + view.measuredWidth + insets.left - insets.right - view.paddingRight) / 2
override fun getScrollOffsetStart(view: View, insets: Rect): Int = insets.top + view.paddingTop
override fun getScrollOffsetEnd(view: View, insets: Rect): Int =
view.height - view.paddingBottom - insets.bottom
override val secondaryTranslationDirectionFactor: Int = 1
override fun getSplitTranslationDirectionFactor(
stagePosition: Int,
deviceProfile: DeviceProfile
): Int = if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) -1 else 1
override fun getTaskMenuX(
x: Float,
thumbnailView: View,
deviceProfile: DeviceProfile,
taskInsetMargin: Float,
taskViewIcon: View
): Float = thumbnailView.measuredWidth + x - taskInsetMargin
override fun getTaskMenuY(
y: Float,
thumbnailView: View,
stagePosition: Int,
taskMenuView: View,
taskInsetMargin: Float,
taskViewIcon: View
): Float {
val layoutParams = taskMenuView.layoutParams as BaseDragLayer.LayoutParams
var taskMenuY = y + taskInsetMargin
if (stagePosition == STAGE_POSITION_UNDEFINED) {
taskMenuY += (thumbnailView.measuredHeight - layoutParams.width) / 2f
}
return taskMenuY
}
override fun getTaskMenuWidth(
thumbnailView: View,
deviceProfile: DeviceProfile,
@StagePosition stagePosition: Int
): Int =
when {
Flags.enableOverviewIconMenu() ->
thumbnailView.resources.getDimensionPixelSize(
R.dimen.task_thumbnail_icon_menu_expanded_width
)
stagePosition == STAGE_POSITION_UNDEFINED -> thumbnailView.measuredWidth
else -> thumbnailView.measuredHeight
}
override fun getTaskMenuHeight(
taskInsetMargin: Float,
deviceProfile: DeviceProfile,
taskMenuX: Float,
taskMenuY: Float
): Int = (taskMenuX - taskInsetMargin).toInt()
override fun setTaskOptionsMenuLayoutOrientation(
deviceProfile: DeviceProfile,
taskMenuLayout: LinearLayout,
dividerSpacing: Int,
dividerDrawable: ShapeDrawable
) {
taskMenuLayout.orientation = LinearLayout.VERTICAL
dividerDrawable.intrinsicHeight = dividerSpacing
taskMenuLayout.dividerDrawable = dividerDrawable
}
override fun setLayoutParamsForTaskMenuOptionItem(
lp: LinearLayout.LayoutParams,
viewGroup: LinearLayout,
deviceProfile: DeviceProfile
) {
// Phone fake landscape
viewGroup.orientation = LinearLayout.HORIZONTAL
lp.width = ViewGroup.LayoutParams.MATCH_PARENT
lp.height = ViewGroup.LayoutParams.WRAP_CONTENT
}
override fun getDwbLayoutTranslations(
taskViewWidth: Int,
taskViewHeight: Int,
splitBounds: SplitBounds?,
deviceProfile: DeviceProfile,
thumbnailViews: Array<View>,
desiredTaskId: Int,
banner: View
): Pair<Float, Float> {
val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL
val translationX = banner.height.toFloat()
val bannerParams = banner.layoutParams as FrameLayout.LayoutParams
bannerParams.gravity = Gravity.TOP or if (isRtl) Gravity.END else Gravity.START
banner.pivotX = 0f
banner.pivotY = 0f
banner.rotation = degreesRotated
if (splitBounds == null) {
// Single, fullscreen case
bannerParams.width = taskViewHeight - snapshotParams.topMargin
return Pair(translationX, snapshotParams.topMargin.toFloat())
}
// Set correct width and translations
val translationY: Float
if (desiredTaskId == splitBounds.leftTopTaskId) {
bannerParams.width = thumbnailViews[0].measuredHeight
translationY = snapshotParams.topMargin.toFloat()
} else {
bannerParams.width = thumbnailViews[1].measuredHeight
val topLeftTaskPlusDividerPercent =
if (splitBounds.appsStackedVertically) {
splitBounds.topTaskPercent + splitBounds.dividerHeightPercent
} else {
splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent
}
translationY =
snapshotParams.topMargin +
(taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent
}
return Pair(translationX, translationY)
}
/* ---------- The following are only used by TaskViewTouchHandler. ---------- */
override val upDownSwipeDirection: SingleAxisSwipeDetector.Direction =
SingleAxisSwipeDetector.HORIZONTAL
override fun getUpDirection(isRtl: Boolean): Int =
if (isRtl) SingleAxisSwipeDetector.DIRECTION_NEGATIVE
else SingleAxisSwipeDetector.DIRECTION_POSITIVE
override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean =
if (isRtl) displacement < 0 else displacement > 0
override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) 1 else -1
/* -------------------- */
override fun getChildBounds(
child: View,
childStart: Int,
pageCenter: Int,
layoutChild: Boolean
): ChildBounds {
val childHeight = child.measuredHeight
val childWidth = child.measuredWidth
val childBottom = childStart + childHeight
val childLeft = pageCenter - childWidth / 2
if (layoutChild) {
child.layout(childLeft, childStart, childLeft + childWidth, childBottom)
}
return ChildBounds(childHeight, childWidth, childBottom, childLeft)
}
override fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int = rect.left
override fun getSplitPositionOptions(dp: DeviceProfile): List<SplitPositionOption> =
// Add "left" side of phone which is actually the top
listOf(
SplitPositionOption(
R.drawable.ic_split_horizontal,
R.string.recent_task_option_split_screen,
STAGE_POSITION_TOP_OR_LEFT,
STAGE_TYPE_MAIN
)
)
override fun getInitialSplitPlaceholderBounds(
placeholderHeight: Int,
placeholderInset: Int,
dp: DeviceProfile,
@StagePosition stagePosition: Int,
out: Rect
) {
// In fake land/seascape, the placeholder always needs to go to the "top" of the device,
// which is the same bounds as 0 rotation.
val width = dp.widthPx
val insetSizeAdjustment = getPlaceholderSizeAdjustment(dp)
out.set(0, 0, width, placeholderHeight + insetSizeAdjustment)
out.inset(placeholderInset, 0)
// Adjust the top to account for content off screen. This will help to animate the view in
// with rounded corners.
val screenWidth = dp.widthPx
val screenHeight = dp.heightPx
val totalHeight =
(1.0f * screenHeight / 2 * (screenWidth - 2 * placeholderInset) / screenWidth).toInt()
out.top -= totalHeight - placeholderHeight
}
override fun updateSplitIconParams(
out: View,
onScreenRectCenterX: Float,
onScreenRectCenterY: Float,
fullscreenScaleX: Float,
fullscreenScaleY: Float,
drawableWidth: Int,
drawableHeight: Int,
dp: DeviceProfile,
@StagePosition stagePosition: Int
) {
val insetAdjustment = getPlaceholderSizeAdjustment(dp) / 2f
out.x = (onScreenRectCenterX / fullscreenScaleX - 1.0f * drawableWidth / 2)
out.y =
((onScreenRectCenterY + insetAdjustment) / fullscreenScaleY - 1.0f * drawableHeight / 2)
}
/**
* The split placeholder comes with a default inset to buffer the icon from the top of the
* screen. But if the device already has a large inset (from cutouts etc), use that instead.
*/
private fun getPlaceholderSizeAdjustment(dp: DeviceProfile?): Int =
max((dp!!.insets.top - dp.splitPlaceholderInset).toDouble(), 0.0).toInt()
override fun setSplitInstructionsParams(
out: View,
dp: DeviceProfile,
splitInstructionsHeight: Int,
splitInstructionsWidth: Int
) {
out.pivotX = 0f
out.pivotY = splitInstructionsHeight.toFloat()
out.rotation = degreesRotated
val distanceToEdge =
out.resources.getDimensionPixelSize(
R.dimen.split_instructions_bottom_margin_phone_landscape
)
// Adjust for any insets on the left edge
val insetCorrectionX = dp.insets.left
// Center the view in case of unbalanced insets on top or bottom of screen
val insetCorrectionY = (dp.insets.bottom - dp.insets.top) / 2
out.translationX = (distanceToEdge - insetCorrectionX).toFloat()
out.translationY =
(-splitInstructionsHeight - splitInstructionsWidth) / 2f + insetCorrectionY
// Setting gravity to LEFT instead of the lint-recommended START because we always want this
// view to be screen-left when phone is in landscape, regardless of the RtL setting.
val lp = out.layoutParams as FrameLayout.LayoutParams
lp.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
out.layoutParams = lp
}
override fun getFinalSplitPlaceholderBounds(
splitDividerSize: Int,
dp: DeviceProfile,
@StagePosition stagePosition: Int,
out1: Rect,
out2: Rect
) {
// In fake land/seascape, the window bounds are always top and bottom half
val screenHeight = dp.heightPx
val screenWidth = dp.widthPx
out1.set(0, 0, screenWidth, screenHeight / 2 - splitDividerSize)
out2.set(0, screenHeight / 2 + splitDividerSize, screenWidth, screenHeight)
}
override fun setSplitTaskSwipeRect(
dp: DeviceProfile,
outRect: Rect,
splitInfo: SplitBounds,
desiredStagePosition: Int
) {
val topLeftTaskPercent: Float
val dividerBarPercent: Float
if (splitInfo.appsStackedVertically) {
topLeftTaskPercent = splitInfo.topTaskPercent
dividerBarPercent = splitInfo.dividerHeightPercent
} else {
topLeftTaskPercent = splitInfo.leftTaskPercent
dividerBarPercent = splitInfo.dividerWidthPercent
}
if (desiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
outRect.bottom = outRect.top + (outRect.height() * topLeftTaskPercent).toInt()
} else {
outRect.top += (outRect.height() * (topLeftTaskPercent + dividerBarPercent)).toInt()
}
}
override fun measureGroupedTaskViewThumbnailBounds(
primarySnapshot: View,
secondarySnapshot: View,
parentWidth: Int,
parentHeight: Int,
splitBoundsConfig: SplitBounds,
dp: DeviceProfile,
isRtl: Boolean
) {
val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams
val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams
// Swap the margins that are set in TaskView#setRecentsOrientedState()
secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx
primaryParams.topMargin = 0
// Measure and layout the thumbnails bottom up, since the primary is on the visual left
// (portrait bottom) and secondary is on the right (portrait top)
val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
val dividerBar =
Math.round(
totalThumbnailHeight *
if (splitBoundsConfig.appsStackedVertically)
splitBoundsConfig.dividerHeightPercent
else splitBoundsConfig.dividerWidthPercent
)
val (taskViewFirst, taskViewSecond) =
getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight)
primarySnapshot.translationY = spaceAboveSnapshot.toFloat()
primarySnapshot.measure(
MeasureSpec.makeMeasureSpec(taskViewFirst.x, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(taskViewFirst.y, MeasureSpec.EXACTLY)
)
val translationY = taskViewFirst.y + spaceAboveSnapshot + dividerBar
secondarySnapshot.translationY = (translationY - spaceAboveSnapshot).toFloat()
secondarySnapshot.measure(
MeasureSpec.makeMeasureSpec(taskViewSecond.x, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(taskViewSecond.y, MeasureSpec.EXACTLY)
)
}
override fun getGroupedTaskViewSizes(
dp: DeviceProfile,
splitBoundsConfig: SplitBounds,
parentWidth: Int,
parentHeight: Int
): Pair<Point, Point> {
val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
val dividerBar =
Math.round(
totalThumbnailHeight *
if (splitBoundsConfig.appsStackedVertically)
splitBoundsConfig.dividerHeightPercent
else splitBoundsConfig.dividerWidthPercent
)
val taskPercent =
if (splitBoundsConfig.appsStackedVertically) {
splitBoundsConfig.topTaskPercent
} else {
splitBoundsConfig.leftTaskPercent
}
val firstTaskViewSize = Point(parentWidth, (totalThumbnailHeight * taskPercent).toInt())
val secondTaskViewSize =
Point(parentWidth, totalThumbnailHeight - firstTaskViewSize.y - dividerBar)
return Pair(firstTaskViewSize, secondTaskViewSize)
}
override fun setTaskIconParams(
iconParams: FrameLayout.LayoutParams,
taskIconMargin: Int,
taskIconHeight: Int,
thumbnailTopMargin: Int,
isRtl: Boolean
) {
iconParams.gravity =
if (isRtl) {
Gravity.START or Gravity.CENTER_VERTICAL
} else {
Gravity.END or Gravity.CENTER_VERTICAL
}
iconParams.rightMargin = -taskIconHeight - taskIconMargin / 2
iconParams.leftMargin = 0
iconParams.topMargin = thumbnailTopMargin / 2
iconParams.bottomMargin = 0
}
override fun setIconAppChipChildrenParams(
iconParams: FrameLayout.LayoutParams,
chipChildMarginStart: Int
) {
iconParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL
iconParams.marginStart = chipChildMarginStart
iconParams.topMargin = 0
}
override fun setIconAppChipMenuParams(
iconAppChipView: IconAppChipView,
iconMenuParams: FrameLayout.LayoutParams,
iconMenuMargin: Int,
thumbnailTopMargin: Int
) {
val isRtl = iconAppChipView.layoutDirection == View.LAYOUT_DIRECTION_RTL
if (isRtl) {
iconMenuParams.gravity = Gravity.START or Gravity.BOTTOM
iconMenuParams.marginStart = iconMenuMargin
iconMenuParams.bottomMargin = iconMenuMargin
iconAppChipView.pivotX = iconMenuParams.width - iconMenuParams.height / 2f
iconAppChipView.pivotY = iconMenuParams.height / 2f
} else {
iconMenuParams.gravity = Gravity.END or Gravity.TOP
iconMenuParams.marginStart = 0
iconMenuParams.bottomMargin = 0
iconAppChipView.pivotX = iconMenuParams.width / 2f
iconAppChipView.pivotY = iconMenuParams.width / 2f
}
iconMenuParams.topMargin = iconMenuMargin
iconMenuParams.marginEnd = iconMenuMargin
iconAppChipView.setSplitTranslationY(0f)
iconAppChipView.setRotation(degreesRotated)
}
override fun setSplitIconParams(
primaryIconView: View,
secondaryIconView: View,
taskIconHeight: Int,
primarySnapshotWidth: Int,
primarySnapshotHeight: Int,
groupedTaskViewHeight: Int,
groupedTaskViewWidth: Int,
isRtl: Boolean,
deviceProfile: DeviceProfile,
splitConfig: SplitBounds
) {
val primaryIconParams = primaryIconView.layoutParams as FrameLayout.LayoutParams
val secondaryIconParams =
if (Flags.enableOverviewIconMenu())
secondaryIconView.layoutParams as FrameLayout.LayoutParams
else FrameLayout.LayoutParams(primaryIconParams)
// We calculate the "midpoint" of the thumbnail area, and place the icons there.
// This is the place where the thumbnail area splits by default, in a near-50/50 split.
// It is usually not exactly 50/50, due to insets/screen cutouts.
val fullscreenInsetThickness = (deviceProfile.insets.top - deviceProfile.insets.bottom)
val fullscreenMidpointFromBottom = ((deviceProfile.heightPx - fullscreenInsetThickness) / 2)
val midpointFromBottomPct = fullscreenMidpointFromBottom.toFloat() / deviceProfile.heightPx
val insetPct = fullscreenInsetThickness.toFloat() / deviceProfile.heightPx
val spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx
val overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots
val bottomToMidpointOffset =
(overviewThumbnailAreaThickness * midpointFromBottomPct).toInt()
val insetOffset = (overviewThumbnailAreaThickness * insetPct).toInt()
if (Flags.enableOverviewIconMenu()) {
val gravity = if (isRtl) Gravity.BOTTOM or Gravity.START else Gravity.TOP or Gravity.END
primaryIconParams.gravity = gravity
secondaryIconParams.gravity = gravity
} else {
primaryIconParams.gravity = Gravity.BOTTOM or if (isRtl) Gravity.START else Gravity.END
secondaryIconParams.gravity =
Gravity.BOTTOM or if (isRtl) Gravity.START else Gravity.END
}
primaryIconView.translationX = 0f
secondaryIconView.translationX = 0f
when {
Flags.enableOverviewIconMenu() -> {
val primaryAppChipView = primaryIconView as IconAppChipView
val secondaryAppChipView = secondaryIconView as IconAppChipView
if (primaryIconView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight.toFloat())
primaryAppChipView.setSplitTranslationY(0f)
} else {
val secondarySnapshotHeight = groupedTaskViewHeight - primarySnapshotHeight
primaryAppChipView.setSplitTranslationY(secondarySnapshotHeight.toFloat())
}
}
splitConfig.initiatedFromSeascape -> {
// if the split was initiated from seascape,
// the task on the right (secondary) is slightly larger
primaryIconView.translationY = (-bottomToMidpointOffset - insetOffset).toFloat()
secondaryIconView.translationY =
(-bottomToMidpointOffset - insetOffset + taskIconHeight).toFloat()
}
else -> {
// if not,
// the task on the left (primary) is slightly larger
primaryIconView.translationY = -bottomToMidpointOffset.toFloat()
secondaryIconView.translationY =
(-bottomToMidpointOffset + taskIconHeight).toFloat()
}
}
primaryIconView.layoutParams = primaryIconParams
secondaryIconView.layoutParams = secondaryIconParams
}
override fun getDefaultSplitPosition(deviceProfile: DeviceProfile): Int {
throw IllegalStateException("Default position not available in fake landscape")
}
override fun <T> getSplitSelectTaskOffset(
primary: FloatProperty<T>,
secondary: FloatProperty<T>,
deviceProfile: DeviceProfile
): Pair<FloatProperty<T>, FloatProperty<T>> = Pair(primary, secondary)
override fun getFloatingTaskOffscreenTranslationTarget(
floatingTask: View,
onScreenRect: RectF,
@StagePosition stagePosition: Int,
dp: DeviceProfile
): Float = floatingTask.translationY - onScreenRect.height()
override fun setFloatingTaskPrimaryTranslation(
floatingTask: View,
translation: Float,
dp: DeviceProfile
) {
floatingTask.translationY = translation
}
override fun getFloatingTaskPrimaryTranslation(floatingTask: View, dp: DeviceProfile): Float =
floatingTask.translationY
}
@@ -795,7 +795,7 @@ public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements
}
@Override
public Float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp) {
public float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp) {
return dp.isLeftRightSplit
? floatingTask.getTranslationX()
: floatingTask.getTranslationY();
@@ -1,261 +0,0 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.orientation;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.ShapeDrawable;
import android.util.FloatProperty;
import android.util.Pair;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.touch.SingleAxisSwipeDetector;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
import com.android.quickstep.views.IconAppChipView;
import java.util.List;
/**
* Abstraction layer to separate horizontal and vertical specific implementations
* for {@link com.android.quickstep.views.RecentsView}. Majority of these implementations are
* (should be) as simple as choosing the correct X and Y analogous methods.
*/
public interface RecentsPagedOrientationHandler extends PagedOrientationHandler {
RecentsPagedOrientationHandler PORTRAIT = new PortraitPagedViewHandler();
RecentsPagedOrientationHandler LANDSCAPE = new LandscapePagedViewHandler();
RecentsPagedOrientationHandler SEASCAPE = new SeascapePagedViewHandler();
<T> void setSecondary(T target, Float2DAction<T> action, float param);
<T> void set(T target, Int2DAction<T> action, int primaryParam, int secondaryParam);
int getPrimarySize(View view);
float getPrimarySize(RectF rect);
int getSecondaryTranslationDirectionFactor();
float getDegreesRotated();
int getRotation();
boolean isLayoutNaturalToLauncher();
<T> T getPrimaryValue(T x, T y);
<T> T getSecondaryValue(T x, T y);
void setPrimaryScale(View view, float scale);
void setSecondaryScale(View view, float scale);
float getStart(RectF rect);
float getEnd(RectF rect);
int getClearAllSidePadding(View view, boolean isRtl);
int getSecondaryDimension(View view);
FloatProperty<View> getPrimaryViewTranslate();
FloatProperty<View> getSecondaryViewTranslate();
int getSplitTranslationDirectionFactor(@StagePosition int stagePosition,
DeviceProfile deviceProfile);
Pair<FloatProperty, FloatProperty> getSplitSelectTaskOffset(FloatProperty primary,
FloatProperty secondary, DeviceProfile deviceProfile);
int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect);
List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp);
/**
* @param placeholderHeight height of placeholder view in portrait, width in landscape
*/
void getInitialSplitPlaceholderBounds(int placeholderHeight, int placeholderInset,
DeviceProfile dp, @StagePosition int stagePosition, Rect out);
/**
* Centers an icon in the split staging area, accounting for insets.
* @param out The icon that needs to be centered.
* @param onScreenRectCenterX The x-center of the on-screen staging area (most of the Rect is
* offscreen).
* @param onScreenRectCenterY The y-center of the on-screen staging area (most of the Rect is
* offscreen).
* @param fullscreenScaleX A x-scaling factor used to convert coordinates back into pixels.
* @param fullscreenScaleY A y-scaling factor used to convert coordinates back into pixels.
* @param drawableWidth The icon's drawable (final) width.
* @param drawableHeight The icon's drawable (final) height.
* @param dp The device profile, used to report rotation and hardware insets.
* @param stagePosition 0 if the staging area is pinned to top/left, 1 for bottom/right.
*/
void updateSplitIconParams(View out, float onScreenRectCenterX,
float onScreenRectCenterY, float fullscreenScaleX, float fullscreenScaleY,
int drawableWidth, int drawableHeight, DeviceProfile dp,
@StagePosition int stagePosition);
/**
* Sets positioning and rotation for a SplitInstructionsView.
* @param out The SplitInstructionsView that needs to be positioned.
* @param dp The device profile, used to report rotation and device type.
* @param splitInstructionsHeight The SplitInstructionView's height.
* @param splitInstructionsWidth The SplitInstructionView's width.
*/
void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight,
int splitInstructionsWidth);
/**
* @param splitDividerSize height of split screen drag handle in portrait, width in landscape
* @param stagePosition the split position option (top/left, bottom/right) of the first
* task selected for entering split
* @param out1 the bounds for where the first selected app will be
* @param out2 the bounds for where the second selected app will be, complimentary to
* {@param out1} based on {@param initialSplitOption}
*/
void getFinalSplitPlaceholderBounds(int splitDividerSize, DeviceProfile dp,
@StagePosition int stagePosition, Rect out1, Rect out2);
int getDefaultSplitPosition(DeviceProfile deviceProfile);
/**
* @param outRect This is expected to be the rect that has the dimensions for a non-split,
* fullscreen task in overview. This will directly be modified.
* @param desiredStagePosition Which stage position (topLeft/rightBottom) we want to resize
* outRect for
*/
void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, SplitBounds splitInfo,
@SplitConfigurationOptions.StagePosition int desiredStagePosition);
void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot,
int parentWidth, int parentHeight,
SplitBounds splitBoundsConfig, DeviceProfile dp, boolean isRtl);
/**
* Creates two Points representing the dimensions of the two tasks in a GroupedTaskView
*
* @return first -> primary task snapshot, second -> secondary task snapshot.
* x -> width, y -> height
*/
Pair<Point, Point> getGroupedTaskViewSizes(DeviceProfile dp, SplitBounds splitBoundsConfig,
int parentWidth, int parentHeight);
// Overview TaskMenuView methods
/** Sets layout params on a task's app icon. Only use this when app chip is disabled. */
void setTaskIconParams(FrameLayout.LayoutParams iconParams,
int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl);
/**
* Sets layout params on the children of an app chip. Only use this when app chip is enabled.
*/
void setIconAppChipChildrenParams(
FrameLayout.LayoutParams iconParams, int chipChildMarginStart);
void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
FrameLayout.LayoutParams iconMenuParams,
int iconMenuMargin, int thumbnailTopMargin);
void setSplitIconParams(View primaryIconView, View secondaryIconView,
int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
DeviceProfile deviceProfile, SplitBounds splitConfig);
/*
* The following two methods try to center the TaskMenuView in landscape by finding the center
* of the thumbnail view and then subtracting half of the taskMenu width. In this case, the
* taskMenu width is the same size as the thumbnail width (what got set below in
* getTaskMenuWidth()), so we directly use that in the calculations.
*/
float getTaskMenuX(float x, View thumbnailView, DeviceProfile deviceProfile,
float taskInsetMargin, View taskViewIcon);
float getTaskMenuY(float y, View thumbnailView, int stagePosition,
View taskMenuView, float taskInsetMargin, View taskViewIcon);
int getTaskMenuWidth(View thumbnailView, DeviceProfile deviceProfile,
@StagePosition int stagePosition);
int getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile, float taskMenuX,
float taskMenuY);
/**
* Sets linear layout orientation for {@link com.android.launcher3.popup.SystemShortcut} items
* inside task menu view.
*/
void setTaskOptionsMenuLayoutOrientation(DeviceProfile deviceProfile,
LinearLayout taskMenuLayout, int dividerSpacing,
ShapeDrawable dividerDrawable);
/**
* Sets layout param attributes for {@link com.android.launcher3.popup.SystemShortcut} child
* views inside task menu view.
*/
void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp,
LinearLayout viewGroup, DeviceProfile deviceProfile);
/**
* Calculates the position where a Digital Wellbeing Banner should be placed on its parent
* TaskView.
* @return A Pair of Floats representing the proper x and y translations.
*/
Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth,
int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile,
View[] thumbnailViews, int desiredTaskId, View banner);
// The following are only used by TaskViewTouchHandler.
/** @return Either VERTICAL or HORIZONTAL. */
SingleAxisSwipeDetector.Direction getUpDownSwipeDirection();
/** @return Given {@link #getUpDownSwipeDirection()}, whether POSITIVE or NEGATIVE is up. */
int getUpDirection(boolean isRtl);
/** @return Whether the displacement is going towards the top of the screen. */
boolean isGoingUp(float displacement, boolean isRtl);
/** @return Either 1 or -1, a factor to multiply by so the animation goes the correct way. */
int getTaskDragDisplacementFactor(boolean isRtl);
/**
* Maps the velocity from the coordinate plane of the foreground app to that
* of Launcher's (which now will always be portrait)
*/
void adjustFloatingIconStartVelocity(PointF velocity);
/**
* Ensures that outStartRect left bound is within the DeviceProfile's visual boundaries
* @param outStartRect The start rect that will directly be modified
*/
void fixBoundsForHomeAnimStartRect(RectF outStartRect, DeviceProfile deviceProfile);
/**
* Determine the target translation for animating the FloatingTaskView out. This value could
* either be an x-coordinate or a y-coordinate, depending on which way the FloatingTaskView was
* docked.
*
* @param floatingTask The FloatingTaskView.
* @param onScreenRect The current on-screen dimensions of the FloatingTaskView.
* @param stagePosition STAGE_POSITION_TOP_OR_LEFT or STAGE_POSITION_BOTTOM_OR_RIGHT.
* @param dp The device profile.
* @return A float. When an animation translates the FloatingTaskView to this position, it will
* appear to tuck away off the edge of the screen.
*/
float getFloatingTaskOffscreenTranslationTarget(View floatingTask, RectF onScreenRect,
@StagePosition int stagePosition, DeviceProfile dp);
/**
* Sets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be
* either x or y), depending on how the view is oriented.
*
* @param floatingTask The FloatingTaskView to be translated.
* @param translation The target translation value.
* @param dp The current device profile.
*/
void setFloatingTaskPrimaryTranslation(View floatingTask, float translation, DeviceProfile dp);
/**
* Gets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be
* either x or y), depending on how the view is oriented.
*
* @param floatingTask The FloatingTaskView in question.
* @param dp The current device profile.
* @return The current translation value.
*/
Float getFloatingTaskPrimaryTranslation(View floatingTask, DeviceProfile dp);
}
@@ -0,0 +1,379 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.orientation
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.ShapeDrawable
import android.util.FloatProperty
import android.util.Pair
import android.view.View
import android.widget.FrameLayout
import android.widget.LinearLayout
import com.android.launcher3.DeviceProfile
import com.android.launcher3.touch.PagedOrientationHandler
import com.android.launcher3.touch.PagedOrientationHandler.Float2DAction
import com.android.launcher3.touch.PagedOrientationHandler.Int2DAction
import com.android.launcher3.touch.SingleAxisSwipeDetector
import com.android.launcher3.util.SplitConfigurationOptions
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption
import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
import com.android.quickstep.views.IconAppChipView
/**
* Abstraction layer to separate horizontal and vertical specific implementations for
* [com.android.quickstep.views.RecentsView]. Majority of these implementations are (should be) as
* simple as choosing the correct X and Y analogous methods.
*/
interface RecentsPagedOrientationHandler : PagedOrientationHandler {
fun <T> setSecondary(target: T, action: Float2DAction<T>, param: Float)
operator fun <T> set(target: T, action: Int2DAction<T>, primaryParam: Int, secondaryParam: Int)
fun getPrimarySize(view: View): Int
fun getPrimarySize(rect: RectF): Float
val secondaryTranslationDirectionFactor: Int
val degreesRotated: Float
val rotation: Int
val isLayoutNaturalToLauncher: Boolean
fun <T> getPrimaryValue(x: T, y: T): T
fun <T> getSecondaryValue(x: T, y: T): T
fun setPrimaryScale(view: View, scale: Float)
fun setSecondaryScale(view: View, scale: Float)
fun getStart(rect: RectF): Float
fun getEnd(rect: RectF): Float
fun getClearAllSidePadding(view: View, isRtl: Boolean): Int
fun getSecondaryDimension(view: View): Int
val primaryViewTranslate: FloatProperty<View>
val secondaryViewTranslate: FloatProperty<View>
fun getSplitTranslationDirectionFactor(
@StagePosition stagePosition: Int,
deviceProfile: DeviceProfile
): Int
fun <T> getSplitSelectTaskOffset(
primary: FloatProperty<T>,
secondary: FloatProperty<T>,
deviceProfile: DeviceProfile
): Pair<FloatProperty<T>, FloatProperty<T>>
fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int
fun getSplitPositionOptions(dp: DeviceProfile): List<SplitPositionOption>
/** @param placeholderHeight height of placeholder view in portrait, width in landscape */
fun getInitialSplitPlaceholderBounds(
placeholderHeight: Int,
placeholderInset: Int,
dp: DeviceProfile,
@StagePosition stagePosition: Int,
out: Rect
)
/**
* Centers an icon in the split staging area, accounting for insets.
*
* @param out The icon that needs to be centered.
* @param onScreenRectCenterX The x-center of the on-screen staging area (most of the Rect is
* offscreen).
* @param onScreenRectCenterY The y-center of the on-screen staging area (most of the Rect is
* offscreen).
* @param fullscreenScaleX A x-scaling factor used to convert coordinates back into pixels.
* @param fullscreenScaleY A y-scaling factor used to convert coordinates back into pixels.
* @param drawableWidth The icon's drawable (final) width.
* @param drawableHeight The icon's drawable (final) height.
* @param dp The device profile, used to report rotation and hardware insets.
* @param stagePosition 0 if the staging area is pinned to top/left, 1 for bottom/right.
*/
fun updateSplitIconParams(
out: View,
onScreenRectCenterX: Float,
onScreenRectCenterY: Float,
fullscreenScaleX: Float,
fullscreenScaleY: Float,
drawableWidth: Int,
drawableHeight: Int,
dp: DeviceProfile,
@StagePosition stagePosition: Int
)
/**
* Sets positioning and rotation for a SplitInstructionsView.
*
* @param out The SplitInstructionsView that needs to be positioned.
* @param dp The device profile, used to report rotation and device type.
* @param splitInstructionsHeight The SplitInstructionView's height.
* @param splitInstructionsWidth The SplitInstructionView's width.
*/
fun setSplitInstructionsParams(
out: View,
dp: DeviceProfile,
splitInstructionsHeight: Int,
splitInstructionsWidth: Int
)
/**
* @param splitDividerSize height of split screen drag handle in portrait, width in landscape
* @param stagePosition the split position option (top/left, bottom/right) of the first task
* selected for entering split
* @param out1 the bounds for where the first selected app will be
* @param out2 the bounds for where the second selected app will be, complimentary to {@param
* out1} based on {@param initialSplitOption}
*/
fun getFinalSplitPlaceholderBounds(
splitDividerSize: Int,
dp: DeviceProfile,
@StagePosition stagePosition: Int,
out1: Rect,
out2: Rect
)
fun getDefaultSplitPosition(deviceProfile: DeviceProfile): Int
/**
* @param outRect This is expected to be the rect that has the dimensions for a non-split,
* fullscreen task in overview. This will directly be modified.
* @param desiredStagePosition Which stage position (topLeft/rightBottom) we want to resize
* outRect for
*/
fun setSplitTaskSwipeRect(
dp: DeviceProfile,
outRect: Rect,
splitInfo: SplitConfigurationOptions.SplitBounds,
@StagePosition desiredStagePosition: Int
)
fun measureGroupedTaskViewThumbnailBounds(
primarySnapshot: View,
secondarySnapshot: View,
parentWidth: Int,
parentHeight: Int,
splitBoundsConfig: SplitConfigurationOptions.SplitBounds,
dp: DeviceProfile,
isRtl: Boolean
)
/**
* Creates two Points representing the dimensions of the two tasks in a GroupedTaskView
*
* @return first -> primary task snapshot, second -> secondary task snapshot. x -> width, y ->
* height
*/
fun getGroupedTaskViewSizes(
dp: DeviceProfile,
splitBoundsConfig: SplitConfigurationOptions.SplitBounds,
parentWidth: Int,
parentHeight: Int
): Pair<Point, Point>
// Overview TaskMenuView methods
/** Sets layout params on a task's app icon. Only use this when app chip is disabled. */
fun setTaskIconParams(
iconParams: FrameLayout.LayoutParams,
taskIconMargin: Int,
taskIconHeight: Int,
thumbnailTopMargin: Int,
isRtl: Boolean
)
/**
* Sets layout params on the children of an app chip. Only use this when app chip is enabled.
*/
fun setIconAppChipChildrenParams(
iconParams: FrameLayout.LayoutParams,
chipChildMarginStart: Int
)
fun setIconAppChipMenuParams(
iconAppChipView: IconAppChipView,
iconMenuParams: FrameLayout.LayoutParams,
iconMenuMargin: Int,
thumbnailTopMargin: Int
)
fun setSplitIconParams(
primaryIconView: View,
secondaryIconView: View,
taskIconHeight: Int,
primarySnapshotWidth: Int,
primarySnapshotHeight: Int,
groupedTaskViewHeight: Int,
groupedTaskViewWidth: Int,
isRtl: Boolean,
deviceProfile: DeviceProfile,
splitConfig: SplitConfigurationOptions.SplitBounds
)
/*
* The following two methods try to center the TaskMenuView in landscape by finding the center
* of the thumbnail view and then subtracting half of the taskMenu width. In this case, the
* taskMenu width is the same size as the thumbnail width (what got set below in
* getTaskMenuWidth()), so we directly use that in the calculations.
*/
fun getTaskMenuX(
x: Float,
thumbnailView: View,
deviceProfile: DeviceProfile,
taskInsetMargin: Float,
taskViewIcon: View
): Float
fun getTaskMenuY(
y: Float,
thumbnailView: View,
stagePosition: Int,
taskMenuView: View,
taskInsetMargin: Float,
taskViewIcon: View
): Float
fun getTaskMenuWidth(
thumbnailView: View,
deviceProfile: DeviceProfile,
@StagePosition stagePosition: Int
): Int
fun getTaskMenuHeight(
taskInsetMargin: Float,
deviceProfile: DeviceProfile,
taskMenuX: Float,
taskMenuY: Float
): Int
/**
* Sets linear layout orientation for [com.android.launcher3.popup.SystemShortcut] items inside
* task menu view.
*/
fun setTaskOptionsMenuLayoutOrientation(
deviceProfile: DeviceProfile,
taskMenuLayout: LinearLayout,
dividerSpacing: Int,
dividerDrawable: ShapeDrawable
)
/**
* Sets layout param attributes for [com.android.launcher3.popup.SystemShortcut] child views
* inside task menu view.
*/
fun setLayoutParamsForTaskMenuOptionItem(
lp: LinearLayout.LayoutParams,
viewGroup: LinearLayout,
deviceProfile: DeviceProfile
)
/**
* Calculates the position where a Digital Wellbeing Banner should be placed on its parent
* TaskView.
*
* @return A Pair of Floats representing the proper x and y translations.
*/
fun getDwbLayoutTranslations(
taskViewWidth: Int,
taskViewHeight: Int,
splitBounds: SplitConfigurationOptions.SplitBounds?,
deviceProfile: DeviceProfile,
thumbnailViews: Array<View>,
desiredTaskId: Int,
banner: View
): Pair<Float, Float>
// The following are only used by TaskViewTouchHandler.
/** @return Either VERTICAL or HORIZONTAL. */
val upDownSwipeDirection: SingleAxisSwipeDetector.Direction
/** @return Given [.getUpDownSwipeDirection], whether POSITIVE or NEGATIVE is up. */
fun getUpDirection(isRtl: Boolean): Int
/** @return Whether the displacement is going towards the top of the screen. */
fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean
/** @return Either 1 or -1, a factor to multiply by so the animation goes the correct way. */
fun getTaskDragDisplacementFactor(isRtl: Boolean): Int
/**
* Maps the velocity from the coordinate plane of the foreground app to that of Launcher's
* (which now will always be portrait)
*/
fun adjustFloatingIconStartVelocity(velocity: PointF)
/**
* Ensures that outStartRect left bound is within the DeviceProfile's visual boundaries
*
* @param outStartRect The start rect that will directly be modified
*/
fun fixBoundsForHomeAnimStartRect(outStartRect: RectF, deviceProfile: DeviceProfile)
/**
* Determine the target translation for animating the FloatingTaskView out. This value could
* either be an x-coordinate or a y-coordinate, depending on which way the FloatingTaskView was
* docked.
*
* @param floatingTask The FloatingTaskView.
* @param onScreenRect The current on-screen dimensions of the FloatingTaskView.
* @param stagePosition STAGE_POSITION_TOP_OR_LEFT or STAGE_POSITION_BOTTOM_OR_RIGHT.
* @param dp The device profile.
* @return A float. When an animation translates the FloatingTaskView to this position, it will
* appear to tuck away off the edge of the screen.
*/
fun getFloatingTaskOffscreenTranslationTarget(
floatingTask: View,
onScreenRect: RectF,
@StagePosition stagePosition: Int,
dp: DeviceProfile
): Float
/**
* Sets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be
* either x or y), depending on how the view is oriented.
*
* @param floatingTask The FloatingTaskView to be translated.
* @param translation The target translation value.
* @param dp The current device profile.
*/
fun setFloatingTaskPrimaryTranslation(floatingTask: View, translation: Float, dp: DeviceProfile)
/**
* Gets the translation of a FloatingTaskView along its "slide-in/slide-out" axis (could be
* either x or y), depending on how the view is oriented.
*
* @param floatingTask The FloatingTaskView in question.
* @param dp The current device profile.
* @return The current translation value.
*/
fun getFloatingTaskPrimaryTranslation(floatingTask: View, dp: DeviceProfile): Float
companion object {
@JvmField val PORTRAIT: RecentsPagedOrientationHandler = PortraitPagedViewHandler()
@JvmField val LANDSCAPE: RecentsPagedOrientationHandler = LandscapePagedViewHandler()
@JvmField val SEASCAPE: RecentsPagedOrientationHandler = SeascapePagedViewHandler()
}
}
@@ -1,418 +0,0 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.orientation;
import static android.view.Gravity.BOTTOM;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.view.Gravity.END;
import static android.view.Gravity.RIGHT;
import static android.view.Gravity.START;
import static android.view.Gravity.TOP;
import static android.view.View.LAYOUT_DIRECTION_RTL;
import static com.android.launcher3.Flags.enableOverviewIconMenu;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.util.Pair;
import android.view.Gravity;
import android.view.Surface;
import android.view.View;
import android.widget.FrameLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.touch.SingleAxisSwipeDetector;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.launcher3.views.BaseDragLayer;
import com.android.quickstep.views.IconAppChipView;
import java.util.Collections;
import java.util.List;
public class SeascapePagedViewHandler extends LandscapePagedViewHandler {
@Override
public int getSecondaryTranslationDirectionFactor() {
return -1;
}
@Override
public int getSplitTranslationDirectionFactor(int stagePosition, DeviceProfile deviceProfile) {
if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) {
return -1;
} else {
return 1;
}
}
@Override
public boolean getRecentsRtlSetting(Resources resources) {
return Utilities.isRtl(resources);
}
@Override
public float getDegreesRotated() {
return 270;
}
@Override
public int getRotation() {
return Surface.ROTATION_270;
}
@Override
public void adjustFloatingIconStartVelocity(PointF velocity) {
float oldX = velocity.x;
float oldY = velocity.y;
velocity.set(oldY, -oldX);
}
@Override
public float getTaskMenuX(float x, View thumbnailView,
DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) {
return x + taskInsetMargin;
}
@Override
public float getTaskMenuY(float y, View thumbnailView, int stagePosition,
View taskMenuView, float taskInsetMargin, View taskViewIcon) {
if (enableOverviewIconMenu()) {
return y;
}
BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskMenuView.getLayoutParams();
int taskMenuWidth = lp.width;
if (stagePosition == STAGE_POSITION_UNDEFINED) {
return y + taskInsetMargin
+ (thumbnailView.getMeasuredHeight() + taskMenuWidth) / 2f;
} else {
return y + taskMenuWidth + taskInsetMargin;
}
}
@Override
public int getTaskMenuHeight(float taskInsetMargin, DeviceProfile deviceProfile,
float taskMenuX, float taskMenuY) {
return (int) (deviceProfile.availableWidthPx - taskInsetMargin - taskMenuX);
}
@Override
public void setSplitTaskSwipeRect(DeviceProfile dp, Rect outRect, SplitBounds splitInfo,
int desiredStagePosition) {
float topLeftTaskPercent = splitInfo.appsStackedVertically
? splitInfo.topTaskPercent
: splitInfo.leftTaskPercent;
float dividerBarPercent = splitInfo.appsStackedVertically
? splitInfo.dividerHeightPercent
: splitInfo.dividerWidthPercent;
// In seascape, the primary thumbnail is counterintuitively placed at the physical bottom of
// the screen. This is to preserve consistency when the user rotates: From the user's POV,
// the primary should always be on the left.
if (desiredStagePosition == SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT) {
outRect.top += (int) (outRect.height() * ((1 - topLeftTaskPercent)));
} else {
outRect.bottom -= (int) (outRect.height() * (topLeftTaskPercent + dividerBarPercent));
}
}
@Override
public Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth,
int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile,
View[] thumbnailViews, int desiredTaskId, View banner) {
boolean isRtl = banner.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
float translationX = 0;
float translationY = 0;
FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams();
banner.setPivotX(0);
banner.setPivotY(0);
banner.setRotation(getDegreesRotated());
translationX = taskViewWidth - banner.getHeight();
FrameLayout.LayoutParams snapshotParams =
(FrameLayout.LayoutParams) thumbnailViews[0]
.getLayoutParams();
bannerParams.gravity = BOTTOM | (isRtl ? END : START);
if (splitBounds == null) {
// Single, fullscreen case
bannerParams.width = taskViewHeight - snapshotParams.topMargin;
translationY = banner.getHeight();
return new Pair<>(translationX, translationY);
}
// Set correct width
if (desiredTaskId == splitBounds.leftTopTaskId) {
bannerParams.width = thumbnailViews[0].getMeasuredHeight();
} else {
bannerParams.width = thumbnailViews[1].getMeasuredHeight();
}
// Set translations
if (desiredTaskId == splitBounds.rightBottomTaskId) {
translationY = banner.getHeight();
}
if (desiredTaskId == splitBounds.leftTopTaskId) {
float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically
? (1f - splitBounds.topTaskPercent)
: (1f - splitBounds.leftTaskPercent);
translationY = banner.getHeight()
- ((taskViewHeight - snapshotParams.topMargin)
* bottomRightTaskPlusDividerPercent);
}
return new Pair<>(translationX, translationY);
}
@Override
public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
return dp.widthPx - rect.right;
}
@Override
public List<SplitPositionOption> getSplitPositionOptions(DeviceProfile dp) {
// Add "right" option which is actually the top
return Collections.singletonList(new SplitPositionOption(
R.drawable.ic_split_horizontal, R.string.recent_task_option_split_screen,
STAGE_POSITION_BOTTOM_OR_RIGHT, STAGE_TYPE_MAIN));
}
@Override
public void setSplitInstructionsParams(View out, DeviceProfile dp, int splitInstructionsHeight,
int splitInstructionsWidth) {
out.setPivotX(0);
out.setPivotY(splitInstructionsHeight);
out.setRotation(getDegreesRotated());
int distanceToEdge = out.getResources().getDimensionPixelSize(
R.dimen.split_instructions_bottom_margin_phone_landscape);
// Adjust for any insets on the right edge
int insetCorrectionX = dp.getInsets().right;
// Center the view in case of unbalanced insets on top or bottom of screen
int insetCorrectionY = (dp.getInsets().bottom - dp.getInsets().top) / 2;
out.setTranslationX(splitInstructionsWidth - distanceToEdge + insetCorrectionX);
out.setTranslationY(((-splitInstructionsHeight + splitInstructionsWidth) / 2f)
+ insetCorrectionY);
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) out.getLayoutParams();
// Setting gravity to RIGHT instead of the lint-recommended END because we always want this
// view to be screen-right when phone is in seascape, regardless of the RtL setting.
lp.gravity = RIGHT | CENTER_VERTICAL;
out.setLayoutParams(lp);
}
@Override
public void setTaskIconParams(FrameLayout.LayoutParams iconParams,
int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
iconParams.setMargins(-taskIconHeight - taskIconMargin / 2, thumbnailTopMargin / 2, 0, 0);
}
@Override
public void setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams,
int chipChildMarginStart) {
iconParams.setMargins(0, 0, 0, 0);
iconParams.setMarginStart(chipChildMarginStart);
iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
}
@Override
public void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) {
boolean isRtl = iconAppChipView.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
iconMenuParams.gravity = (isRtl ? TOP : BOTTOM) | (isRtl ? END : START);
iconMenuParams.setMarginStart(0);
iconMenuParams.topMargin = isRtl ? iconMenuMargin : 0;
iconMenuParams.bottomMargin = 0;
iconMenuParams.setMarginEnd(isRtl ? thumbnailTopMargin : 0);
// Use half menu height to place the pivot within the X/Y center of icon in the menu.
float iconCenter = iconAppChipView.getHeight() / 2f;
iconAppChipView.setPivotX(isRtl ? iconMenuParams.width / 2f : iconCenter);
iconAppChipView.setPivotY(
isRtl ? iconMenuParams.width / 2f : iconCenter - iconMenuMargin);
iconAppChipView.setSplitTranslationY(0);
iconAppChipView.setRotation(getDegreesRotated());
}
@Override
public void setSplitIconParams(View primaryIconView, View secondaryIconView,
int taskIconHeight, int primarySnapshotWidth, int primarySnapshotHeight,
int groupedTaskViewHeight, int groupedTaskViewWidth, boolean isRtl,
DeviceProfile deviceProfile, SplitBounds splitConfig) {
super.setSplitIconParams(primaryIconView, secondaryIconView, taskIconHeight,
primarySnapshotWidth, primarySnapshotHeight, groupedTaskViewHeight,
groupedTaskViewWidth, isRtl, deviceProfile, splitConfig);
FrameLayout.LayoutParams primaryIconParams =
(FrameLayout.LayoutParams) primaryIconView.getLayoutParams();
FrameLayout.LayoutParams secondaryIconParams =
(FrameLayout.LayoutParams) secondaryIconView.getLayoutParams();
// We calculate the "midpoint" of the thumbnail area, and place the icons there.
// This is the place where the thumbnail area splits by default, in a near-50/50 split.
// It is usually not exactly 50/50, due to insets/screen cutouts.
int fullscreenInsetThickness = deviceProfile.getInsets().top
- deviceProfile.getInsets().bottom;
int fullscreenMidpointFromBottom = ((deviceProfile.heightPx
- fullscreenInsetThickness) / 2);
float midpointFromBottomPct = (float) fullscreenMidpointFromBottom / deviceProfile.heightPx;
float insetPct = (float) fullscreenInsetThickness / deviceProfile.heightPx;
int spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx;
int overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots;
int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness * midpointFromBottomPct);
int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct);
int gravity = (isRtl ? TOP : BOTTOM) | (isRtl ? END : START);
primaryIconParams.gravity = gravity;
secondaryIconParams.gravity = gravity;
primaryIconView.setTranslationX(0);
secondaryIconView.setTranslationX(0);
if (enableOverviewIconMenu()) {
IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
if (isRtl) {
primaryAppChipView.setSplitTranslationY(
groupedTaskViewHeight - primarySnapshotHeight);
secondaryAppChipView.setSplitTranslationY(0);
} else {
secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight);
primaryAppChipView.setSplitTranslationY(0);
}
} else if (splitConfig.initiatedFromSeascape) {
// if the split was initiated from seascape,
// the task on the right (secondary) is slightly larger
if (isRtl) {
primaryIconView.setTranslationY(bottomToMidpointOffset - insetOffset
+ taskIconHeight);
secondaryIconView.setTranslationY(bottomToMidpointOffset - insetOffset);
} else {
primaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset
+ taskIconHeight);
secondaryIconView.setTranslationY(-bottomToMidpointOffset - insetOffset);
}
} else {
// if not,
// the task on the left (primary) is slightly larger
if (isRtl) {
primaryIconView.setTranslationY(bottomToMidpointOffset + taskIconHeight);
secondaryIconView.setTranslationY(bottomToMidpointOffset);
} else {
primaryIconView.setTranslationY(-bottomToMidpointOffset + taskIconHeight);
secondaryIconView.setTranslationY(-bottomToMidpointOffset);
}
}
primaryIconView.setLayoutParams(primaryIconParams);
secondaryIconView.setLayoutParams(secondaryIconParams);
}
@Override
public void measureGroupedTaskViewThumbnailBounds(View primarySnapshot, View secondarySnapshot,
int parentWidth, int parentHeight, SplitBounds splitBoundsConfig, DeviceProfile dp,
boolean isRtl) {
FrameLayout.LayoutParams primaryParams =
(FrameLayout.LayoutParams) primarySnapshot.getLayoutParams();
FrameLayout.LayoutParams secondaryParams =
(FrameLayout.LayoutParams) secondarySnapshot.getLayoutParams();
// Swap the margins that are set in TaskView#setRecentsOrientedState()
secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx;
primaryParams.topMargin = 0;
// Measure and layout the thumbnails bottom up, since the primary is on the visual left
// (portrait bottom) and secondary is on the right (portrait top)
int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
int dividerBar = Math.round(totalThumbnailHeight * (splitBoundsConfig.appsStackedVertically
? splitBoundsConfig.dividerHeightPercent
: splitBoundsConfig.dividerWidthPercent));
Pair<Point, Point> taskViewSizes =
getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight);
secondarySnapshot.setTranslationY(0);
primarySnapshot.setTranslationY(taskViewSizes.second.y + spaceAboveSnapshot + dividerBar);
primarySnapshot.measure(
View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.x, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(taskViewSizes.first.y, View.MeasureSpec.EXACTLY)
);
secondarySnapshot.measure(
View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.x, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(taskViewSizes.second.y, View.MeasureSpec.EXACTLY)
);
}
@Override
public Pair<Point, Point> getGroupedTaskViewSizes(
DeviceProfile dp,
SplitBounds splitBoundsConfig,
int parentWidth,
int parentHeight) {
// Measure and layout the thumbnails bottom up, since the primary is on the visual left
// (portrait bottom) and secondary is on the right (portrait top)
int spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx;
int totalThumbnailHeight = parentHeight - spaceAboveSnapshot;
int dividerBar = Math.round(totalThumbnailHeight * (splitBoundsConfig.appsStackedVertically
? splitBoundsConfig.dividerHeightPercent
: splitBoundsConfig.dividerWidthPercent));
float taskPercent = splitBoundsConfig.appsStackedVertically
? splitBoundsConfig.topTaskPercent
: splitBoundsConfig.leftTaskPercent;
Point firstTaskViewSize = new Point(
parentWidth,
(int) (totalThumbnailHeight * taskPercent)
);
Point secondTaskViewSize = new Point(
parentWidth,
totalThumbnailHeight - firstTaskViewSize.y - dividerBar
);
return new Pair<>(firstTaskViewSize, secondTaskViewSize);
}
/* ---------- The following are only used by TaskViewTouchHandler. ---------- */
@Override
public SingleAxisSwipeDetector.Direction getUpDownSwipeDirection() {
return HORIZONTAL;
}
@Override
public int getUpDirection(boolean isRtl) {
return isRtl ? SingleAxisSwipeDetector.DIRECTION_POSITIVE
: SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
}
@Override
public boolean isGoingUp(float displacement, boolean isRtl) {
return isRtl ? displacement > 0 : displacement < 0;
}
@Override
public int getTaskDragDisplacementFactor(boolean isRtl) {
return isRtl ? -1 : 1;
}
/* -------------------- */
}
@@ -0,0 +1,433 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.orientation
import android.content.res.Resources
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
import android.util.Pair
import android.view.Gravity
import android.view.Surface
import android.view.View
import android.view.View.MeasureSpec
import android.widget.FrameLayout
import androidx.core.util.component1
import androidx.core.util.component2
import com.android.launcher3.DeviceProfile
import com.android.launcher3.Flags
import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.touch.SingleAxisSwipeDetector
import com.android.launcher3.util.SplitConfigurationOptions.*
import com.android.launcher3.views.BaseDragLayer
import com.android.quickstep.views.IconAppChipView
class SeascapePagedViewHandler : LandscapePagedViewHandler() {
override val secondaryTranslationDirectionFactor: Int = -1
override fun getSplitTranslationDirectionFactor(
stagePosition: Int,
deviceProfile: DeviceProfile
): Int = if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) -1 else 1
override fun getRecentsRtlSetting(resources: Resources): Boolean = Utilities.isRtl(resources)
override val degreesRotated: Float = 270f
override val rotation: Int = Surface.ROTATION_270
override fun adjustFloatingIconStartVelocity(velocity: PointF) =
velocity.set(velocity.y, -velocity.x)
override fun getTaskMenuX(
x: Float,
thumbnailView: View,
deviceProfile: DeviceProfile,
taskInsetMargin: Float,
taskViewIcon: View
): Float = x + taskInsetMargin
override fun getTaskMenuY(
y: Float,
thumbnailView: View,
stagePosition: Int,
taskMenuView: View,
taskInsetMargin: Float,
taskViewIcon: View
): Float {
if (Flags.enableOverviewIconMenu()) {
return y
}
val lp = taskMenuView.layoutParams as BaseDragLayer.LayoutParams
val taskMenuWidth = lp.width
return if (stagePosition == STAGE_POSITION_UNDEFINED) {
y + taskInsetMargin + (thumbnailView.measuredHeight + taskMenuWidth) / 2f
} else {
y + taskMenuWidth + taskInsetMargin
}
}
override fun getTaskMenuHeight(
taskInsetMargin: Float,
deviceProfile: DeviceProfile,
taskMenuX: Float,
taskMenuY: Float
): Int = (deviceProfile.availableWidthPx - taskInsetMargin - taskMenuX).toInt()
override fun setSplitTaskSwipeRect(
dp: DeviceProfile,
outRect: Rect,
splitInfo: SplitBounds,
desiredStagePosition: Int
) {
val topLeftTaskPercent: Float
val dividerBarPercent: Float
if (splitInfo.appsStackedVertically) {
topLeftTaskPercent = splitInfo.topTaskPercent
dividerBarPercent = splitInfo.dividerHeightPercent
} else {
topLeftTaskPercent = splitInfo.leftTaskPercent
dividerBarPercent = splitInfo.dividerWidthPercent
}
// In seascape, the primary thumbnail is counterintuitively placed at the physical bottom of
// the screen. This is to preserve consistency when the user rotates: From the user's POV,
// the primary should always be on the left.
if (desiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
outRect.top += (outRect.height() * (1 - topLeftTaskPercent)).toInt()
} else {
outRect.bottom -= (outRect.height() * (topLeftTaskPercent + dividerBarPercent)).toInt()
}
}
override fun getDwbLayoutTranslations(
taskViewWidth: Int,
taskViewHeight: Int,
splitBounds: SplitBounds?,
deviceProfile: DeviceProfile,
thumbnailViews: Array<View>,
desiredTaskId: Int,
banner: View
): Pair<Float, Float> {
val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL
val bannerParams = banner.layoutParams as FrameLayout.LayoutParams
bannerParams.gravity = Gravity.BOTTOM or if (isRtl) Gravity.END else Gravity.START
banner.pivotX = 0f
banner.pivotY = 0f
banner.rotation = degreesRotated
val translationX: Float = (taskViewWidth - banner.height).toFloat()
if (splitBounds == null) {
// Single, fullscreen case
bannerParams.width = taskViewHeight - snapshotParams.topMargin
return Pair(translationX, banner.height.toFloat())
}
// Set correct width and translations
val translationY: Float
if (desiredTaskId == splitBounds.leftTopTaskId) {
bannerParams.width = thumbnailViews[0].measuredHeight
val bottomRightTaskPlusDividerPercent =
if (splitBounds.appsStackedVertically) {
1f - splitBounds.topTaskPercent
} else {
1f - splitBounds.leftTaskPercent
}
translationY =
banner.height -
(taskViewHeight - snapshotParams.topMargin) * bottomRightTaskPlusDividerPercent
} else {
bannerParams.width = thumbnailViews[1].measuredHeight
translationY = banner.height.toFloat()
}
return Pair(translationX, translationY)
}
override fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int =
dp.widthPx - rect.right
override fun getSplitPositionOptions(dp: DeviceProfile): List<SplitPositionOption> =
// Add "right" option which is actually the top
listOf(
SplitPositionOption(
R.drawable.ic_split_horizontal,
R.string.recent_task_option_split_screen,
STAGE_POSITION_BOTTOM_OR_RIGHT,
STAGE_TYPE_MAIN
)
)
override fun setSplitInstructionsParams(
out: View,
dp: DeviceProfile,
splitInstructionsHeight: Int,
splitInstructionsWidth: Int
) {
out.pivotX = 0f
out.pivotY = splitInstructionsHeight.toFloat()
out.rotation = degreesRotated
val distanceToEdge =
out.resources.getDimensionPixelSize(
R.dimen.split_instructions_bottom_margin_phone_landscape
)
// Adjust for any insets on the right edge
val insetCorrectionX = dp.insets.right
// Center the view in case of unbalanced insets on top or bottom of screen
val insetCorrectionY = (dp.insets.bottom - dp.insets.top) / 2
out.translationX = (splitInstructionsWidth - distanceToEdge + insetCorrectionX).toFloat()
out.translationY =
(-splitInstructionsHeight + splitInstructionsWidth) / 2f + insetCorrectionY
// Setting gravity to RIGHT instead of the lint-recommended END because we always want this
// view to be screen-right when phone is in seascape, regardless of the RtL setting.
val lp = out.layoutParams as FrameLayout.LayoutParams
lp.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
out.layoutParams = lp
}
override fun setTaskIconParams(
iconParams: FrameLayout.LayoutParams,
taskIconMargin: Int,
taskIconHeight: Int,
thumbnailTopMargin: Int,
isRtl: Boolean
) {
iconParams.gravity =
if (isRtl) {
Gravity.END or Gravity.CENTER_VERTICAL
} else {
Gravity.START or Gravity.CENTER_VERTICAL
}
iconParams.setMargins(-taskIconHeight - taskIconMargin / 2, thumbnailTopMargin / 2, 0, 0)
}
override fun setIconAppChipChildrenParams(
iconParams: FrameLayout.LayoutParams,
chipChildMarginStart: Int
) {
iconParams.setMargins(0, 0, 0, 0)
iconParams.marginStart = chipChildMarginStart
iconParams.gravity = Gravity.START or Gravity.CENTER_VERTICAL
}
override fun setIconAppChipMenuParams(
iconAppChipView: IconAppChipView,
iconMenuParams: FrameLayout.LayoutParams,
iconMenuMargin: Int,
thumbnailTopMargin: Int
) {
val isRtl = iconAppChipView.layoutDirection == View.LAYOUT_DIRECTION_RTL
val iconCenter = iconAppChipView.getHeight() / 2f
if (isRtl) {
iconMenuParams.gravity = Gravity.TOP or Gravity.END
iconMenuParams.topMargin = iconMenuMargin
iconMenuParams.marginEnd = thumbnailTopMargin
// Use half menu height to place the pivot within the X/Y center of icon in the menu.
iconAppChipView.pivotX = iconMenuParams.width / 2f
iconAppChipView.pivotY = iconMenuParams.width / 2f
} else {
iconMenuParams.gravity = Gravity.BOTTOM or Gravity.START
iconMenuParams.topMargin = 0
iconMenuParams.marginEnd = 0
iconAppChipView.pivotX = iconCenter
iconAppChipView.pivotY = iconCenter - iconMenuMargin
}
iconMenuParams.marginStart = 0
iconMenuParams.bottomMargin = 0
iconAppChipView.setSplitTranslationY(0f)
iconAppChipView.setRotation(degreesRotated)
}
override fun setSplitIconParams(
primaryIconView: View,
secondaryIconView: View,
taskIconHeight: Int,
primarySnapshotWidth: Int,
primarySnapshotHeight: Int,
groupedTaskViewHeight: Int,
groupedTaskViewWidth: Int,
isRtl: Boolean,
deviceProfile: DeviceProfile,
splitConfig: SplitBounds
) {
super.setSplitIconParams(
primaryIconView,
secondaryIconView,
taskIconHeight,
primarySnapshotWidth,
primarySnapshotHeight,
groupedTaskViewHeight,
groupedTaskViewWidth,
isRtl,
deviceProfile,
splitConfig
)
val primaryIconParams = primaryIconView.layoutParams as FrameLayout.LayoutParams
val secondaryIconParams = secondaryIconView.layoutParams as FrameLayout.LayoutParams
// We calculate the "midpoint" of the thumbnail area, and place the icons there.
// This is the place where the thumbnail area splits by default, in a near-50/50 split.
// It is usually not exactly 50/50, due to insets/screen cutouts.
val fullscreenInsetThickness = (deviceProfile.insets.top - deviceProfile.insets.bottom)
val fullscreenMidpointFromBottom = (deviceProfile.heightPx - fullscreenInsetThickness) / 2
val midpointFromBottomPct = fullscreenMidpointFromBottom.toFloat() / deviceProfile.heightPx
val insetPct = fullscreenInsetThickness.toFloat() / deviceProfile.heightPx
val spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx
val overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots
val bottomToMidpointOffset =
(overviewThumbnailAreaThickness * midpointFromBottomPct).toInt()
val insetOffset = (overviewThumbnailAreaThickness * insetPct).toInt()
val gravity = if (isRtl) Gravity.TOP or Gravity.END else Gravity.BOTTOM or Gravity.START
primaryIconParams.gravity = gravity
secondaryIconParams.gravity = gravity
primaryIconView.translationX = 0f
secondaryIconView.translationX = 0f
when {
Flags.enableOverviewIconMenu() -> {
val primaryAppChipView = primaryIconView as IconAppChipView
val secondaryAppChipView = secondaryIconView as IconAppChipView
if (isRtl) {
primaryAppChipView.setSplitTranslationY(
(groupedTaskViewHeight - primarySnapshotHeight).toFloat()
)
secondaryAppChipView.setSplitTranslationY(0f)
} else {
secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight.toFloat())
primaryAppChipView.setSplitTranslationY(0f)
}
}
splitConfig.initiatedFromSeascape -> {
// if the split was initiated from seascape,
// the task on the right (secondary) is slightly larger
if (isRtl) {
primaryIconView.translationY =
(bottomToMidpointOffset - insetOffset + taskIconHeight).toFloat()
secondaryIconView.translationY =
(bottomToMidpointOffset - insetOffset).toFloat()
} else {
primaryIconView.translationY =
(-bottomToMidpointOffset - insetOffset + taskIconHeight).toFloat()
secondaryIconView.translationY =
(-bottomToMidpointOffset - insetOffset).toFloat()
}
}
else -> {
// if not,
// the task on the left (primary) is slightly larger
if (isRtl) {
primaryIconView.translationY =
(bottomToMidpointOffset + taskIconHeight).toFloat()
secondaryIconView.translationY = bottomToMidpointOffset.toFloat()
} else {
primaryIconView.translationY =
(-bottomToMidpointOffset + taskIconHeight).toFloat()
secondaryIconView.translationY = -bottomToMidpointOffset.toFloat()
}
}
}
primaryIconView.layoutParams = primaryIconParams
secondaryIconView.layoutParams = secondaryIconParams
}
override fun measureGroupedTaskViewThumbnailBounds(
primarySnapshot: View,
secondarySnapshot: View,
parentWidth: Int,
parentHeight: Int,
splitBoundsConfig: SplitBounds,
dp: DeviceProfile,
isRtl: Boolean
) {
val primaryParams = primarySnapshot.layoutParams as FrameLayout.LayoutParams
val secondaryParams = secondarySnapshot.layoutParams as FrameLayout.LayoutParams
// Swap the margins that are set in TaskView#setRecentsOrientedState()
secondaryParams.topMargin = dp.overviewTaskThumbnailTopMarginPx
primaryParams.topMargin = 0
// Measure and layout the thumbnails bottom up, since the primary is on the visual left
// (portrait bottom) and secondary is on the right (portrait top)
val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
val dividerBar =
Math.round(
totalThumbnailHeight *
if (splitBoundsConfig.appsStackedVertically)
splitBoundsConfig.dividerHeightPercent
else splitBoundsConfig.dividerWidthPercent
)
val (taskViewFirst, taskViewSecond) =
getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight)
secondarySnapshot.translationY = 0f
primarySnapshot.translationY =
(taskViewSecond.y + spaceAboveSnapshot + dividerBar).toFloat()
primarySnapshot.measure(
MeasureSpec.makeMeasureSpec(taskViewFirst.x, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(taskViewFirst.y, MeasureSpec.EXACTLY)
)
secondarySnapshot.measure(
MeasureSpec.makeMeasureSpec(taskViewSecond.x, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(taskViewSecond.y, MeasureSpec.EXACTLY)
)
}
override fun getGroupedTaskViewSizes(
dp: DeviceProfile,
splitBoundsConfig: SplitBounds,
parentWidth: Int,
parentHeight: Int
): Pair<Point, Point> {
// Measure and layout the thumbnails bottom up, since the primary is on the visual left
// (portrait bottom) and secondary is on the right (portrait top)
val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
val dividerBar =
Math.round(
totalThumbnailHeight *
if (splitBoundsConfig.appsStackedVertically)
splitBoundsConfig.dividerHeightPercent
else splitBoundsConfig.dividerWidthPercent
)
val taskPercent =
if (splitBoundsConfig.appsStackedVertically) {
splitBoundsConfig.topTaskPercent
} else {
splitBoundsConfig.leftTaskPercent
}
val firstTaskViewSize = Point(parentWidth, (totalThumbnailHeight * taskPercent).toInt())
val secondTaskViewSize =
Point(parentWidth, totalThumbnailHeight - firstTaskViewSize.y - dividerBar)
return Pair(firstTaskViewSize, secondTaskViewSize)
}
/* ---------- The following are only used by TaskViewTouchHandler. ---------- */
override val upDownSwipeDirection: SingleAxisSwipeDetector.Direction =
SingleAxisSwipeDetector.HORIZONTAL
override fun getUpDirection(isRtl: Boolean): Int =
if (isRtl) SingleAxisSwipeDetector.DIRECTION_POSITIVE
else SingleAxisSwipeDetector.DIRECTION_NEGATIVE
override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean =
if (isRtl) displacement > 0 else displacement < 0
override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1
/* -------------------- */
}
@@ -5032,7 +5032,7 @@ public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_T
firstFloatingTaskView.update(mTempRectF, /*progress=*/1f);
RecentsPagedOrientationHandler orientationHandler = getPagedOrientationHandler();
Pair<FloatProperty, FloatProperty> taskViewsFloat =
Pair<FloatProperty<RecentsView>, FloatProperty<RecentsView>> taskViewsFloat =
orientationHandler.getSplitSelectTaskOffset(
TASK_PRIMARY_SPLIT_TRANSLATION, TASK_SECONDARY_SPLIT_TRANSLATION,
mActivity.getDeviceProfile());