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 
17 package com.android.calculator2;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.content.Context;
23 import android.graphics.PointF;
24 import android.graphics.Rect;
25 import android.os.Bundle;
26 import android.os.Parcelable;
27 import android.support.v4.view.ViewCompat;
28 import android.support.v4.widget.ViewDragHelper;
29 import android.util.AttributeSet;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.widget.FrameLayout;
34 
35 import java.util.HashMap;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.concurrent.CopyOnWriteArrayList;
39 
40 public 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 
DragLayout(Context context, AttributeSet attrs)59     public DragLayout(Context context, AttributeSet attrs) {
60         super(context, attrs);
61     }
62 
63     @Override
onFinishInflate()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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)71     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
72         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
73         measureChildren(widthMeasureSpec, heightMeasureSpec);
74     }
75 
76     @Override
onLayout(boolean changed, int l, int t, int r, int b)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
onSaveInstanceState()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
onRestoreInstanceState(Parcelable state)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 
saveLastMotion(MotionEvent event)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
onInterceptTouchEvent(MotionEvent event)160     public boolean onInterceptTouchEvent(MotionEvent event) {
161         saveLastMotion(event);
162         return mDragHelper.shouldInterceptTouchEvent(event);
163     }
164 
165     @Override
onTouchEvent(MotionEvent event)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
computeScroll()183     public void computeScroll() {
184         if (mDragHelper.continueSettling(true)) {
185             ViewCompat.postInvalidateOnAnimation(this);
186         }
187     }
188 
onStartDragging()189     private void onStartDragging() {
190         for (DragCallback c : mDragCallbacks) {
191             c.onStartDraggingOpen();
192         }
193         mHistoryFrame.setVisibility(VISIBLE);
194     }
195 
isViewUnder(View view, int x, int y)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 
isMoving()202     public boolean isMoving() {
203         final int draggingState = mDragHelper.getViewDragState();
204         return draggingState == ViewDragHelper.STATE_DRAGGING
205                 || draggingState == ViewDragHelper.STATE_SETTLING;
206     }
207 
isOpen()208     public boolean isOpen() {
209         return mIsOpen;
210     }
211 
setClosed()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 
createAnimator(boolean toOpen)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 
setCloseCallback(CloseCallback callback)243     public void setCloseCallback(CloseCallback callback) {
244         mCloseCallback = callback;
245     }
246 
addDragCallback(DragCallback callback)247     public void addDragCallback(DragCallback callback) {
248         mDragCallbacks.add(callback);
249     }
250 
removeDragCallback(DragCallback callback)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 {
onClose()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.
onStartDraggingOpen()271         void onStartDraggingOpen();
272 
273         // Callback in onRestoreInstanceState.
onInstanceStateRestored(boolean isOpen)274         void onInstanceStateRestored(boolean isOpen);
275 
276         // Animate the RecyclerView text.
whileDragging(float yFraction)277         void whileDragging(float yFraction);
278 
279         // Whether we should allow the view to be dragged.
shouldCaptureView(View view, int x, int y)280         boolean shouldCaptureView(View view, int x, int y);
281 
getDisplayHeight()282         int getDisplayHeight();
283     }
284 
285     public class DragHelperCallback extends ViewDragHelper.Callback {
286         @Override
onViewDragStateChanged(int state)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
onViewPositionChanged(View changedView, int left, int top, int dx, int dy)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
getViewVerticalDragRange(View child)304         public int getViewVerticalDragRange(View child) {
305             return mVerticalRange;
306         }
307 
308         @Override
tryCaptureView(View view, int pointerId)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
clampViewPositionVertical(View child, int top, int dy)327         public int clampViewPositionVertical(View child, int top, int dy) {
328             return Math.max(Math.min(top, 0), -mVerticalRange);
329         }
330 
331         @Override
onViewCaptured(View capturedChild, int activePointerId)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
onViewReleased(View releasedChild, float xvel, float yvel)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