* 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;
/**
* 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;
children[i].dispatchConfigurationChanged(newConfig);
}
}
-
+
/**
* {@inheritDoc}
*/
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
- final ArrayList<View> preorderedList = buildOrderedChildList();
+ final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
}
/**
+ * 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<View> buildTouchDispatchChildList() {
+ return buildOrderedChildList();
+ }
+
+ /**
* Finds the child which has accessibility focus.
*
* @return The child that has focus.
* @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) {
/**
* <p>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.</p>
- *
+ *
* <p><strong>Note:</strong> 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.</p>
/**
* 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.
- *
+ *
* <p><strong>Note:</strong> 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.</p>
/**
* {@inheritDoc}
- *
+ *
* <p><strong>Note:</strong> 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.</p>
* <p><strong>Note:</strong> 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.</p>
- *
+ *
* @param view the view to remove from the group
*/
public void removeViewInLayout(View view) {
* <p><strong>Note:</strong> 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.</p>
- *
+ *
* @param index the position in the group of the view to remove
*/
public void removeViewAt(int index) {
/**
* Call this method to remove all child views from the
* ViewGroup.
- *
+ *
* <p><strong>Note:</strong> 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.</p>
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.
* <ul>
* <li>The caption, containing the system buttons like maximize, close and such as well as
* allowing the user to drag the window around.</li>
- * After creating the view, the function
- * {@link #setPhoneWindow} needs to be called to make
+ * </ul>
+ * 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):
* <li>setSurfaceFormat</li>
* <li>..</li>
* </ul>
+ *
+ * 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:
+ * <ul>
+ * <li>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.</li>
+ * <li>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()}.</li>
+ * <li>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.</li>
+ * </ul>
*/
-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;
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<View> 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
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
// 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
}
// Abort the ongoing dragging.
mDragging = false;
- return true;
+ return !mCheckForDragging;
}
- return mDragging;
+ return mDragging || mCheckForDragging;
+ }
+
+ @Override
+ public ArrayList<View> 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;
}
/**
}
@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(
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) {
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;
+ }
}