From a33bdf372cf045aa55db841307c64d30a50ab60d Mon Sep 17 00:00:00 2001 From: Filip Gruszczynski Date: Thu, 19 Nov 2015 18:22:16 -0800 Subject: [PATCH] Handling touch events on the caption. We need a more sophisticated touch handling to support overlaying the caption. The touch events need to be routed in following order: close/maximize buttons, application content, caption dragging. Bug: 25486369 Change-Id: I9d4e971fb055c217c0bd83f0490fb42a5c22e93b --- core/java/android/view/ViewGroup.java | 34 ++-- .../android/internal/widget/DecorCaptionView.java | 191 ++++++++++++++++++--- 2 files changed, 187 insertions(+), 38 deletions(-) diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 6812fd14ff69..11df9a3826b7 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -224,7 +224,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * NOTE: If you change the flags below make sure to reflect the changes * the DisplayList class */ - + // When set, ViewGroup invalidates only the child's rectangle // Set by default static final int FLAG_CLIP_CHILDREN = 0x1; @@ -269,7 +269,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * When set, the drawing method will call {@link #getChildDrawingOrder(int, int)} * to get the index of the child to draw for that iteration. - * + * * @hide */ protected static final int FLAG_USE_CHILD_DRAWING_ORDER = 0x400; @@ -1327,7 +1327,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager children[i].dispatchConfigurationChanged(newConfig); } } - + /** * {@inheritDoc} */ @@ -2214,7 +2214,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final float y = ev.getY(actionIndex); // Find a child that can receive the event. // Scan children from front to back. - final ArrayList preorderedList = buildOrderedChildList(); + final ArrayList preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; @@ -2347,6 +2347,18 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Provide custom ordering of views in which the touch will be dispatched. + * + * This is called within a tight loop, so you are not allowed to allocate objects, including + * the return array. Instead, you should return a pre-allocated list that will be cleared + * after the dispatch is finished. + * @hide + */ + public ArrayList buildTouchDispatchChildList() { + return buildOrderedChildList(); + } + + /** * Finds the child which has accessibility focus. * * @return The child that has focus. @@ -2787,7 +2799,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * @see #FOCUS_BEFORE_DESCENDANTS * @see #FOCUS_AFTER_DESCENDANTS * @see #FOCUS_BLOCK_DESCENDANTS - * @see #onRequestFocusInDescendants(int, android.graphics.Rect) + * @see #onRequestFocusInDescendants(int, android.graphics.Rect) */ @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { @@ -4104,7 +4116,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** *

Adds a child view. If no layout parameters are already set on the child, the * default parameters for this ViewGroup are set on the child.

- * + * *

Note: do not invoke this method from * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)}, * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.

@@ -4120,7 +4132,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * Adds a child view. If no layout parameters are already set on the child, the * default parameters for this ViewGroup are set on the child. - * + * *

Note: do not invoke this method from * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)}, * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.

@@ -4560,7 +4572,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * {@inheritDoc} - * + * *

Note: do not invoke this method from * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)}, * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.

@@ -4579,7 +4591,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager *

Note: do not invoke this method from * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)}, * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.

- * + * * @param view the view to remove from the group */ public void removeViewInLayout(View view) { @@ -4607,7 +4619,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager *

Note: do not invoke this method from * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)}, * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.

