Update the drag to dismiss UI to be closer to design

Rather than a circular target for drag to dismiss, there is text / icon
as well as a gradient. As the PIP approaches the text / icon they grow
in size. If the PIP is released overtop of the text / icon, it is
dismissed.

Test: Using PIP test app, have a PIP, drag it to dismiss target area.
Change-Id: I339ad14e144dfd61f0e990ba4d2559642a47b141
This commit is contained in:
Mady Mellor
2017-01-26 10:43:16 -08:00
parent 5523f9a2fc
commit d4e40fb097
6 changed files with 178 additions and 80 deletions

View File

@@ -1,22 +1,22 @@
<!--
Copyright (C) 2016 The Android Open Source Project
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
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
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.
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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<corners
android:radius="100dp" />
<solid
android:color="#66000000" />
android:shape="rectangle">
<gradient
android:startColor="#B3000000"
android:endColor="#00000000"
android:angle="90"/>
</shape>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project
<!--
Copyright (C) 2016 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.
@@ -13,12 +14,44 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pip_dismiss_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/pip_dismiss_background"
android:foreground="@drawable/pip_dismiss"
android:alpha="0"
android:forceHasOverlappingRendering="false" />
android:alpha="0">
<!-- The height of the below view needs to be animated from a window
so it needs to be in a container to resize smoothly -->
<View
android:id="@+id/gradient_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:background="@drawable/pip_dismiss_background" />
<LinearLayout
android:id="@+id/pip_dismiss_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:orientation="horizontal"
android:paddingBottom="32dp" >
<ImageView
android:id="@+id/pip_dismiss_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="2dp"
android:src="@drawable/pip_dismiss"
android:tint="#FFFFFFFF" />
<TextView
android:id="@+id/pip_dismiss_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pip_phone_close"
android:textColor="#FFFFFFFF"
android:textSize="16sp" />
</LinearLayout>
</FrameLayout>

View File

@@ -27,7 +27,7 @@
android:layout_height="48dp"
android:layout_gravity="top|end"
android:padding="10dp"
android:contentDescription="@string/pip_phone_dismiss"
android:contentDescription="@string/pip_phone_close"
android:src="@drawable/ic_close_white"
android:background="?android:selectableItemBackgroundBorderless" />

View File

@@ -1746,8 +1746,8 @@
<!-- Label for PIP action to Minimize the PIP [CHAR LIMIT=25] -->
<string name="pip_phone_minimize">Minimize</string>
<!-- Label for PIP action to Dismiss the PIP -->
<string name="pip_phone_dismiss">Dismiss</string>
<!-- Label for PIP the drag to close zone [CHAR LIMIT=NONE]-->
<string name="pip_phone_close">Close</string>
<!-- PIP section of the tuner. Non-translatable since it should
not appear on production builds ever. -->

View File

