Update how PIP size is determined

- minSize = 108dp
- defaultSmallestEdge = max(23% of screen width, minSize)
- the shortest edge of the PIP should be minSize and the rest scales
  according to the aspect ratio
- rather than a default PIP size, use default aspect ratio
- adding expand button
- fitting actions to spec

Fixes: 35358504
Test: manually used test app to try different aspect ratios
Change-Id: Ib6890fb7824889b9edeea7efb5b9771e64fc1514
Signed-off-by: Winson Chung <winsonc@google.com>
This commit is contained in:
Mady Mellor
2017-02-03 11:00:20 -08:00
committed by Winson Chung
parent d2d909778c
commit a7f69740b5
13 changed files with 225 additions and 39 deletions

View File

@@ -18,9 +18,11 @@ package com.android.internal.policy;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.util.Size;
import android.view.Gravity;
import android.view.ViewConfiguration;
import android.widget.Scroller;
@@ -56,6 +58,10 @@ public class PipSnapAlgorithm {
private final int mDefaultSnapMode = SNAP_MODE_CORNERS_AND_EDGES;
private int mSnapMode = mDefaultSnapMode;
private final float mDefaultSizePercent;
private final float mMinAspectRatioForMinSize;
private final float mMaxAspectRatioForMinSize;
private Scroller mScroller;
private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
@@ -63,9 +69,15 @@ public class PipSnapAlgorithm {
private boolean mIsMinimized;
public PipSnapAlgorithm(Context context) {
Resources res = context.getResources();
mContext = context;
mMinimizedVisibleSize = context.getResources().getDimensionPixelSize(
mMinimizedVisibleSize = res.getDimensionPixelSize(
com.android.internal.R.dimen.pip_minimized_visible_size);
mDefaultSizePercent = res.getFloat(
com.android.internal.R.dimen.config_pictureInPictureDefaultSizePercent);
mMaxAspectRatioForMinSize = res.getFloat(
com.android.internal.R.dimen.config_pictureInPictureAspectRatioLimitForMinSize);
mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize;
onConfigurationChanged();
}
@@ -241,6 +253,40 @@ public class PipSnapAlgorithm {
movementBoundsOut.bottom -= imeHeight;
}
/**
* @return the size of the PiP at the given {@param aspectRatio}, ensuring that the minimum edge
* is at least {@param minEdgeSize}.
*/
public Size getSizeForAspectRatio(float aspectRatio, float minEdgeSize, int displayWidth,
int displayHeight) {
final int smallestDisplaySize = Math.min(displayWidth, displayHeight);
final int minSize = (int) Math.max(minEdgeSize, smallestDisplaySize * mDefaultSizePercent);
final int width;
final int height;
if (aspectRatio <= mMinAspectRatioForMinSize || aspectRatio > mMaxAspectRatioForMinSize) {
// Beyond these points, we can just use the min size as the shorter edge
if (aspectRatio <= 1) {
// Portrait, width is the minimum size
width = minSize;
height = Math.round(width / aspectRatio);
} else {
// Landscape, height is the minimum size
height = minSize;
width = Math.round(height * aspectRatio);
}
} else {
// Within these points, we ensure that the bounds fit within the radius of the limits
// at the points
final float widthAtMaxAspectRatioForMinSize = mMaxAspectRatioForMinSize * minSize;
final float radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize);
height = (int) Math.round(Math.sqrt((radius * radius) /
(aspectRatio * aspectRatio + 1)));
width = Math.round(height * aspectRatio);
}
return new Size(width, height);
}
/**
* @return the closest point in {@param points} to the given {@param x} and {@param y}.
*/

View File

@@ -24,9 +24,10 @@
<!-- Flags enabling default window features. See Window.java -->
<bool name="config_defaultWindowFeatureOptionsPanel">false</bool>
<!-- Max default size [WIDTHxHEIGHT] on screen for picture-in-picture windows to fit inside.
These values are in DPs and will be converted to pixel sizes internally. -->
<string translatable="false" name="config_defaultPictureInPictureSize">240x135</string>
<!-- The percentage of the screen width to use for the default width or height of
picture-in-picture windows. Regardless of the percent set here, calculated size will never
be smaller than @dimen/default_minimal_size_pip_resizable_task. -->
<item name="config_pictureInPictureDefaultSizePercent" format="float" type="dimen">0.14</item>
<!-- Default insets [LEFT/RIGHTxTOP/BOTTOM] from the screen edge for picture-in-picture windows.
These values are in DPs and will be converted to pixel sizes internally. -->

View File

@@ -2545,9 +2545,17 @@
These values are in DPs and will be converted to pixel sizes internally. -->
<string translatable="false" name="config_defaultPictureInPictureScreenEdgeInsets">16x16</string>
<!-- Max default size [WIDTHxHEIGHT] on screen for picture-in-picture windows to fit inside.
These values are in DPs and will be converted to pixel sizes internally. -->
<string translatable="false" name="config_defaultPictureInPictureSize">192x120</string>
<!-- The percentage of the screen width to use for the default width or height of
picture-in-picture windows. Regardless of the percent set here, calculated size will never
be smaller than @dimen/default_minimal_size_pip_resizable_task. -->
<item name="config_pictureInPictureDefaultSizePercent" format="float" type="dimen">0.23</item>
<!-- The default aspect ratio for picture-in-picture windows. -->
<item name="config_pictureInPictureDefaultAspectRatio" format="float" type="dimen">1.777778</item>
<!-- This is the limit for the max and min aspect ratio (1 / this value) at which the min size
will be used instead of an adaptive size based loosely on area. -->
<item name="config_pictureInPictureAspectRatioLimitForMinSize" format="float" type="dimen">1.777778</item>
<!-- The default gravity for the picture-in-picture window.
Currently, this maps to Gravity.BOTTOM | Gravity.RIGHT -->

View File

@@ -486,6 +486,9 @@
<!-- The default minimal size of a resizable task, in both dimensions. -->
<dimen name="default_minimal_size_resizable_task">220dp</dimen>
<!-- The default minimal size of a PiP task, in both dimensions. -->
<dimen name="default_minimal_size_pip_resizable_task">108dp</dimen>
<!-- Height of a task when in minimized mode from the top when launcher is resizable. -->
<dimen name="task_height_of_minimized_mode">80dp</dimen>

View File

@@ -318,7 +318,9 @@
<java-symbol type="integer" name="config_defaultDisplayDefaultColorMode" />
<java-symbol type="bool" name="config_enableAppWidgetService" />
<java-symbol type="string" name="config_defaultPictureInPictureScreenEdgeInsets" />
<java-symbol type="string" name="config_defaultPictureInPictureSize" />
<java-symbol type="dimen" name="config_pictureInPictureDefaultSizePercent" />
<java-symbol type="dimen" name="config_pictureInPictureDefaultAspectRatio" />
<java-symbol type="dimen" name="config_pictureInPictureAspectRatioLimitForMinSize" />
<java-symbol type="integer" name="config_defaultPictureInPictureGravity" />
<java-symbol type="dimen" name="config_pictureInPictureMinAspectRatio" />
<java-symbol type="dimen" name="config_pictureInPictureMaxAspectRatio" />
@@ -1777,6 +1779,7 @@
<java-symbol type="id" name="replace_message" />
<java-symbol type="fraction" name="config_dimBehindFadeDuration" />
<java-symbol type="dimen" name="default_minimal_size_resizable_task" />
<java-symbol type="dimen" name="default_minimal_size_pip_resizable_task" />
<java-symbol type="dimen" name="task_height_of_minimized_mode" />
<java-symbol type="fraction" name="config_screenAutoBrightnessDozeScaleFactor" />
<java-symbol type="fraction" name="config_autoBrightnessAdjustmentMaxGamma" />

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2017 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="60dp"
android:height="60dp"
android:viewportWidth="60"
android:viewportHeight="60">
<path
android:fillColor="#FFFFFF"
android:pathData="M8,7.5v45c0,2.75,2.25,5,5,5h34.05c2.75,0,4.95-2.25,4.95-5v-45c0-2.75-2.2-5-4.95-5H13
C10.25,2.5,8,4.75,8,7.5z
M13,6.5h34c0.55,0,1,0.45,1,1v45c0,0.55-0.45,1-1,1H13c-0.55,0-1-0.45-1-1v-45C12,6.95,12.45,6.5,13,6.5z" />
<path
android:pathData="M60,0L0,0l0,60h60V0z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M20.86,35h-2c-0.55,0-1,0.45-1,1v10.5c0,0.55,0.45,1,1,1h10.5c0.55,0,1-0.45,1-1v-2c0-0.55-0.45-1-1-1h-7.5
V36C21.86,35.45,21.41,35,20.86,35z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M29.64,13.5v2c0,0.55,0.45,1,1,1h7.5V24c0,0.55,0.45,1,1,1h2c0.55,0,1-0.45,1-1V13.5c0-0.55-0.45-1-1-1
h-10.5C30.09,12.5,29.64,12.95,29.64,13.5z" />
</vector>

View File

@@ -31,20 +31,33 @@
android:src="@drawable/ic_close_white"
android:background="?android:selectableItemBackgroundBorderless" />
<FrameLayout
android:id="@+id/expand_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:contentDescription="@string/pip_phone_expand"
android:src="@drawable/pip_expand"
android:background="?android:selectableItemBackgroundBorderless" />
</FrameLayout>
<FrameLayout
android:id="@+id/actions_container"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_gravity="bottom"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:background="#66000000"
android:visibility="invisible">
<LinearLayout
android:id="@+id/actions"
android:id="@+id/actions_group"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:orientation="horizontal" />
android:orientation="horizontal"
android:divider="@android:color/transparent"
android:showDividers="middle" />
</FrameLayout>
</FrameLayout>

View File

@@ -731,6 +731,13 @@
<!-- The size of the PIP drag-to-dismiss target. -->
<dimen name="pip_dismiss_target_size">48dp</dimen>
<!-- The shortest-edge size of the expanded PiP. -->
<dimen name="pip_expanded_shortest_edge_size">160dp</dimen>
<!-- The padding between actions in the PiP in landscape Note that the PiP does not reflect
the configuration of the device, so we can't use -land resources. -->
<dimen name="pip_between_action_padding_land">8dp</dimen>
<dimen name="default_gear_space">18dp</dimen>
<dimen name="cell_overlay_padding">18dp</dimen>

View File

@@ -40,8 +40,9 @@ import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.LinearLayout;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
@@ -57,8 +58,9 @@ public class PipMenuActivity extends Activity {
private static final String TAG = "PipMenuActivity";
public static final int MESSAGE_SHOW_MENU = 1;
public static final int MESSAGE_HIDE_MENU = 2;
public static final int MESSAGE_UPDATE_ACTIONS = 3;
public static final int MESSAGE_POKE_MENU = 2;
public static final int MESSAGE_HIDE_MENU = 3;
public static final int MESSAGE_UPDATE_ACTIONS = 4;
private static final long INITIAL_DISMISS_DELAY = 2000;
private static final long POST_INTERACTION_DISMISS_DELAY = 1500;
@@ -67,7 +69,9 @@ public class PipMenuActivity extends Activity {
private boolean mMenuVisible;
private final List<RemoteAction> mActions = new ArrayList<>();
private View mMenuContainer;
private LinearLayout mActionsGroup;
private View mDismissButton;
private int mBetweenActionPaddingLand;
private ObjectAnimator mMenuContainerAnimator;
@@ -83,6 +87,9 @@ public class PipMenuActivity extends Activity {
case MESSAGE_SHOW_MENU:
showMenu();
break;
case MESSAGE_POKE_MENU:
cancelDelayedFinish();
break;
case MESSAGE_HIDE_MENU:
hideMenu();
break;
@@ -127,6 +134,9 @@ public class PipMenuActivity extends Activity {
mDismissButton.setOnClickListener((v) -> {
dismissPip();
});
mActionsGroup = (LinearLayout) findViewById(R.id.actions_group);
mBetweenActionPaddingLand = getResources().getDimensionPixelSize(
R.dimen.pip_between_action_padding_land);
notifyActivityCallback(mMessenger);
showMenu();
@@ -260,26 +270,30 @@ public class PipMenuActivity extends Activity {
}
private void updateActionViews() {
ViewGroup expandContainer = (ViewGroup) findViewById(R.id.expand_container);
ViewGroup actionsContainer = (ViewGroup) findViewById(R.id.actions_container);
actionsContainer.setOnTouchListener((v, ev) -> {
// Do nothing, prevent click through to parent
return true;
});
int actionsContainerHeight = 0;
if (mActions.isEmpty()) {
actionsContainer.setVisibility(View.INVISIBLE);
} else {
actionsContainer.setVisibility(View.VISIBLE);
ViewGroup actionsGroup = (ViewGroup) findViewById(R.id.actions);
if (actionsGroup != null) {
actionsGroup.removeAllViews();
if (mActionsGroup != null) {
mActionsGroup.removeAllViews();
// Recreate the layout
final View decorView = getWindow().getDecorView();
final boolean isLandscapePip = decorView.getMeasuredWidth()
> decorView.getMeasuredHeight();
final LayoutInflater inflater = LayoutInflater.from(this);
for (int i = 0; i < mActions.size(); i++) {
final RemoteAction action = mActions.get(i);
final ImageView actionView = (ImageView) inflater.inflate(
R.layout.pip_menu_action, actionsGroup, false);
R.layout.pip_menu_action, mActionsGroup, false);
action.getIcon().loadDrawableAsync(this, d -> {
d.setTint(Color.WHITE);
actionView.setImageDrawable(d);
@@ -292,10 +306,21 @@ public class PipMenuActivity extends Activity {
Log.w(TAG, "Failed to send action", e);
}
});
actionsGroup.addView(actionView);
if (isLandscapePip && i > 0) {
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
actionView.getLayoutParams();
lp.leftMargin = mBetweenActionPaddingLand;
}
mActionsGroup.addView(actionView);
}
}
actionsContainerHeight = actionsContainer.getLayoutParams().height;
}
// Update the expand container margin to account for the existence of the action container
((FrameLayout.LayoutParams) expandContainer.getLayoutParams()).bottomMargin =
actionsContainerHeight;
expandContainer.requestLayout();
}
private void notifyRegisterInputConsumer() {

View File

@@ -209,6 +209,21 @@ public class PipMenuActivityController {
}
}
/**
* Pokes the menu, indicating that the user is interacting with it.
*/
public void pokeMenu() {
if (mToActivityMessenger != null) {
Message m = Message.obtain();
m.what = PipMenuActivity.MESSAGE_POKE_MENU;
try {
mToActivityMessenger.send(m);
} catch (RemoteException e) {
Log.e(TAG, "Could not notify poke menu", e);
}
}
}
/**
* Hides the menu activity.
*/

View File

@@ -57,8 +57,8 @@ public class PipMotionHelper {
private static final int DEFAULT_MOVE_STACK_DURATION = 225;
private static final int SNAP_STACK_DURATION = 225;
private static final int DISMISS_STACK_DURATION = 375;
private static final int SHRINK_STACK_FROM_MENU_DURATION = 175;
private static final int EXPAND_STACK_TO_MENU_DURATION = 175;
private static final int SHRINK_STACK_FROM_MENU_DURATION = 150;
private static final int EXPAND_STACK_TO_MENU_DURATION = 150;
private static final int EXPAND_STACK_TO_FULLSCREEN_DURATION = 225;
private static final int MINIMIZE_STACK_MAX_DURATION = 200;

View File

@@ -18,11 +18,13 @@ package com.android.systemui.pip.phone;
import android.app.IActivityManager;
import android.content.Context;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
import android.util.Size;
import android.view.IPinnedStackController;
import android.view.IWindowManager;
import android.view.MotionEvent;
@@ -31,6 +33,7 @@ import android.view.ViewConfiguration;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.policy.PipSnapAlgorithm;
import com.android.systemui.R;
import com.android.systemui.statusbar.FlingAnimationUtils;
import java.io.PrintWriter;
@@ -69,6 +72,7 @@ public class PipTouchHandler {
private Rect mNormalMovementBounds = new Rect();
private Rect mExpandedBounds = new Rect();
private Rect mExpandedMovementBounds = new Rect();
private int mExpandedShortestEdgeSize;
private Handler mHandler = new Handler();
private Runnable mShowDismissAffordance = new Runnable() {
@@ -145,6 +149,8 @@ public class PipTouchHandler {
};
mMotionHelper = new PipMotionHelper(mContext, mActivityManager, mSnapAlgorithm,
mFlingAnimationUtils);
mExpandedShortestEdgeSize = context.getResources().getDimensionPixelSize(
R.dimen.pip_expanded_shortest_edge_size);
// Register the listener for input consumer touch events
inputConsumerController.setTouchListener(this::handleTouchEvent);
@@ -181,8 +187,14 @@ public class PipTouchHandler {
Rect normalMovementBounds = new Rect();
mSnapAlgorithm.getMovementBounds(mNormalBounds, insetBounds, normalMovementBounds,
mIsImeShowing ? mImeHeight : 0);
// TODO: Figure out the expanded size policy
mExpandedBounds = new Rect(normalBounds);
// Calculate the expanded size
float aspectRatio = (float) normalBounds.width() / normalBounds.height();
Point displaySize = new Point();
mContext.getDisplay().getRealSize(displaySize);
Size expandedSize = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio,
mExpandedShortestEdgeSize, displaySize.x, displaySize.y);
mExpandedBounds.set(0, 0, expandedSize.getWidth(), expandedSize.getHeight());
Rect expandedMovementBounds = new Rect();
mSnapAlgorithm.getMovementBounds(mExpandedBounds, insetBounds, expandedMovementBounds,
mIsImeShowing ? mImeHeight : 0);
@@ -360,6 +372,12 @@ public class PipTouchHandler {
return;
}
// If the menu is still visible, and we aren't minimized, then just poke the menu
// so that it will timeout after the user stops touching it
if (mMenuController.isMenuVisible() && !mIsMinimized) {
mMenuController.pokeMenu();
}
if (ENABLE_DRAG_TO_DISMISS) {
mDismissViewController.createDismissTarget();
mHandler.postDelayed(mShowDismissAffordance, SHOW_DISMISS_AFFORDANCE_DELAY);

View File

@@ -94,13 +94,16 @@ class PinnedStackController {
// The size and position information that describes where the pinned stack will go by default.
private int mDefaultStackGravity;
private Size mDefaultStackSize;
private float mDefaultAspectRatio;
private Point mScreenEdgeInsets;
// The aspect ratio bounds of the PIP.
private float mMinAspectRatio;
private float mMaxAspectRatio;
// The minimum edge size of the normal PiP bounds.
private int mMinSize;
// Temp vars for calculation
private final DisplayMetrics mTmpMetrics = new DisplayMetrics();
private final Rect mTmpInsets = new Rect();
@@ -151,15 +154,15 @@ class PinnedStackController {
*/
void reloadResources() {
final Resources res = mService.mContext.getResources();
final Size defaultSizeDp = Size.parseSize(res.getString(
com.android.internal.R.string.config_defaultPictureInPictureSize));
mMinSize = res.getDimensionPixelSize(
com.android.internal.R.dimen.default_minimal_size_pip_resizable_task);
mDefaultAspectRatio = res.getFloat(
com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio);
final Size screenEdgeInsetsDp = Size.parseSize(res.getString(
com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets));
mDefaultStackGravity = res.getInteger(
com.android.internal.R.integer.config_defaultPictureInPictureGravity);
mDisplayContent.getDisplay().getRealMetrics(mTmpMetrics);
mDefaultStackSize = new Size(dpToPx(defaultSizeDp.getWidth(), mTmpMetrics),
dpToPx(defaultSizeDp.getHeight(), mTmpMetrics));
mScreenEdgeInsets = new Point(dpToPx(screenEdgeInsetsDp.getWidth(), mTmpMetrics),
dpToPx(screenEdgeInsetsDp.getHeight(), mTmpMetrics));
mMinAspectRatio = res.getFloat(
@@ -199,16 +202,13 @@ class PinnedStackController {
* specified aspect ratio.
*/
Rect transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio) {
// Save the snap fraction, calculate the aspect ratio based on the current bounds
// Save the snap fraction, calculate the aspect ratio based on screen size
final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
getMovementBounds(stackBounds));
final float radius = PointF.length(stackBounds.width(), stackBounds.height());
final int height = (int) Math.round(Math.sqrt((radius * radius) /
(aspectRatio * aspectRatio + 1)));
final int width = Math.round(height * aspectRatio);
final int left = (int) (stackBounds.centerX() - width / 2f);
final int top = (int) (stackBounds.centerY() - height / 2f);
stackBounds.set(left, top, left + width, top + height);
final Size size = getSize(aspectRatio);
final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f);
final int top = (int) (stackBounds.centerY() - size.getHeight() / 2f);
stackBounds.set(left, top, left + size.getWidth(), top + size.getHeight());
mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
if (mIsMinimized) {
applyMinimizedOffset(stackBounds, getMovementBounds(stackBounds));
@@ -216,6 +216,14 @@ class PinnedStackController {
return stackBounds;
}
/**
* @return the size of the PIP based on the given {@param aspectRatio}.
*/
Size getSize(float aspectRatio) {
return mSnapAlgorithm.getSizeForAspectRatio(aspectRatio, mMinSize,
mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
}
/**
* @return the default bounds to show the PIP when there is no active PIP.
*/
@@ -224,8 +232,9 @@ class PinnedStackController {
getInsetBounds(insetBounds);
final Rect defaultBounds = new Rect();
Gravity.apply(mDefaultStackGravity, mDefaultStackSize.getWidth(),
mDefaultStackSize.getHeight(), insetBounds, 0, 0, defaultBounds);
final Size size = getSize(mDefaultAspectRatio);
Gravity.apply(mDefaultStackGravity, size.getWidth(), size.getHeight(), insetBounds, 0, 0,
defaultBounds);
return defaultBounds;
}