summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java
blob: 31f09699098a402eefce45629908a20f8d578f32 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
package com.android.launcher3.shortcuts;

import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;

import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CheckLongPressHelper;
import com.android.launcher3.Launcher;
import com.android.launcher3.Utilities;
import com.android.launcher3.dragndrop.DragLayer;

/**
 * A {@link android.view.View.OnTouchListener} that creates a {@link DeepShortcutsContainer} and
 * forwards touch events to it. This listener should be put on any icon that supports shortcuts.
 */
public class ShortcutsContainerListener implements View.OnTouchListener,
        View.OnAttachStateChangeListener {

    /** Scaled touch slop, used for detecting movement outside bounds. */
    private final float mScaledTouchSlop;

    /** Timeout before accepting a long-press to start forwarding. */
    private final int mLongPressTimeout;

    /** Source view from which events are forwarded. */
    private final BubbleTextView mSrcIcon;

    /** Runnable used to trigger forwarding on long-press. */
    private Runnable mTriggerLongPress;

    /** Whether this listener is currently forwarding touch events. */
    private boolean mForwarding;

    /** The id of the first pointer down in the current event stream. */
    private int mActivePointerId;

    private Launcher mLauncher;
    private DragLayer mDragLayer;
    /** The coordinates of the touch down, relative to the shortcuts container. */
    private final int[] mTouchDown;
    private boolean mHasMappedTouchDownToContainerCoord;

    /** If true, the gesture is not handled. The value is reset when next gesture starts. */
    private boolean mIgnoreCurrentGesture;
    private DeepShortcutsContainer mShortcutsContainer;

    public ShortcutsContainerListener(BubbleTextView icon) {
        mSrcIcon = icon;
        mScaledTouchSlop = ViewConfiguration.get(icon.getContext()).getScaledTouchSlop();

        mLongPressTimeout = CheckLongPressHelper.DEFAULT_LONG_PRESS_TIMEOUT;

        icon.addOnAttachStateChangeListener(this);

        mLauncher = Launcher.getLauncher(mSrcIcon.getContext());
        mDragLayer = mLauncher.getDragLayer();
        mTouchDown = new int[2];
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            // There are no shortcuts associated with this item,
            // so return to normal touch handling.
            mIgnoreCurrentGesture = !mSrcIcon.hasDeepShortcuts();

            mTouchDown[0] = (int) event.getX();
            mTouchDown[1] = (int) event.getY();
            mDragLayer.getDescendantCoordRelativeToSelf(mSrcIcon, mTouchDown);
            mHasMappedTouchDownToContainerCoord = false;
        }

        if (mIgnoreCurrentGesture) {
            return false;
        }

        final boolean wasForwarding = mForwarding;
        final boolean forwarding;
        if (wasForwarding) {
            forwarding = onTouchForwarded(event) || !onForwardingStopped();
        } else {
            forwarding = onTouchObserved(event) && onForwardingStarted();

            if (forwarding) {
                // Make sure we cancel any ongoing source event stream.
                final long now = SystemClock.uptimeMillis();
                final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL,
                        0.0f, 0.0f, 0);
                mSrcIcon.onTouchEvent(e);
                e.recycle();
            }
        }

        mForwarding = forwarding;
        return forwarding || wasForwarding;
    }

    @Override
    public void onViewAttachedToWindow(View v) {
    }

    @Override
    public void onViewDetachedFromWindow(View v) {
        mForwarding = false;
        mActivePointerId = MotionEvent.INVALID_POINTER_ID;
    }

    /**
     * Called when forwarding would like to start.
     * <p>
     * This is when we populate the shortcuts container and add it to the DragLayer.
     *
     * @return true to start forwarding, false otherwise
     */
    protected boolean onForwardingStarted() {
        mShortcutsContainer = DeepShortcutsContainer.showForIcon(mSrcIcon);
        return mShortcutsContainer != null;
    }

    /**
     * Called when forwarding would like to stop.
     *
     * @return true to stop forwarding, false otherwise
     */
    protected boolean onForwardingStopped() {
        mShortcutsContainer = null;
        return true;
    }

    /**
     * Observes motion events and determines when to start forwarding.
     *
     * @param srcEvent motion event in source view coordinates
     * @return true to start forwarding motion events, false otherwise
     */
    private boolean onTouchObserved(MotionEvent srcEvent) {
        final View src = mSrcIcon;
        if (!src.isEnabled()) {
            return false;
        }

        final int actionMasked = srcEvent.getActionMasked();
        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN:
                mActivePointerId = srcEvent.getPointerId(0);

                if (mTriggerLongPress == null) {
                    mTriggerLongPress = new TriggerLongPress();
                }
                src.postDelayed(mTriggerLongPress, mLongPressTimeout);
                break;
            case MotionEvent.ACTION_MOVE:
                final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
                if (activePointerIndex >= 0) {
                    final float x = srcEvent.getX(activePointerIndex);
                    final float y = srcEvent.getY(activePointerIndex);

                    // Has the pointer moved outside of the view?
                    if (!Utilities.pointInView(src, x, y, mScaledTouchSlop)) {
                        clearCallbacks();

                        return false;
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                clearCallbacks();
                break;
        }

        return false;
    }

    private void clearCallbacks() {
        if (mTriggerLongPress != null) {
            mSrcIcon.removeCallbacks(mTriggerLongPress);
        }
    }

    private void onLongPress() {
        clearCallbacks();

        final BubbleTextView src = mSrcIcon;
        if (!src.isEnabled() || !src.hasDeepShortcuts()) {
            // Ignore long-press if the view is disabled or doesn't have shortcuts.
            return;
        }

        if (!onForwardingStarted()) {
            return;
        }

        // Don't let the parent intercept our events.
        src.getParent().requestDisallowInterceptTouchEvent(true);

        // Make sure we cancel any ongoing source event stream.
        final long now = SystemClock.uptimeMillis();
        final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
        src.onTouchEvent(e);
        e.recycle();

        mForwarding = true;
    }

    /**
     * Handles forwarded motion events and determines when to stop
     * forwarding.
     *
     * @param srcEvent motion event in source view coordinates
     * @return true to continue forwarding motion events, false to cancel
     */
    private boolean onTouchForwarded(MotionEvent srcEvent) {
        final View src = mSrcIcon;
        final DeepShortcutsContainer dst = mShortcutsContainer;
        if (dst == null) {
            return false;
        }
        // Always cancel forwarding when the touch stream ends.
        final int action = srcEvent.getActionMasked();
        final boolean keepForwarding = action != MotionEvent.ACTION_UP
                && action != MotionEvent.ACTION_CANCEL;
        if (!dst.isLaidOut()) {
            return keepForwarding;
        }

        // Convert event to destination-local coordinates.
        final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
        Utilities.translateEventCoordinates(src, dst, dstEvent);

        // Convert touch down event to destination-local coordinates.
        if (!mHasMappedTouchDownToContainerCoord) {
            mDragLayer.mapCoordInSelfToDescendent(dst, mTouchDown);
            mHasMappedTouchDownToContainerCoord = true;
        }

        // Forward converted event to destination view, then recycle it.
        final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId, mTouchDown);
        dstEvent.recycle();

        return handled && keepForwarding;
    }

    private class TriggerLongPress implements Runnable {
        @Override
        public void run() {
            onLongPress();
        }
    }
}