Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a33bdf37 authored by Filip Gruszczynski's avatar Filip Gruszczynski
Browse files

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
parent b2d8e4f0
Loading
Loading
Loading
Loading
+23 −11
Original line number Original line Diff line number Diff line
@@ -2214,7 +2214,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
                        final float y = ev.getY(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildOrderedChildList();
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        final View[] children = mChildren;
@@ -2346,6 +2346,18 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
        return handled;
        return handled;
    }
    }


    /**
     * 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.
     * Finds the child which has accessibility focus.
     *
     *
+164 −27
Original line number Original line Diff line number Diff line
@@ -19,18 +19,24 @@ package com.android.internal.widget;
import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;


import android.content.Context;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.RemoteException;
import android.util.AttributeSet;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewOutlineProvider;
import android.view.Window;
import android.view.Window;
import android.util.Log;


import com.android.internal.R;
import com.android.internal.R;
import com.android.internal.policy.PhoneWindow;
import com.android.internal.policy.PhoneWindow;


import java.util.ArrayList;

/**
/**
 * This class represents the special screen elements to control a window on freeform
 * This class represents the special screen elements to control a window on freeform
 * environment.
 * environment.
@@ -38,8 +44,8 @@ import com.android.internal.policy.PhoneWindow;
 * <ul>
 * <ul>
 * <li>The caption, containing the system buttons like maximize, close and such as well as
 * <li>The caption, containing the system buttons like maximize, close and such as well as
 * allowing the user to drag the window around.</li>
 * allowing the user to drag the window around.</li>
 * After creating the view, the function
 * </ul>
 * {@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.
 * the connection to it's owning PhoneWindow.
 * Note: At this time the application can change various attributes of the DecorView which
 * Note: At this time the application can change various attributes of the DecorView which
 * will break things (in settle/unexpected ways):
 * will break things (in settle/unexpected ways):
@@ -48,9 +54,29 @@ import com.android.internal.policy.PhoneWindow;
 * <li>setSurfaceFormat</li>
 * <li>setSurfaceFormat</li>
 * <li>..</li>
 * <li>..</li>
 * </ul>
 * </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
public class DecorCaptionView extends ViewGroup implements View.OnTouchListener,
        implements View.OnClickListener, View.OnTouchListener {
        GestureDetector.OnGestureListener {
    private final static String TAG = "DecorCaptionView";
    private final static String TAG = "DecorCaptionView";
    private PhoneWindow mOwner = null;
    private PhoneWindow mOwner = null;
    private boolean mShow = false;
    private boolean mShow = false;
@@ -65,17 +91,42 @@ public class DecorCaptionView extends ViewGroup


    private View mCaption;
    private View mCaption;
    private View mContent;
    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) {
    public DecorCaptionView(Context context) {
        super(context);
        super(context);
        init(context);
    }
    }


    public DecorCaptionView(Context context, AttributeSet attrs) {
    public DecorCaptionView(Context context, AttributeSet attrs) {
        super(context, attrs);
        super(context, attrs);
        init(context);
    }
    }


    public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) {
    public DecorCaptionView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        super(context, attrs, defStyle);
        init(context);
    }

    private void init(Context context) {
        mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mGestureDetector = new GestureDetector(context, this);
    }
    }


    @Override
    @Override
@@ -88,13 +139,47 @@ public class DecorCaptionView extends ViewGroup
        mOwner = owner;
        mOwner = owner;
        mShow = show;
        mShow = show;
        mOverlayWithAppContent = owner.getOverlayDecorCaption();
        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();
        updateCaptionVisibility();
        // By changing the outline provider to BOUNDS, the window can remove its
        // By changing the outline provider to BOUNDS, the window can remove its
        // background without removing the shadow.
        // background without removing the shadow.
        mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
        mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
        mMaximize = findViewById(R.id.maximize_window);
        mClose = findViewById(R.id.close_window);
    }


        findViewById(R.id.maximize_window).setOnClickListener(this);
    @Override
        findViewById(R.id.close_window).setOnClickListener(this);
    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
    @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)
        // 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
        // the old input device events get cancelled first. So no need to remember the kind of
        // input device we are listening to.
        // input device we are listening to.
        final int x = (int) e.getX();
        final int y = (int) e.getY();
        switch (e.getActionMasked()) {
        switch (e.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_DOWN:
                if (!mShow) {
                if (!mShow) {
                    // When there is no caption we should not react to anything.
                    // When there is no caption we should not react to anything.
                    return false;
                    return false;
                }
                }
                // A drag action is started if we aren't dragging already and the starting event is
                // Checking for a drag action is started if we aren't dragging already and the
                // either a left mouse button or any other input device.
                // starting event is either a left mouse button or any other input device.
                if (!mDragging &&
                if (((e.getToolType(e.getActionIndex()) != MotionEvent.TOOL_TYPE_MOUSE ||
                        (e.getToolType(e.getActionIndex()) != MotionEvent.TOOL_TYPE_MOUSE ||
                        (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0))) {
                                (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0)) {
                    mCheckForDragging = true;
                    mDragging = true;
                    mTouchDownX = x;
                    mLeftMouseButtonReleased = false;
                    mTouchDownY = y;
                    startMovingTask(e.getRawX(), e.getRawY());
                }
                }
                break;
                break;


            case MotionEvent.ACTION_MOVE:
            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 &&
                    if (e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE &&
                            (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) == 0) {
                            (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) == 0) {
                        // There is no separate mouse button up call and if the user mixes mouse
                        // 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.
                // Abort the ongoing dragging.
                mDragging = false;
                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;
    }
    }


    /**
    /**
@@ -152,15 +259,6 @@ public class DecorCaptionView extends ViewGroup
        updateCaptionVisibility();
        updateCaptionVisibility();
    }
    }


    @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
    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        if (!(params instanceof MarginLayoutParams)) {
        if (!(params instanceof MarginLayoutParams)) {
@@ -205,8 +303,12 @@ public class DecorCaptionView extends ViewGroup
        if (mCaption.getVisibility() != View.GONE) {
        if (mCaption.getVisibility() != View.GONE) {
            mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight());
            mCaption.layout(0, 0, mCaption.getMeasuredWidth(), mCaption.getMeasuredHeight());
            captionHeight = mCaption.getBottom() - mCaption.getTop();
            captionHeight = mCaption.getBottom() - mCaption.getTop();
            mMaximize.getHitRect(mMaximizeRect);
            mClose.getHitRect(mCloseRect);
        } else {
        } else {
            captionHeight = 0;
            captionHeight = 0;
            mMaximizeRect.setEmpty();
            mCloseRect.setEmpty();
        }
        }


        if (mContent != null) {
        if (mContent != null) {
@@ -291,4 +393,39 @@ public class DecorCaptionView extends ViewGroup
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof MarginLayoutParams;
        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;
    }
}
}