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:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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. -->
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user