DragLayout.java revision a8b31db9656c2af194c8ff1e3062aa9667ae5da4
1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *   http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.calculator2;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.content.Context;
23import android.graphics.PointF;
24import android.graphics.Rect;
25import android.os.Bundle;
26import android.os.Parcelable;
27import android.support.v4.view.ViewCompat;
28import android.support.v4.widget.ViewDragHelper;
29import android.util.AttributeSet;
30import android.view.MotionEvent;
31import android.view.View;
32import android.view.ViewGroup;
33import android.widget.FrameLayout;
34
35import java.util.HashMap;
36import java.util.List;
37import java.util.Map;
38import java.util.concurrent.CopyOnWriteArrayList;
39
40public class DragLayout extends ViewGroup {
41
42    private static final double AUTO_OPEN_SPEED_LIMIT = 600.0;
43    private static final String KEY_IS_OPEN = "IS_OPEN";
44    private static final String KEY_SUPER_STATE = "SUPER_STATE";
45
46    private FrameLayout mHistoryFrame;
47    private ViewDragHelper mDragHelper;
48
49    // No concurrency; allow modifications while iterating.
50    private final List<DragCallback> mDragCallbacks = new CopyOnWriteArrayList<>();
51    private CloseCallback mCloseCallback;
52
53    private final Map<Integer, PointF> mLastMotionPoints = new HashMap<>();
54    private final Rect mHitRect = new Rect();
55
56    private int mVerticalRange;
57    private boolean mIsOpen;
58
59    public DragLayout(Context context, AttributeSet attrs) {
60        super(context, attrs);
61    }
62
63    @Override
64    protected void onFinishInflate() {
65        mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
66        mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame);
67        super.onFinishInflate();
68    }
69
70    @Override
71    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
72        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
73        measureChildren(widthMeasureSpec, heightMeasureSpec);
74    }
75
76    @Override
77    protected void onLayout(boolean changed, int l, int t, int r, int b) {
78        int displayHeight = 0;
79        for (DragCallback c : mDragCallbacks) {
80            displayHeight = Math.max(displayHeight, c.getDisplayHeight());
81        }
82        mVerticalRange = getHeight() - displayHeight;
83
84        final int childCount = getChildCount();
85        for (int i = 0; i < childCount; ++i) {
86            final View child = getChildAt(i);
87
88            int top = 0;
89            if (child == mHistoryFrame) {
90                if (mDragHelper.getCapturedView() == mHistoryFrame
91                        && mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) {
92                    top = child.getTop();
93                } else {
94                    top = mIsOpen ? 0 : -mVerticalRange;
95                }
96            }
97            child.layout(0, top, child.getMeasuredWidth(), top + child.getMeasuredHeight());
98        }
99    }
100
101    @Override
102    protected Parcelable onSaveInstanceState() {
103        final Bundle bundle = new Bundle();
104        bundle.putParcelable(KEY_SUPER_STATE, super.onSaveInstanceState());
105        bundle.putBoolean(KEY_IS_OPEN, mIsOpen);
106        return bundle;
107    }
108
109    @Override
110    protected void onRestoreInstanceState(Parcelable state) {
111        if (state instanceof Bundle) {
112            final Bundle bundle = (Bundle) state;
113            mIsOpen = bundle.getBoolean(KEY_IS_OPEN);
114            mHistoryFrame.setVisibility(mIsOpen ? View.VISIBLE : View.INVISIBLE);
115            for (DragCallback c : mDragCallbacks) {
116                c.onInstanceStateRestored(mIsOpen);
117            }
118
119            state = bundle.getParcelable(KEY_SUPER_STATE);
120        }
121        super.onRestoreInstanceState(state);
122    }
123
124    private void saveLastMotion(MotionEvent event) {
125        final int action = event.getActionMasked();
126        switch (action) {
127            case MotionEvent.ACTION_DOWN:
128            case MotionEvent.ACTION_POINTER_DOWN: {
129                final int actionIndex = event.getActionIndex();
130                final int pointerId = event.getPointerId(actionIndex);
131                final PointF point = new PointF(event.getX(actionIndex), event.getY(actionIndex));
132                mLastMotionPoints.put(pointerId, point);
133                break;
134            }
135            case MotionEvent.ACTION_MOVE: {
136                for (int i = event.getPointerCount() - 1; i >= 0; --i) {
137                    final int pointerId = event.getPointerId(i);
138                    final PointF point = mLastMotionPoints.get(pointerId);
139                    if (point != null) {
140                        point.set(event.getX(i), event.getY(i));
141                    }
142                }
143                break;
144            }
145            case MotionEvent.ACTION_POINTER_UP: {
146                final int actionIndex = event.getActionIndex();
147                final int pointerId = event.getPointerId(actionIndex);
148                mLastMotionPoints.remove(pointerId);
149                break;
150            }
151            case MotionEvent.ACTION_UP:
152            case MotionEvent.ACTION_CANCEL: {
153                mLastMotionPoints.clear();
154                break;
155            }
156        }
157    }
158
159    @Override
160    public boolean onInterceptTouchEvent(MotionEvent event) {
161        saveLastMotion(event);
162        return mDragHelper.shouldInterceptTouchEvent(event);
163    }
164
165    @Override
166    public boolean onTouchEvent(MotionEvent event) {
167        // Workaround: do not process the error case where multi-touch would cause a crash.
168        if (event.getActionMasked() == MotionEvent.ACTION_MOVE
169                && mDragHelper.getViewDragState() == ViewDragHelper.STATE_DRAGGING
170                && mDragHelper.getActivePointerId() != ViewDragHelper.INVALID_POINTER
171                && event.findPointerIndex(mDragHelper.getActivePointerId()) == -1) {
172            mDragHelper.cancel();
173            return false;
174        }
175
176        saveLastMotion(event);
177
178        mDragHelper.processTouchEvent(event);
179        return true;
180    }
181
182    @Override
183    public void computeScroll() {
184        if (mDragHelper.continueSettling(true)) {
185            ViewCompat.postInvalidateOnAnimation(this);
186        }
187    }
188
189    private void onStartDragging() {
190        for (DragCallback c : mDragCallbacks) {
191            c.onStartDraggingOpen();
192        }
193        mHistoryFrame.setVisibility(VISIBLE);
194    }
195
196    public boolean isViewUnder(View view, int x, int y) {
197        view.getHitRect(mHitRect);
198        offsetDescendantRectToMyCoords((View) view.getParent(), mHitRect);
199        return mHitRect.contains(x, y);
200    }
201
202    public boolean isMoving() {
203        final int draggingState = mDragHelper.getViewDragState();
204        return draggingState == ViewDragHelper.STATE_DRAGGING
205                || draggingState == ViewDragHelper.STATE_SETTLING;
206    }
207
208    public boolean isOpen() {
209        return mIsOpen;
210    }
211
212    private void setClosed() {
213        if (mIsOpen) {
214            mIsOpen = false;
215            mHistoryFrame.setVisibility(View.INVISIBLE);
216
217            if (mCloseCallback != null) {
218                mCloseCallback.onClose();
219            }
220        }
221    }
222
223    public Animator createAnimator(boolean toOpen) {
224        if (mIsOpen == toOpen) {
225            return ValueAnimator.ofFloat(0f, 1f).setDuration(0L);
226        }
227
228        mIsOpen = toOpen;
229        mHistoryFrame.setVisibility(VISIBLE);
230
231        final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
232        animator.addListener(new AnimatorListenerAdapter() {
233            @Override
234            public void onAnimationStart(Animator animation) {
235                mDragHelper.cancel();
236                mDragHelper.smoothSlideViewTo(mHistoryFrame, 0, mIsOpen ? 0 : -mVerticalRange);
237            }
238        });
239
240        return animator;
241    }
242
243    public void setCloseCallback(CloseCallback callback) {
244        mCloseCallback = callback;
245    }
246
247    public void addDragCallback(DragCallback callback) {
248        mDragCallbacks.add(callback);
249    }
250
251    public void removeDragCallback(DragCallback callback) {
252        mDragCallbacks.remove(callback);
253    }
254
255    /**
256     * Callback when the layout is closed.
257     * We use this to pop the HistoryFragment off the backstack.
258     * We can't use a method in DragCallback because we get ConcurrentModificationExceptions on
259     * mDragCallbacks when executePendingTransactions() is called for popping the fragment off the
260     * backstack.
261     */
262    public interface CloseCallback {
263        void onClose();
264    }
265
266    /**
267     * Callbacks for coordinating with the RecyclerView or HistoryFragment.
268     */
269    public interface DragCallback {
270        // Callback when a drag to open begins.
271        void onStartDraggingOpen();
272
273        // Callback in onRestoreInstanceState.
274        void onInstanceStateRestored(boolean isOpen);
275
276        // Animate the RecyclerView text.
277        void whileDragging(float yFraction);
278
279        // Whether we should allow the view to be dragged.
280        boolean shouldCaptureView(View view, int x, int y);
281
282        int getDisplayHeight();
283    }
284
285    public class DragHelperCallback extends ViewDragHelper.Callback {
286        @Override
287        public void onViewDragStateChanged(int state) {
288            // The view stopped moving.
289            if (state == ViewDragHelper.STATE_IDLE
290                    && mDragHelper.getCapturedView().getTop() < -(mVerticalRange / 2)) {
291                setClosed();
292            }
293        }
294
295        @Override
296        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
297            for (DragCallback c : mDragCallbacks) {
298                // Top is between [-mVerticalRange, 0].
299                c.whileDragging(1f + (float) top / mVerticalRange);
300            }
301        }
302
303        @Override
304        public int getViewVerticalDragRange(View child) {
305            return mVerticalRange;
306        }
307
308        @Override
309        public boolean tryCaptureView(View view, int pointerId) {
310            final PointF point = mLastMotionPoints.get(pointerId);
311            if (point == null) {
312                return false;
313            }
314
315            final int x = (int) point.x;
316            final int y = (int) point.y;
317
318            for (DragCallback c : mDragCallbacks) {
319                if (!c.shouldCaptureView(view, x, y)) {
320                    return false;
321                }
322            }
323            return true;
324        }
325
326        @Override
327        public int clampViewPositionVertical(View child, int top, int dy) {
328            return Math.max(Math.min(top, 0), -mVerticalRange);
329        }
330
331        @Override
332        public void onViewCaptured(View capturedChild, int activePointerId) {
333            super.onViewCaptured(capturedChild, activePointerId);
334
335            if (!mIsOpen) {
336                mIsOpen = true;
337                onStartDragging();
338            }
339        }
340
341        @Override
342        public void onViewReleased(View releasedChild, float xvel, float yvel) {
343            final boolean settleToOpen;
344            if (yvel > AUTO_OPEN_SPEED_LIMIT) {
345                // Speed has priority over position.
346                settleToOpen = true;
347            } else if (yvel < -AUTO_OPEN_SPEED_LIMIT) {
348                settleToOpen = false;
349            } else {
350                settleToOpen = releasedChild.getTop() > -(mVerticalRange / 2);
351            }
352
353            if (mDragHelper.settleCapturedViewAt(0, settleToOpen ? 0 : -mVerticalRange)) {
354                ViewCompat.postInvalidateOnAnimation(DragLayout.this);
355            }
356        }
357    }
358}
359