Merge "Merge "Double tap to expand PiP." into oc-mr1-dev am: 2a6f25b995" into oc-mr1-dev-plus-aosp
This commit is contained in:
committed by
Android (Google) Code Review
commit
aa960edb78
@@ -55,6 +55,7 @@ import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.View.OnTouchListener;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
@@ -119,6 +120,7 @@ public class PipMenuActivity extends Activity {
|
||||
}
|
||||
};
|
||||
|
||||
private PipTouchState mTouchState;
|
||||
private PointF mDownPosition = new PointF();
|
||||
private PointF mDownDelta = new PointF();
|
||||
private ViewConfiguration mViewConfig;
|
||||
@@ -175,6 +177,13 @@ public class PipMenuActivity extends Activity {
|
||||
// Set the flags to allow us to watch for outside touches and also hide the menu and start
|
||||
// manipulating the PIP in the same touch gesture
|
||||
mViewConfig = ViewConfiguration.get(this);
|
||||
mTouchState = new PipTouchState(mViewConfig, mHandler, () -> {
|
||||
if (mMenuState == MENU_STATE_CLOSE) {
|
||||
showPipMenu();
|
||||
} else {
|
||||
expandPip();
|
||||
}
|
||||
});
|
||||
getWindow().addFlags(LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | LayoutParams.FLAG_SLIPPERY);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -186,12 +195,28 @@ public class PipMenuActivity extends Activity {
|
||||
mViewRoot.setBackground(mBackgroundDrawable);
|
||||
mMenuContainer = findViewById(R.id.menu_container);
|
||||
mMenuContainer.setAlpha(0);
|
||||
mMenuContainer.setOnClickListener((v) -> {
|
||||
if (mMenuState == MENU_STATE_CLOSE) {
|
||||
showPipMenu();
|
||||
} else {
|
||||
expandPip();
|
||||
mMenuContainer.setOnTouchListener((v, event) -> {
|
||||
mTouchState.onTouchEvent(event);
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (mTouchState.isDoubleTap() || mMenuState == MENU_STATE_FULL) {
|
||||
// Expand to fullscreen if this is a double tap or we are already expanded
|
||||
expandPip();
|
||||
} else if (!mTouchState.isWaitingForDoubleTap()) {
|
||||
// User has stalled long enough for this not to be a drag or a double tap,
|
||||
// just expand the menu if necessary
|
||||
if (mMenuState == MENU_STATE_CLOSE) {
|
||||
showPipMenu();
|
||||
}
|
||||
} else {
|
||||
// Next touch event _may_ be the second tap for the double-tap, schedule a
|
||||
// fallback runnable to trigger the menu if no touch event occurs before the
|
||||
// next tap
|
||||
mTouchState.scheduleDoubleTapTimeoutCallback();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
mDismissButton = findViewById(R.id.dismiss);
|
||||
mDismissButton.setAlpha(0);
|
||||
|
||||
@@ -211,6 +211,10 @@ public class PipMenuActivityController {
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
public boolean isMenuActivityVisible() {
|
||||
return mToActivityMessenger != null;
|
||||
}
|
||||
|
||||
public void onActivityPinned() {
|
||||
if (mMenuState == MENU_STATE_NONE) {
|
||||
// If the menu is not visible, then re-register the input consumer if it is not already
|
||||
|
||||
@@ -187,13 +187,15 @@ public class PipTouchHandler {
|
||||
mMenuController.addListener(mMenuListener);
|
||||
mDismissViewController = new PipDismissViewController(context);
|
||||
mSnapAlgorithm = new PipSnapAlgorithm(mContext);
|
||||
mTouchState = new PipTouchState(mViewConfig);
|
||||
mFlingAnimationUtils = new FlingAnimationUtils(context, 2.5f);
|
||||
mGestures = new PipTouchGesture[] {
|
||||
mDefaultMovementGesture
|
||||
};
|
||||
mMotionHelper = new PipMotionHelper(mContext, mActivityManager, mMenuController,
|
||||
mSnapAlgorithm, mFlingAnimationUtils);
|
||||
mTouchState = new PipTouchState(mViewConfig, mHandler,
|
||||
() -> mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
|
||||
mMovementBounds, true /* allowMenuTimeout */, willResizeMenu()));
|
||||
|
||||
Resources res = context.getResources();
|
||||
mExpandedShortestEdgeSize = res.getDimensionPixelSize(
|
||||
@@ -429,7 +431,7 @@ public class PipTouchHandler {
|
||||
final float distance = bounds.bottom - target;
|
||||
fraction = Math.min(distance / bounds.height(), 1f);
|
||||
}
|
||||
if (Float.compare(fraction, 0f) != 0 || mMenuState != MENU_STATE_NONE) {
|
||||
if (Float.compare(fraction, 0f) != 0 || mMenuController.isMenuActivityVisible()) {
|
||||
// Update if the fraction > 0, or if fraction == 0 and the menu was already visible
|
||||
mMenuController.setDismissFraction(fraction);
|
||||
}
|
||||
@@ -730,8 +732,20 @@ public class PipTouchHandler {
|
||||
null /* animatorListener */);
|
||||
setMinimizedStateInternal(false);
|
||||
} else if (mMenuState != MENU_STATE_FULL) {
|
||||
mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
|
||||
mMovementBounds, true /* allowMenuTimeout */, willResizeMenu());
|
||||
if (mTouchState.isDoubleTap()) {
|
||||
// Expand to fullscreen if this is a double tap
|
||||
mMotionHelper.expandPip();
|
||||
} else if (!mTouchState.isWaitingForDoubleTap()) {
|
||||
// User has stalled long enough for this not to be a drag or a double tap, just
|
||||
// expand the menu
|
||||
mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
|
||||
mMovementBounds, true /* allowMenuTimeout */, willResizeMenu());
|
||||
} else {
|
||||
// Next touch event _may_ be the second tap for the double-tap, schedule a
|
||||
// fallback runnable to trigger the menu if no touch event occurs before the
|
||||
// next tap
|
||||
mTouchState.scheduleDoubleTapTimeoutCallback();
|
||||
}
|
||||
} else {
|
||||
mMenuController.hideMenu();
|
||||
mMotionHelper.expandPip();
|
||||
|
||||
@@ -17,11 +17,15 @@
|
||||
package com.android.systemui.pip.phone;
|
||||
|
||||
import android.graphics.PointF;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.VelocityTracker;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
@@ -31,9 +35,17 @@ public class PipTouchState {
|
||||
private static final String TAG = "PipTouchHandler";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private ViewConfiguration mViewConfig;
|
||||
@VisibleForTesting
|
||||
static final long DOUBLE_TAP_TIMEOUT = 200;
|
||||
|
||||
private final Handler mHandler;
|
||||
private final ViewConfiguration mViewConfig;
|
||||
private final Runnable mDoubleTapTimeoutCallback;
|
||||
|
||||
private VelocityTracker mVelocityTracker;
|
||||
private long mDownTouchTime = 0;
|
||||
private long mLastDownTouchTime = 0;
|
||||
private long mUpTouchTime = 0;
|
||||
private final PointF mDownTouch = new PointF();
|
||||
private final PointF mDownDelta = new PointF();
|
||||
private final PointF mLastTouch = new PointF();
|
||||
@@ -41,13 +53,22 @@ public class PipTouchState {
|
||||
private final PointF mVelocity = new PointF();
|
||||
private boolean mAllowTouches = true;
|
||||
private boolean mIsUserInteracting = false;
|
||||
// Set to true only if the multiple taps occur within the double tap timeout
|
||||
private boolean mIsDoubleTap = false;
|
||||
// Set to true only if a gesture
|
||||
private boolean mIsWaitingForDoubleTap = false;
|
||||
private boolean mIsDragging = false;
|
||||
// The previous gesture was a drag
|
||||
private boolean mPreviouslyDragging = false;
|
||||
private boolean mStartedDragging = false;
|
||||
private boolean mAllowDraggingOffscreen = false;
|
||||
private int mActivePointerId;
|
||||
|
||||
public PipTouchState(ViewConfiguration viewConfig) {
|
||||
public PipTouchState(ViewConfiguration viewConfig, Handler handler,
|
||||
Runnable doubleTapTimeoutCallback) {
|
||||
mViewConfig = viewConfig;
|
||||
mHandler = handler;
|
||||
mDoubleTapTimeoutCallback = doubleTapTimeoutCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,6 +102,14 @@ public class PipTouchState {
|
||||
mDownTouch.set(mLastTouch);
|
||||
mAllowDraggingOffscreen = true;
|
||||
mIsUserInteracting = true;
|
||||
mDownTouchTime = ev.getEventTime();
|
||||
mIsDoubleTap = !mPreviouslyDragging &&
|
||||
(mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT;
|
||||
mIsWaitingForDoubleTap = false;
|
||||
mLastDownTouchTime = mDownTouchTime;
|
||||
if (mDoubleTapTimeoutCallback != null) {
|
||||
mHandler.removeCallbacks(mDoubleTapTimeoutCallback);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
@@ -155,7 +184,11 @@ public class PipTouchState {
|
||||
break;
|
||||
}
|
||||
|
||||
mUpTouchTime = ev.getEventTime();
|
||||
mLastTouch.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
|
||||
mPreviouslyDragging = mIsDragging;
|
||||
mIsWaitingForDoubleTap = !mIsDoubleTap && !mIsDragging &&
|
||||
(mUpTouchTime - mDownTouchTime) < DOUBLE_TAP_TIMEOUT;
|
||||
|
||||
// Fall through to clean up
|
||||
}
|
||||
@@ -251,6 +284,39 @@ public class PipTouchState {
|
||||
return mAllowDraggingOffscreen;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether this gesture is a double-tap.
|
||||
*/
|
||||
public boolean isDoubleTap() {
|
||||
return mIsDoubleTap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether this gesture will potentially lead to a following double-tap.
|
||||
*/
|
||||
public boolean isWaitingForDoubleTap() {
|
||||
return mIsWaitingForDoubleTap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the callback to run if the next double tap does not occur. Only runs if
|
||||
* isWaitingForDoubleTap() is true.
|
||||
*/
|
||||
public void scheduleDoubleTapTimeoutCallback() {
|
||||
if (mIsWaitingForDoubleTap) {
|
||||
long delay = getDoubleTapTimeoutCallbackDelay();
|
||||
mHandler.removeCallbacks(mDoubleTapTimeoutCallback);
|
||||
mHandler.postDelayed(mDoubleTapTimeoutCallback, delay);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting long getDoubleTapTimeoutCallbackDelay() {
|
||||
if (mIsWaitingForDoubleTap) {
|
||||
return Math.max(0, DOUBLE_TAP_TIMEOUT - (mUpTouchTime - mDownTouchTime));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void initOrResetVelocityTracker() {
|
||||
if (mVelocityTracker == null) {
|
||||
mVelocityTracker = VelocityTracker.obtain();
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
package com.android.systemui.pip.phone;
|
||||
|
||||
import static android.view.MotionEvent.ACTION_DOWN;
|
||||
import static android.view.MotionEvent.ACTION_MOVE;
|
||||
import static android.view.MotionEvent.ACTION_UP;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.support.test.filters.SmallTest;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import com.android.systemui.SysuiTestCase;
|
||||
import com.android.systemui.pip.phone.PipTouchState;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@SmallTest
|
||||
public class PipTouchStateTest extends SysuiTestCase {
|
||||
|
||||
private Handler mHandler;
|
||||
private HandlerThread mHandlerThread;
|
||||
private PipTouchState mTouchState;
|
||||
private CountDownLatch mDoubleTapCallbackTriggeredLatch;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mHandlerThread = new HandlerThread("PipTouchStateTestThread");
|
||||
mHandlerThread.start();
|
||||
mHandler = new Handler(mHandlerThread.getLooper());
|
||||
|
||||
mDoubleTapCallbackTriggeredLatch = new CountDownLatch(1);
|
||||
mTouchState = new PipTouchState(ViewConfiguration.get(getContext()),
|
||||
mHandler, () -> {
|
||||
mDoubleTapCallbackTriggeredLatch.countDown();
|
||||
});
|
||||
assertFalse(mTouchState.isDoubleTap());
|
||||
assertFalse(mTouchState.isWaitingForDoubleTap());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleTapLongSingleTap_notDoubleTapAndNotWaiting() {
|
||||
final long currentTime = SystemClock.uptimeMillis();
|
||||
|
||||
mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
|
||||
mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
|
||||
currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT + 10, 0, 0));
|
||||
assertFalse(mTouchState.isDoubleTap());
|
||||
assertFalse(mTouchState.isWaitingForDoubleTap());
|
||||
assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleTapTimeout_timeoutCallbackCalled() throws Exception {
|
||||
final long currentTime = SystemClock.uptimeMillis();
|
||||
|
||||
mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
|
||||
mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
|
||||
currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0));
|
||||
assertFalse(mTouchState.isDoubleTap());
|
||||
assertTrue(mTouchState.isWaitingForDoubleTap());
|
||||
|
||||
assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == 10);
|
||||
mTouchState.scheduleDoubleTapTimeoutCallback();
|
||||
mDoubleTapCallbackTriggeredLatch.await(1, TimeUnit.SECONDS);
|
||||
assertTrue(mDoubleTapCallbackTriggeredLatch.getCount() == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleTapDrag_doubleTapCanceled() {
|
||||
final long currentTime = SystemClock.uptimeMillis();
|
||||
|
||||
mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
|
||||
mTouchState.onTouchEvent(createMotionEvent(ACTION_MOVE, currentTime + 10, 500, 500));
|
||||
mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 20, 500, 500));
|
||||
assertTrue(mTouchState.isDragging());
|
||||
assertFalse(mTouchState.isDoubleTap());
|
||||
assertFalse(mTouchState.isWaitingForDoubleTap());
|
||||
assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleTap_doubleTapRegistered() {
|
||||
final long currentTime = SystemClock.uptimeMillis();
|
||||
|
||||
mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0));
|
||||
mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 10, 0, 0));
|
||||
mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN,
|
||||
currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 20, 0, 0));
|
||||
mTouchState.onTouchEvent(createMotionEvent(ACTION_UP,
|
||||
currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0));
|
||||
assertTrue(mTouchState.isDoubleTap());
|
||||
assertFalse(mTouchState.isWaitingForDoubleTap());
|
||||
assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1);
|
||||
}
|
||||
|
||||
private MotionEvent createMotionEvent(int action, long eventTime, float x, float y) {
|
||||
return MotionEvent.obtain(0, eventTime, action, x, y, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user