• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 androidx.core.view.ViewCompat;
28 import androidx.customview.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     public void setClosed() {
213         mIsOpen = false;
214         mHistoryFrame.setVisibility(View.INVISIBLE);
215         if (mCloseCallback != null) {
216             mCloseCallback.onClose();
217         }
218     }
219 
createAnimator(boolean toOpen)220     public Animator createAnimator(boolean toOpen) {
221         if (mIsOpen == toOpen) {
222             return ValueAnimator.ofFloat(0f, 1f).setDuration(0L);
223         }
224 
225         mIsOpen = toOpen;
226         mHistoryFrame.setVisibility(VISIBLE);
227 
228         final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
229         animator.addListener(new AnimatorListenerAdapter() {
230             @Override
231             public void onAnimationStart(Animator animation) {
232                 mDragHelper.cancel();
233                 mDragHelper.smoothSlideViewTo(mHistoryFrame, 0, mIsOpen ? 0 : -mVerticalRange);
234             }
235         });
236 
237         return animator;
238     }
239 
setCloseCallback(CloseCallback callback)240     public void setCloseCallback(CloseCallback callback) {
241         mCloseCallback = callback;
242     }
243 
addDragCallback(DragCallback callback)244     public void addDragCallback(DragCallback callback) {
245         mDragCallbacks.add(callback);
246     }
247 
removeDragCallback(DragCallback callback)248     public void removeDragCallback(DragCallback callback) {
249         mDragCallbacks.remove(callback);
250     }
251 
252     /**
253      * Callback when the layout is closed.
254      * We use this to pop the HistoryFragment off the backstack.
255      * We can't use a method in DragCallback because we get ConcurrentModificationExceptions on
256      * mDragCallbacks when executePendingTransactions() is called for popping the fragment off the
257      * backstack.
258      */
259     public interface CloseCallback {
onClose()260         void onClose();
261     }
262 
263     /**
264      * Callbacks for coordinating with the RecyclerView or HistoryFragment.
265      */
266     public interface DragCallback {
267         // Callback when a drag to open begins.
onStartDraggingOpen()268         void onStartDraggingOpen();
269 
270         // Callback in onRestoreInstanceState.
onInstanceStateRestored(boolean isOpen)271         void onInstanceStateRestored(boolean isOpen);
272 
273         // Animate the RecyclerView text.
whileDragging(float yFraction)274         void whileDragging(float yFraction);
275 
276         // Whether we should allow the view to be dragged.
shouldCaptureView(View view, int x, int y)277         boolean shouldCaptureView(View view, int x, int y);
278 
getDisplayHeight()279         int getDisplayHeight();
280     }
281 
282     public class DragHelperCallback extends ViewDragHelper.Callback {
283         @Override
onViewDragStateChanged(int state)284         public void onViewDragStateChanged(int state) {
285             // The view stopped moving.
286             if (state == ViewDragHelper.STATE_IDLE
287                     && mDragHelper.getCapturedView().getTop() < -(mVerticalRange / 2)) {
288                 setClosed();
289             }
290         }
291 
292         @Override
onViewPositionChanged(View changedView, int left, int top, int dx, int dy)293         public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
294             for (DragCallback c : mDragCallbacks) {
295                 // Top is between [-mVerticalRange, 0].
296                 c.whileDragging(1f + (float) top / mVerticalRange);
297             }
298         }
299 
300         @Override
getViewVerticalDragRange(View child)301         public int getViewVerticalDragRange(View child) {
302             return mVerticalRange;
303         }
304 
305         @Override
tryCaptureView(View view, int pointerId)306         public boolean tryCaptureView(View view, int pointerId) {
307             final PointF point = mLastMotionPoints.get(pointerId);
308             if (point == null) {
309                 return false;
310             }
311 
312             final int x = (int) point.x;
313             final int y = (int) point.y;
314 
315             for (DragCallback c : mDragCallbacks) {
316                 if (!c.shouldCaptureView(view, x, y)) {
317                     return false;
318                 }
319             }
320             return true;
321         }
322 
323         @Override
clampViewPositionVertical(View child, int top, int dy)324         public int clampViewPositionVertical(View child, int top, int dy) {
325             return Math.max(Math.min(top, 0), -mVerticalRange);
326         }
327 
328         @Override
onViewCaptured(View capturedChild, int activePointerId)329         public void onViewCaptured(View capturedChild, int activePointerId) {
330             super.onViewCaptured(capturedChild, activePointerId);
331 
332             if (!mIsOpen) {
333                 mIsOpen = true;
334                 onStartDragging();
335             }
336         }
337 
338         @Override
onViewReleased(View releasedChild, float xvel, float yvel)339         public void onViewReleased(View releasedChild, float xvel, float yvel) {
340             final boolean settleToOpen;
341             if (yvel > AUTO_OPEN_SPEED_LIMIT) {
342                 // Speed has priority over position.
343                 settleToOpen = true;
344             } else if (yvel < -AUTO_OPEN_SPEED_LIMIT) {
345                 settleToOpen = false;
346             } else {
347                 settleToOpen = releasedChild.getTop() > -(mVerticalRange / 2);
348             }
349 
350             // If the view is not visible, then settle it closed, not open.
351             if (mDragHelper.settleCapturedViewAt(0, settleToOpen && mIsOpen ? 0
352                     : -mVerticalRange)) {
353                 ViewCompat.postInvalidateOnAnimation(DragLayout.this);
354             }
355         }
356     }
357 }
358