- * + * * @param index the position in the group of the view to remove */ public void removeViewAt(int index) { @@ -4796,7 +4808,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * Call this method to remove all child views from the * ViewGroup. - * + * *

Note: do not invoke this method from * {@link #draw(android.graphics.Canvas)}, {@link #onDraw(android.graphics.Canvas)}, * {@link #dispatchDraw(android.graphics.Canvas)} or any related method.

diff --git a/core/java/com/android/internal/widget/DecorCaptionView.java b/core/java/com/android/internal/widget/DecorCaptionView.java index 16e829616704..d747686ca762 100644 --- a/core/java/com/android/internal/widget/DecorCaptionView.java +++ b/core/java/com/android/internal/widget/DecorCaptionView.java @@ -19,18 +19,24 @@ package com.android.internal.widget; import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID; import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; import android.os.RemoteException; import android.util.AttributeSet; +import android.util.Log; +import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.Window; -import android.util.Log; import com.android.internal.R; import com.android.internal.policy.PhoneWindow; +import java.util.ArrayList; + /** * This class represents the special screen elements to control a window on freeform * environment. @@ -38,8 +44,8 @@ import com.android.internal.policy.PhoneWindow; *
    *
  • The caption, containing the system buttons like maximize, close and such as well as * allowing the user to drag the window around.
  • - * After creating the view, the function - * {@link #setPhoneWindow} needs to be called to make + *
+ * After creating the view, the function {@link #setPhoneWindow} needs to be called to make * the connection to it's owning PhoneWindow. * Note: At this time the application can change various attributes of the DecorView which * will break things (in settle/unexpected ways): @@ -48,9 +54,29 @@ import com.android.internal.policy.PhoneWindow; *
  • setSurfaceFormat
  • *
  • ..
  • * + * + * Although this ViewGroup has only two direct sub-Views, its behavior is more complex due to + * overlaying caption on the content and drawing. + * + * First, no matter where the content View gets added, it will always be the first child and the + * caption will be the second. This way the caption will always be drawn on top of the content when + * overlaying is enabled. + * + * Second, the touch dispatch is customized to handle overlaying. This is what happens when touch + * is dispatched on the caption area while overlaying it on content: + *
      + *
    • DecorCaptionView.onInterceptTouchEvent() will try intercepting the touch events if the + * down action is performed on top close or maximize buttons; the reason for that is we want these + * buttons to always work.
    • + *
    • The content View will receive the touch event. Mind that content is actually underneath the + * caption, so we need to introduce our own dispatch ordering. We achieve this by overriding + * {@link #buildTouchDispatchChildList()}.
    • + *
    • If the touch event is not consumed by the content View, it will go to the caption View + * and the dragging logic will be executed.
    • + *
    */ -public class DecorCaptionView extends ViewGroup - implements View.OnClickListener, View.OnTouchListener { +public class DecorCaptionView extends ViewGroup implements View.OnTouchListener, + GestureDetector.OnGestureListener { private final static String TAG = "DecorCaptionView"; private PhoneWindow mOwner = null; private boolean mShow = false; @@ -65,17 +91,42 @@ public class DecorCaptionView extends ViewGroup private View mCaption; private View mContent; + private View mMaximize; + private View mClose; + + // Fields for detecting drag events. + private int mTouchDownX; + private int mTouchDownY; + private boolean mCheckForDragging; + private int mDragSlop; + + // Fields for detecting and intercepting click events on close/maximize. + private ArrayList mTouchDispatchList = new ArrayList<>(2); + // We use the gesture detector to detect clicks on close/maximize buttons and to be consistent + // with existing click detection. + private GestureDetector mGestureDetector; + private final Rect mCloseRect = new Rect(); + private final Rect mMaximizeRect = new Rect(); + private View mClickTarget; public DecorCaptionView(Context context) { super(context); + init(context); } public DecorCaptionView(Context context, AttributeSet attrs) { super(context, attrs); + init(context); } public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mGestureDetector = new GestureDetector(context, this); } @Override @@ -88,13 +139,47 @@ public class DecorCaptionView extends ViewGroup mOwner = owner; mShow = show; mOverlayWithAppContent = owner.getOverlayDecorCaption(); + if (mOverlayWithAppContent) { + // The caption is covering the content, so we make its background transparent to make + // the content visible. + mCaption.setBackgroundColor(Color.TRANSPARENT); + } updateCaptionVisibility(); // By changing the outline provider to BOUNDS, the window can remove its // background without removing the shadow. mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS); + mMaximize = findViewById(R.id.maximize_window); + mClose = findViewById(R.id.close_window); + } - findViewById(R.id.maximize_window).setOnClickListener(this); - findViewById(R.id.close_window).setOnClickListener(this); + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + // If the user starts touch on the maximize/close buttons, we immediately intercept, so + // that these buttons are always clickable. + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + if (mMaximizeRect.contains(x, y)) { + mClickTarget = mMaximize; + } + if (mCloseRect.contains(x, y)) { + mClickTarget = mClose; + } + } + return mClickTarget != null; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mClickTarget != null) { + mGestureDetector.onTouchEvent(event); + final int action = event.getAction(); + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mClickTarget = null; + } + return true; + } + return false; } @Override @@ -102,25 +187,31 @@ public class DecorCaptionView extends ViewGroup // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch) // the old input device events get cancelled first. So no need to remember the kind of // input device we are listening to. + final int x = (int) e.getX(); + final int y = (int) e.getY(); switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: if (!mShow) { // When there is no caption we should not react to anything. return false; } - // A drag action is started if we aren't dragging already and the starting event is - // either a left mouse button or any other input device. - if (!mDragging && - (e.getToolType(e.getActionIndex()) != MotionEvent.TOOL_TYPE_MOUSE || - (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0)) { - mDragging = true; - mLeftMouseButtonReleased = false; - startMovingTask(e.getRawX(), e.getRawY()); + // Checking for a drag action is started if we aren't dragging already and the + // starting event is either a left mouse button or any other input device. + if (((e.getToolType(e.getActionIndex()) != MotionEvent.TOOL_TYPE_MOUSE || + (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0))) { + mCheckForDragging = true; + mTouchDownX = x; + mTouchDownY = y; } break; case MotionEvent.ACTION_MOVE: - if (mDragging && !mLeftMouseButtonReleased) { + if (!mDragging && mCheckForDragging && passedSlop(x, y)) { + mCheckForDragging = false; + mDragging = true; + mLeftMouseButtonReleased = false; + startMovingTask(e.getRawX(), e.getRawY()); + } else if (mDragging && !mLeftMouseButtonReleased) { if (e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE && (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) == 0) { // There is no separate mouse button up call and if the user mixes mouse @@ -138,9 +229,25 @@ public class DecorCaptionView extends ViewGroup } // Abort the ongoing dragging. mDragging = false; - return true; + return !mCheckForDragging; } - return mDragging; + return mDragging || mCheckForDragging; + } + + @Override + public ArrayList buildTouchDispatchChildList() { + mTouchDispatchList.ensureCapacity(3); + if (mCaption != null) { + mTouchDispatchList.add(mCaption); + } + if (mContent != null) { + mTouchDispatchList.add(mContent); + } + return mTouchDispatchList; + } + + private boolean passedSlop(int x, int y) { + return Math.abs(x - mTouchDownX) > mDragSlop || Math.abs(y - mTouchDownY) > mDragSlop; } /** @@ -153,15 +260,6 @@ public class DecorCaptionView extends ViewGroup } @Override - public void onClick(View view) { - if (view.getId() == R.id.maximize_window) { - maximizeWindow(); - } else if (view.getId() == R.id.close_window) { - mOwner.dispatchOnWindowDismissed(true /*finishTask*/); - } - } - - @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (!(params instanceof MarginLayoutParams)) { throw new IllegalArgumentException( @@ -205,8 +303,12 @@ public class DecorCaptionView extends ViewGroup if (mCaption.getVisibility() != View.GONE) { mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight()); captionHeight = mCaption.getBottom() - mCaption.getTop(); + mMaximize.getHitRect(mMaximizeRect); + mClose.getHitRect(mCloseRect); } else { captionHeight = 0; + mMaximizeRect.setEmpty(); + mCloseRect.setEmpty(); } if (mContent != null) { @@ -291,4 +393,39 @@ public class DecorCaptionView extends ViewGroup protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof MarginLayoutParams; } + + @Override + public boolean onDown(MotionEvent e) { + return false; + } + + @Override + public void onShowPress(MotionEvent e) { + + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (mClickTarget == mMaximize) { + maximizeWindow(); + } else if (mClickTarget == mClose) { + mOwner.dispatchOnWindowDismissed(true /*finishTask*/); + } + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + return false; + } + + @Override + public void onLongPress(MotionEvent e) { + + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + return false; + } } -- 2.11.0