@@ -17,13 +17,19 @@
package com.android.systemui.pip.phone;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.TouchDelegate;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.widget.FrameLayout;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
@@ -35,12 +41,22 @@ public class PipDismissViewController {
private static final int SHOW_TARGET_DELAY = 100;
private static final int SHOW_TARGET_DURATION = 200;
private static final float DISMISS_TEXT_MAX_SCALE = 2f;
private static final float DISMISS_GRADIENT_MIN_HEIGHT_PERCENT = 0.33f;
private static final float DISMISS_GRADIENT_MAX_HEIGHT_PERCENT = 0.5f;
private static final float DISMISS_THRESHOLD = 0.55f;
private Context mContext;
private WindowManager mWindowManager;
private View mDismissView;
private Rect mDismissTargetScreenBounds = new Rect();
private View mDismissContainer;
private View mGradientView;
private float mMinHeight;
private float mMaxHeight;
public PipDismissViewController(Context context) {
mContext = context;
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
@@ -51,25 +67,37 @@ public class PipDismissViewController {
*/
public void createDismissTarget() {
if (mDismissView == null) {
// Determine sizes for the gradient
Point windowSize = new Point();
mWindowManager.getDefaultDisplay().getSize(windowSize);
mMinHeight = windowSize.y * DISMISS_GRADIENT_MIN_HEIGHT_PERCENT;
mMaxHeight = windowSize.y * DISMISS_GRADIENT_MAX_HEIGHT_PERCENT;
// Create a new view for the dismiss target
int dismissTargetSize = mContext.getResources().getDimensionPixelSize(
R.dimen.pip_dismiss_target_size);
LayoutInflater inflater = LayoutInflater.from(mContext);
mDismissView = inflater.inflate(R.layout.pip_dismiss_view, null);
mDismissView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
mGradientView = mDismissView.findViewById(R.id.gradient_view);
FrameLayout.LayoutParams glp = (android.widget.FrameLayout.LayoutParams) mGradientView
.getLayoutParams();
glp.height = (int) mMaxHeight;
mGradientView.setLayoutParams(glp);
mGradientView.setPivotY(windowSize.y);
mGradientView.setScaleY(mMaxHeight / mMinHeight); // Set to min height via scaling
mDismissContainer = mDismissView.findViewById(R.id.pip_dismiss_container);
mDismissContainer.addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (mDismissView != null) {
mDismissView.getBoundsOnScreen(mDismissTargetScreenBounds);
if (mDismissContainer != null) {
mDismissContainer.getBoundsOnScreen(mDismissTargetScreenBounds);
}
}
});
// Add the target to the window
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
dismissTargetSize,
dismissTargetSize,
windowSize.x,
(int) mMaxHeight,
WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG,
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
@@ -84,7 +112,8 @@ public class PipDismissViewController {
/**
* Shows the dismiss target.
*/
public void showDismissTarget() {
public void showDismissTarget(Rect pinnedStack) {
updateDismissTarget(pinnedStack);
mDismissView.animate()
.alpha(1f)
.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
@@ -114,6 +143,36 @@ public class PipDismissViewController {
}
}
/**
* Updates the appearance of the dismiss target based on how close the PIP is.
*/
public void updateDismissTarget(Rect pinnedStack) {
// As PIP moves over / away from delete target it grows / shrinks
final float scalePercent = calculateDistancePercent(pinnedStack);
final float newScale = 1 + (DISMISS_TEXT_MAX_SCALE - 1) * scalePercent;
final float minGradientScale = mMinHeight / mMaxHeight;
final float newHeight = Math.max(minGradientScale, scalePercent);
mGradientView.setScaleY(newHeight);
mDismissContainer.setScaleX(newScale);
mDismissContainer.setScaleY(newScale);
}
/**
* @return the percentage of distance the PIP is away from the dismiss target point.
*/
private float calculateDistancePercent(Rect pinnedStack) {
final int distance = mDismissTargetScreenBounds.height();
final int textX = mDismissTargetScreenBounds.centerX();
final int textY = mDismissTargetScreenBounds.bottom;
final float pipCurrX = pinnedStack.centerX();
final float pipCurrY = pinnedStack.bottom;
final float currentDistance = PointF.length(pipCurrX - textX, pipCurrY - textY);
if (currentDistance <= distance) {
return 1 - (currentDistance / distance);
}
return 0;
}
/**
* @return the dismiss target screen bounds.
*/
@@ -121,4 +180,10 @@ public class PipDismissViewController {
return mDismissTargetScreenBounds;
}
/**
* @return whether the PIP is positioned on the dismiss target enough to be dismissed.
*/
public boolean shouldDismiss(Rect pinnedStack) {
return calculateDistancePercent(pinnedStack) >= DISMISS_THRESHOLD;
}
}

View File

@@ -33,6 +33,7 @@ import android.content.Context;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
@@ -71,6 +72,7 @@ public class PipTouchHandler implements TunerService.Tunable {
private static final int DISMISS_STACK_DURATION = 375;
private static final int EXPAND_STACK_DURATION = 225;
private static final int MINIMIZE_STACK_MAX_DURATION = 200;
private static final int SHOW_DISMISS_AFFORDANCE_DELAY = 200;
// The fraction of the stack width that the user has to drag offscreen to minimize the PIP
private static final float MINIMIZE_OFFSCREEN_FRACTION = 0.2f;
@@ -105,6 +107,16 @@ public class PipTouchHandler implements TunerService.Tunable {
}
};
private Handler mHandler = new Handler();
private Runnable mShowDismissAffordance = new Runnable() {
@Override
public void run() {
if (mEnableDragToDismiss) {
mDismissViewController.showDismissTarget(mPinnedStackBounds);
}
}
};
// Behaviour states
private boolean mIsTappingThrough;
private boolean mIsMinimized;
@@ -193,7 +205,7 @@ public class PipTouchHandler implements TunerService.Tunable {
mTouchState = new PipTouchState(mViewConfig);
mFlingAnimationUtils = new FlingAnimationUtils(context, 2f);
mGestures = new PipTouchGesture[] {
mDragToDismissGesture, mDefaultMovementGesture
mDefaultMovementGesture
};
mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler());
registerInputConsumer();
@@ -521,6 +533,9 @@ public class PipTouchHandler implements TunerService.Tunable {
private void movePinnedStack(Rect bounds) {
if (!bounds.equals(mPinnedStackBounds)) {
mPinnedStackBounds.set(bounds);
if (mEnableDragToDismiss) {
mDismissViewController.updateDismissTarget(bounds);
}
mMotionHelper.resizeToBounds(mPinnedStackBounds);
}
}
@@ -562,58 +577,26 @@ public class PipTouchHandler implements TunerService.Tunable {
return PointF.length(r1.left - r2.left, r1.top - r2.top);
}
/**
* Gesture controlling dragging over a target to dismiss the PIP.
*/
private PipTouchGesture mDragToDismissGesture = new PipTouchGesture() {
@Override
public void onDown(PipTouchState touchState) {
if (mEnableDragToDismiss) {
// TODO: Consider setting a timer such at after X time, we show the dismiss
// target if the user hasn't already dragged some distance
mDismissViewController.createDismissTarget();
}
}
@Override
boolean onMove(PipTouchState touchState) {
if (mEnableDragToDismiss && touchState.startedDragging()) {
mDismissViewController.showDismissTarget();
}
return false;
}
@Override
public boolean onUp(PipTouchState touchState) {
if (mEnableDragToDismiss) {
try {
if (touchState.isDragging()) {
Rect dismissBounds = mDismissViewController.getDismissBounds();
PointF lastTouch = touchState.getLastTouchPosition();
if (dismissBounds.contains((int) lastTouch.x, (int) lastTouch.y)) {
animateDismissPinnedStack(dismissBounds);
MetricsLogger.action(mContext,
MetricsEvent.ACTION_PICTURE_IN_PICTURE_DISMISSED,
METRIC_VALUE_DISMISSED_BY_DRAG);
return true;
}
}
} finally {
mDismissViewController.destroyDismissTarget();
}
}
return false;
}
};
/**** Gestures ****/
/**
* Gesture controlling normal movement of the PIP.
*/
private PipTouchGesture mDefaultMovementGesture = new PipTouchGesture() {
@Override
public void onDown(PipTouchState touchState) {
if (mEnableDragToDismiss) {
mDismissViewController.createDismissTarget();
mHandler.postDelayed(mShowDismissAffordance, SHOW_DISMISS_AFFORDANCE_DELAY);
}
}
@Override
boolean onMove(PipTouchState touchState) {
if (touchState.startedDragging() && mEnableDragToDismiss) {
mHandler.removeCallbacks(mShowDismissAffordance);
mDismissViewController.showDismissTarget(mPinnedStackBounds);
}
if (touchState.isDragging()) {
// Move the pinned stack freely
PointF lastDelta = touchState.getLastTouchDelta();
@@ -635,6 +618,23 @@ public class PipTouchHandler implements TunerService.Tunable {
@Override
public boolean onUp(PipTouchState touchState) {
try {
if (mEnableDragToDismiss) {
mHandler.removeCallbacks(mShowDismissAffordance);
PointF vel = mTouchState.getVelocity();
final float velocity = PointF.length(vel.x, vel.y);
if (touchState.isDragging()
&& velocity < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
if (mDismissViewController.shouldDismiss(mPinnedStackBounds)) {
Rect dismissBounds = mDismissViewController.getDismissBounds();
animateDismissPinnedStack(dismissBounds);
return true;
}
}
}
} finally {
mDismissViewController.destroyDismissTarget();
}
if (touchState.isDragging()) {
PointF vel = mTouchState.getVelocity();
if (!mIsMinimized && (shouldMinimizedPinnedStack()