Files
lawnchair/src/com/android/launcher3/views/BaseDragLayer.java
Tony Wickham eccf070bae Change TAPL to swipe to stash taskbar (instead of deprecated long press)
This also means Taskbar is transient by default in automated tests,
instead of persistent. Updated some checks accordingly.

Flag: LEGACY ENABLE_TRANSIENT_TASKBAR ENABLED
Test: TaskbarExpandCollapse#hideShowTaskbar; TaplTestsTaskbar;
TaplTestsTransientTaskbar; TaplTestsPersistentTaskbar
Bug: 270395798

Change-Id: Ib6e592a31a55a912a7ea991a421a9c60bca51c80
2023-11-01 23:16:10 +00:00

576 lines
23 KiB
Java

/*
* Copyright (C) 2018 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.launcher3.views;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_OUTSIDE;
import static android.view.MotionEvent.ACTION_UP;
import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
import android.content.Context;
import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Property;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.Utilities;
import com.android.launcher3.testing.shared.ResourceUtils;
import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.TouchController;
import java.io.PrintWriter;
import java.util.ArrayList;
/**
* A viewgroup with utility methods for drag-n-drop and touch interception
*/
public abstract class BaseDragLayer<T extends Context & ActivityContext>
extends InsettableFrameLayout {
public static final Property<LayoutParams, Integer> LAYOUT_X =
new Property<LayoutParams, Integer>(Integer.TYPE, "x") {
@Override
public Integer get(LayoutParams lp) {
return lp.x;
}
@Override
public void set(LayoutParams lp, Integer x) {
lp.x = x;
}
};
public static final Property<LayoutParams, Integer> LAYOUT_Y =
new Property<LayoutParams, Integer>(Integer.TYPE, "y") {
@Override
public Integer get(LayoutParams lp) {
return lp.y;
}
@Override
public void set(LayoutParams lp, Integer y) {
lp.y = y;
}
};
// Touch coming from normal view system is being dispatched.
private static final int TOUCH_DISPATCHING_FROM_VIEW = 1 << 0;
// Touch is being dispatched through the normal view dispatch system, and started at the
// system gesture region. In this case we prevent internal gesture handling and only allow
// normal view event handling.
private static final int TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION = 1 << 1;
// Touch coming from InputMonitor proxy is being dispatched 'only to gestures'. Note that both
// this and view-system can be active at the same time where view-system would go to the views,
// and this would go to the gestures.
// Note that this is not set when events are coming from proxy, but going through full dispatch
// process (both views and gestures) to allow view-system to easily take over in case it
// comes later.
private static final int TOUCH_DISPATCHING_FROM_PROXY = 1 << 2;
// ACTION_DOWN has been dispatched to child views and ACTION_UP or ACTION_CANCEL is pending.
// Note that the event source can either be view-dispatching or proxy-dispatching based on if
// TOUCH_DISPATCHING_VIEW is present or not.
private static final int TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS = 1 << 3;
protected final float[] mTmpXY = new float[2];
protected final float[] mTmpRectPoints = new float[4];
protected final Rect mHitRect = new Rect();
@ViewDebug.ExportedProperty(category = "launcher")
private final RectF mSystemGestureRegion = new RectF();
private int mTouchDispatchState = 0;
protected final T mActivity;
private final MultiValueAlpha mMultiValueAlpha;
// All the touch controllers for the view
protected TouchController[] mControllers;
// Touch controller which is currently active for the normal view dispatch
protected TouchController mActiveController;
// Touch controller which is being used for the proxy events
protected TouchController mProxyTouchController;
private TouchCompleteListener mTouchCompleteListener;
public BaseDragLayer(Context context, AttributeSet attrs, int alphaChannelCount) {
super(context, attrs);
mActivity = ActivityContext.lookupContext(context);
mMultiValueAlpha = new MultiValueAlpha(this, alphaChannelCount);
}
/**
* Called to reinitialize touch controllers.
*/
public abstract void recreateControllers();
/**
* Same as {@link #isEventOverView(View, MotionEvent, View)} where evView == this drag layer.
*/
public boolean isEventOverView(View view, MotionEvent ev) {
getDescendantRectRelativeToSelf(view, mHitRect);
return mHitRect.contains((int) ev.getX(), (int) ev.getY());
}
/**
* Given a motion event in evView's coordinates, return whether the event is within another
* view's bounds.
*/
public boolean isEventOverView(View view, MotionEvent ev, View evView) {
int[] xy = new int[] {(int) ev.getX(), (int) ev.getY()};
getDescendantCoordRelativeToSelf(evView, xy);
getDescendantRectRelativeToSelf(view, mHitRect);
return mHitRect.contains(xy[0], xy[1]);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == ACTION_UP || action == ACTION_CANCEL) {
if (mTouchCompleteListener != null) {
mTouchCompleteListener.onTouchComplete();
}
mTouchCompleteListener = null;
} else if (action == MotionEvent.ACTION_DOWN) {
mActivity.finishAutoCancelActionMode();
}
return findActiveController(ev);
}
private boolean isEventInLauncher(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
return x >= mSystemGestureRegion.left && x < getWidth() - mSystemGestureRegion.right
&& y >= mSystemGestureRegion.top && y < getHeight() - mSystemGestureRegion.bottom;
}
private TouchController findControllerToHandleTouch(MotionEvent ev) {
AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
if (topView != null
&& (isEventInLauncher(ev) || topView.canInterceptEventsInSystemGestureRegion())
&& topView.onControllerInterceptTouchEvent(ev)) {
return topView;
}
for (TouchController controller : mControllers) {
if (controller.onControllerInterceptTouchEvent(ev)) {
return controller;
}
}
return null;
}
protected boolean findActiveController(MotionEvent ev) {
mActiveController = null;
if (canFindActiveController()) {
mActiveController = findControllerToHandleTouch(ev);
}
return mActiveController != null;
}
protected boolean canFindActiveController() {
// Only look for controllers if we are not dispatching from gesture area and proxy is
// not active
return (mTouchDispatchState & (TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION
| TOUCH_DISPATCHING_FROM_PROXY)) == 0;
}
@Override
public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
// Shortcuts can appear above folder
View topView = AbstractFloatingView.getTopOpenViewWithType(mActivity,
AbstractFloatingView.TYPE_ACCESSIBLE);
if (topView != null) {
if (child == topView) {
return super.onRequestSendAccessibilityEvent(child, event);
}
// Skip propagating onRequestSendAccessibilityEvent for all other children
// which are not topView
return false;
}
return super.onRequestSendAccessibilityEvent(child, event);
}
@Override
public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) {
View topView = AbstractFloatingView.getTopOpenViewWithType(mActivity,
AbstractFloatingView.TYPE_ACCESSIBLE);
if (topView != null) {
// Only add the top view as a child for accessibility when it is open
addAccessibleChildToList(topView, childrenForAccessibility);
} else {
super.addChildrenForAccessibility(childrenForAccessibility);
}
}
protected void addAccessibleChildToList(View child, ArrayList<View> outList) {
if (child.isImportantForAccessibility()) {
outList.add(child);
} else {
child.addChildrenForAccessibility(outList);
}
}
@Override
public void onViewRemoved(View child) {
super.onViewRemoved(child);
if (child instanceof AbstractFloatingView) {
// Handles the case where the view is removed without being properly closed.
// This can happen if something goes wrong during a state change/transition.
AbstractFloatingView floatingView = (AbstractFloatingView) child;
if (floatingView.isOpen()) {
postDelayed(() -> floatingView.close(false), getSingleFrameMs(getContext()));
}
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == ACTION_UP || action == ACTION_CANCEL) {
if (mTouchCompleteListener != null) {
mTouchCompleteListener.onTouchComplete();
}
mTouchCompleteListener = null;
}
if (mActiveController != null && ev.getAction() != ACTION_OUTSIDE) {
// For some reason, once we intercept touches and have an mActiveController, we won't
// get onInterceptTouchEvent() for ACTION_OUTSIDE. Thus, we must recalculate a new
// TouchController (if any) to handle the ACTION_OUTSIDE here in onTouchEvent() as well.
return mActiveController.onControllerTouchEvent(ev);
} else {
// In case no child view handled the touch event, we may not get onIntercept anymore
return findActiveController(ev);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case ACTION_DOWN: {
if ((mTouchDispatchState & TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS) != 0) {
// Cancel the previous touch
int action = ev.getAction();
ev.setAction(ACTION_CANCEL);
super.dispatchTouchEvent(ev);
ev.setAction(action);
}
mTouchDispatchState |= TOUCH_DISPATCHING_FROM_VIEW
| TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS;
if (isEventInLauncher(ev)) {
mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION;
} else {
mTouchDispatchState |= TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION;
}
break;
}
case ACTION_CANCEL:
case ACTION_UP:
mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION;
mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_VIEW;
mTouchDispatchState &= ~TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS;
break;
}
super.dispatchTouchEvent(ev);
// We want to get all events so that mTouchDispatchSource is maintained properly
return true;
}
/**
* Proxies the touch events to the gesture handlers
*/
public boolean proxyTouchEvent(MotionEvent ev, boolean allowViewDispatch) {
int actionMasked = ev.getActionMasked();
boolean isViewDispatching = (mTouchDispatchState & TOUCH_DISPATCHING_FROM_VIEW) != 0;
// Only do view dispatch if another view-dispatching is not running, or we already started
// proxy-dispatching before. Note that view-dispatching can always take over the proxy
// dispatching at anytime, but not vice-versa.
allowViewDispatch = allowViewDispatch && !isViewDispatching
&& (actionMasked == ACTION_DOWN
|| ((mTouchDispatchState & TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS) != 0));
if (allowViewDispatch) {
mTouchDispatchState |= TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS;
super.dispatchTouchEvent(ev);
if (actionMasked == ACTION_UP || actionMasked == ACTION_CANCEL) {
mTouchDispatchState &= ~TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS;
mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_PROXY;
}
return true;
} else {
boolean handled;
if (mProxyTouchController != null) {
handled = mProxyTouchController.onControllerTouchEvent(ev);
} else {
if (actionMasked == ACTION_DOWN) {
if (isViewDispatching && mActiveController != null) {
// A controller is already active, we can't initiate our own controller
mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_PROXY;
} else {
// We will control the handler via proxy
mTouchDispatchState |= TOUCH_DISPATCHING_FROM_PROXY;
}
}
if ((mTouchDispatchState & TOUCH_DISPATCHING_FROM_PROXY) != 0) {
mProxyTouchController = findControllerToHandleTouch(ev);
}
handled = mProxyTouchController != null;
}
if (actionMasked == ACTION_UP || actionMasked == ACTION_CANCEL) {
mProxyTouchController = null;
mTouchDispatchState &= ~TOUCH_DISPATCHING_FROM_PROXY;
}
return handled;
}
}
/**
* Determine the rect of the descendant in this DragLayer's coordinates
*
* @param descendant The descendant whose coordinates we want to find.
* @param r The rect into which to place the results.
* @return The factor by which this descendant is scaled relative to this DragLayer.
*/
public float getDescendantRectRelativeToSelf(View descendant, Rect r) {
mTmpRectPoints[0] = 0;
mTmpRectPoints[1] = 0;
mTmpRectPoints[2] = descendant.getWidth();
mTmpRectPoints[3] = descendant.getHeight();
float s = getDescendantCoordRelativeToSelf(descendant, mTmpRectPoints);
r.left = Math.round(Math.min(mTmpRectPoints[0], mTmpRectPoints[2]));
r.top = Math.round(Math.min(mTmpRectPoints[1], mTmpRectPoints[3]));
r.right = Math.round(Math.max(mTmpRectPoints[0], mTmpRectPoints[2]));
r.bottom = Math.round(Math.max(mTmpRectPoints[1], mTmpRectPoints[3]));
return s;
}
public float getLocationInDragLayer(View child, int[] loc) {
loc[0] = 0;
loc[1] = 0;
return getDescendantCoordRelativeToSelf(child, loc);
}
public float getDescendantCoordRelativeToSelf(View descendant, int[] coord) {
mTmpXY[0] = coord[0];
mTmpXY[1] = coord[1];
float scale = getDescendantCoordRelativeToSelf(descendant, mTmpXY);
Utilities.roundArray(mTmpXY, coord);
return scale;
}
public float getDescendantCoordRelativeToSelf(View descendant, float[] coord) {
return getDescendantCoordRelativeToSelf(descendant, coord, false);
}
/**
* Given a coordinate relative to the descendant, find the coordinate in this DragLayer's
* coordinates.
*
* @param descendant The descendant to which the passed coordinate is relative.
* @param coord The coordinate that we want mapped.
* @param includeRootScroll Whether or not to account for the scroll of the root descendant:
* sometimes this is relevant as in a child's coordinates within the root descendant.
* @return The factor by which this descendant is scaled relative to this DragLayer. Caution
* this scale factor is assumed to be equal in X and Y, and so if at any point this
* assumption fails, we will need to return a pair of scale factors.
*/
public float getDescendantCoordRelativeToSelf(View descendant, float[] coord,
boolean includeRootScroll) {
return Utilities.getDescendantCoordRelativeToAncestor(descendant, this,
coord, includeRootScroll);
}
/**
* Similar to {@link #mapCoordInSelfToDescendant(View descendant, float[] coord)}
* but accepts a Rect instead of float[].
*/
public void mapRectInSelfToDescendant(View descendant, Rect rect) {
Utilities.mapRectInSelfToDescendant(descendant, this, rect);
}
/**
* Inverse of {@link #getDescendantCoordRelativeToSelf(View, float[])}.
*/
public void mapCoordInSelfToDescendant(View descendant, float[] coord) {
Utilities.mapCoordInSelfToDescendant(descendant, this, coord);
}
/**
* Inverse of {@link #getDescendantCoordRelativeToSelf(View, int[])}.
*/
public void mapCoordInSelfToDescendant(View descendant, int[] coord) {
mTmpXY[0] = coord[0];
mTmpXY[1] = coord[1];
Utilities.mapCoordInSelfToDescendant(descendant, this, mTmpXY);
Utilities.roundArray(mTmpXY, coord);
}
public void getViewRectRelativeToSelf(View v, Rect r) {
int[] loc = getViewLocationRelativeToSelf(v);
r.set(loc[0], loc[1], loc[0] + v.getMeasuredWidth(), loc[1] + v.getMeasuredHeight());
}
protected int[] getViewLocationRelativeToSelf(View v) {
int[] loc = new int[2];
getLocationInWindow(loc);
int x = loc[0];
int y = loc[1];
v.getLocationInWindow(loc);
loc[0] -= x;
loc[1] -= y;
return loc;
}
@Override
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
View topView = AbstractFloatingView.getTopOpenView(mActivity);
if (topView != null) {
return topView.requestFocus(direction, previouslyFocusedRect);
} else {
return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
}
@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
View topView = AbstractFloatingView.getTopOpenView(mActivity);
if (topView != null) {
topView.addFocusables(views, direction);
} else {
super.addFocusables(views, direction, focusableMode);
}
}
public void setTouchCompleteListener(TouchCompleteListener listener) {
mTouchCompleteListener = listener;
}
public interface TouchCompleteListener {
void onTouchComplete();
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
// Override to allow type-checking of LayoutParams.
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
public MultiProperty getAlphaProperty(int index) {
return mMultiValueAlpha.get(index);
}
public void dump(String prefix, PrintWriter writer) {
writer.println(prefix + "DragLayer:");
if (mActiveController != null) {
writer.println(prefix + "\tactiveController: " + mActiveController);
mActiveController.dump(prefix + "\t", writer);
}
writer.println(prefix + "\tdragLayerAlpha : " + mMultiValueAlpha );
}
public static class LayoutParams extends InsettableFrameLayout.LayoutParams {
public int x, y;
public boolean customPosition = false;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams lp) {
super(lp);
}
}
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
final FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) child.getLayoutParams();
if (flp instanceof LayoutParams) {
final LayoutParams lp = (LayoutParams) flp;
if (lp.customPosition) {
child.layout(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height);
}
}
}
}
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
if (Utilities.ATLEAST_Q) {
Insets gestureInsets = insets.getMandatorySystemGestureInsets();
int gestureInsetBottom = gestureInsets.bottom;
Insets imeInset = Utilities.ATLEAST_R
? insets.getInsets(WindowInsets.Type.ime())
: Insets.NONE;
DeviceProfile dp = mActivity.getDeviceProfile();
if (dp.isTaskbarPresent) {
// Ignore taskbar gesture insets to avoid interfering with TouchControllers.
gestureInsetBottom = ResourceUtils.getNavbarSize(
ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, getResources());
}
mSystemGestureRegion.set(
Math.max(gestureInsets.left, imeInset.left),
Math.max(gestureInsets.top, imeInset.top),
Math.max(gestureInsets.right, imeInset.right),
Math.max(gestureInsetBottom, imeInset.bottom)
);
}
return super.dispatchApplyWindowInsets(insets);
}
}