blob: e8f02e542f22578d0a732c6504d71ed19be11b94 [file] [log] [blame]
Annie Chin09547532016-10-14 10:59:07 -07001/*
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
Justin Klaassen39297782016-12-19 09:11:38 -080019import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
Annie Chin09547532016-10-14 10:59:07 -070022import android.content.Context;
Annie Chind3443222016-12-07 17:19:07 -080023import android.graphics.PointF;
24import android.graphics.Rect;
Annie Chin09547532016-10-14 10:59:07 -070025import 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;
Justin Klaassen39297782016-12-19 09:11:38 -080032import android.view.ViewGroup;
Annie Chin09547532016-10-14 10:59:07 -070033import android.widget.FrameLayout;
Annie Chin09547532016-10-14 10:59:07 -070034
Annie Chind3443222016-12-07 17:19:07 -080035import java.util.HashMap;
Annie Chind0f87d22016-10-24 09:04:12 -070036import java.util.List;
Annie Chind3443222016-12-07 17:19:07 -080037import java.util.Map;
Annie Chinb9ce4d02016-12-09 15:26:41 -080038import java.util.concurrent.CopyOnWriteArrayList;
Annie Chind0f87d22016-10-24 09:04:12 -070039
Justin Klaassen39297782016-12-19 09:11:38 -080040public class DragLayout extends ViewGroup {
Annie Chin09547532016-10-14 10:59:07 -070041
Justin Klaassen39297782016-12-19 09:11:38 -080042 private static final double AUTO_OPEN_SPEED_LIMIT = 600.0;
Annie Chin09547532016-10-14 10:59:07 -070043 private static final String KEY_IS_OPEN = "IS_OPEN";
44 private static final String KEY_SUPER_STATE = "SUPER_STATE";
45
Annie Chin09547532016-10-14 10:59:07 -070046 private FrameLayout mHistoryFrame;
47 private ViewDragHelper mDragHelper;
48
Annie Chinb9ce4d02016-12-09 15:26:41 -080049 // No concurrency; allow modifications while iterating.
50 private final List<DragCallback> mDragCallbacks = new CopyOnWriteArrayList<>();
Annie Chin9a211132016-11-30 12:52:06 -080051 private CloseCallback mCloseCallback;
Annie Chin09547532016-10-14 10:59:07 -070052
Annie Chind3443222016-12-07 17:19:07 -080053 private final Map<Integer, PointF> mLastMotionPoints = new HashMap<>();
54 private final Rect mHitRect = new Rect();
55
Annie Chin09547532016-10-14 10:59:07 -070056 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);
Annie Chin09547532016-10-14 10:59:07 -070067 super.onFinishInflate();
68 }
69
70 @Override
Justin Klaassen39297782016-12-19 09:11:38 -080071 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
72 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
73 measureChildren(widthMeasureSpec, heightMeasureSpec);
Annie Chin09547532016-10-14 10:59:07 -070074 }
75
76 @Override
Justin Klaassen39297782016-12-19 09:11:38 -080077 protected void onLayout(boolean changed, int l, int t, int r, int b) {
78 int displayHeight = 0;
Annie Chind0f87d22016-10-24 09:04:12 -070079 for (DragCallback c : mDragCallbacks) {
Justin Klaassen39297782016-12-19 09:11:38 -080080 displayHeight = Math.max(displayHeight, c.getDisplayHeight());
Annie Chind0f87d22016-10-24 09:04:12 -070081 }
Justin Klaassen39297782016-12-19 09:11:38 -080082 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) {
Justin Klaassendec67e42017-01-09 08:11:51 -080090 if (mDragHelper.getCapturedView() == mHistoryFrame
91 && mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) {
92 top = child.getTop();
93 } else {
94 top = mIsOpen ? 0 : -mVerticalRange;
95 }
Justin Klaassen39297782016-12-19 09:11:38 -080096 }
97 child.layout(0, top, child.getMeasuredWidth(), top + child.getMeasuredHeight());
98 }
Annie Chin09547532016-10-14 10:59:07 -070099 }
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);
Justin Klaassen39297782016-12-19 09:11:38 -0800114 mHistoryFrame.setVisibility(mIsOpen ? View.VISIBLE : View.INVISIBLE);
115 for (DragCallback c : mDragCallbacks) {
116 c.onInstanceStateRestored(mIsOpen);
117 }
118
Annie Chin09547532016-10-14 10:59:07 -0700119 state = bundle.getParcelable(KEY_SUPER_STATE);
120 }
121 super.onRestoreInstanceState(state);
122 }
123
Annie Chind3443222016-12-07 17:19:07 -0800124 private void saveLastMotion(MotionEvent event) {
Annie Chinc5b6e4f2016-12-05 13:34:14 -0800125 final int action = event.getActionMasked();
Annie Chind0f87d22016-10-24 09:04:12 -0700126 switch (action) {
127 case MotionEvent.ACTION_DOWN:
Annie Chind3443222016-12-07 17:19:07 -0800128 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);
Annie Chind0f87d22016-10-24 09:04:12 -0700133 break;
Annie Chind3443222016-12-07 17:19:07 -0800134 }
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 }
Annie Chind0f87d22016-10-24 09:04:12 -0700142 }
Annie Chind3443222016-12-07 17:19:07 -0800143 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 }
Annie Chind0f87d22016-10-24 09:04:12 -0700156 }
Annie Chind3443222016-12-07 17:19:07 -0800157 }
158
159 @Override
160 public boolean onInterceptTouchEvent(MotionEvent event) {
161 saveLastMotion(event);
162 return mDragHelper.shouldInterceptTouchEvent(event);
Annie Chin09547532016-10-14 10:59:07 -0700163 }
164
165 @Override
166 public boolean onTouchEvent(MotionEvent event) {
Annie Chind3443222016-12-07 17:19:07 -0800167 saveLastMotion(event);
Annie Chinc5b6e4f2016-12-05 13:34:14 -0800168 mDragHelper.processTouchEvent(event);
Annie Chind3443222016-12-07 17:19:07 -0800169 return true;
Annie Chin09547532016-10-14 10:59:07 -0700170 }
171
172 @Override
173 public void computeScroll() {
174 if (mDragHelper.continueSettling(true)) {
175 ViewCompat.postInvalidateOnAnimation(this);
176 }
177 }
178
179 private void onStartDragging() {
Annie Chind0f87d22016-10-24 09:04:12 -0700180 for (DragCallback c : mDragCallbacks) {
Annie Chin9a211132016-11-30 12:52:06 -0800181 c.onStartDraggingOpen();
Annie Chind0f87d22016-10-24 09:04:12 -0700182 }
Annie Chin09547532016-10-14 10:59:07 -0700183 mHistoryFrame.setVisibility(VISIBLE);
184 }
185
Annie Chind3443222016-12-07 17:19:07 -0800186 public boolean isViewUnder(View view, int x, int y) {
187 view.getHitRect(mHitRect);
188 offsetDescendantRectToMyCoords((View) view.getParent(), mHitRect);
189 return mHitRect.contains(x, y);
190 }
191
Annie Chin09547532016-10-14 10:59:07 -0700192 public boolean isMoving() {
Justin Klaassen39297782016-12-19 09:11:38 -0800193 final int draggingState = mDragHelper.getViewDragState();
194 return draggingState == ViewDragHelper.STATE_DRAGGING
195 || draggingState == ViewDragHelper.STATE_SETTLING;
Annie Chin09547532016-10-14 10:59:07 -0700196 }
197
198 public boolean isOpen() {
199 return mIsOpen;
200 }
201
Justin Klaassendec67e42017-01-09 08:11:51 -0800202 private void setClosed() {
Justin Klaassen39297782016-12-19 09:11:38 -0800203 if (mIsOpen) {
204 mIsOpen = false;
Justin Klaassen39297782016-12-19 09:11:38 -0800205 mHistoryFrame.setVisibility(View.INVISIBLE);
206
207 if (mCloseCallback != null) {
208 mCloseCallback.onClose();
209 }
210 }
211 }
212
Justin Klaassendec67e42017-01-09 08:11:51 -0800213 public Animator createAnimator(boolean toOpen) {
214 if (mIsOpen == toOpen) {
215 return null;
216 }
217
218 mIsOpen = true;
Justin Klaassen39297782016-12-19 09:11:38 -0800219 mHistoryFrame.setVisibility(VISIBLE);
220
221 final ValueAnimator animator = ValueAnimator.ofInt(mHistoryFrame.getTop(),
222 toOpen ? 0 : -mVerticalRange);
223 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
224 @Override
225 public void onAnimationUpdate(ValueAnimator animator) {
226 final int top = (int) animator.getAnimatedValue();
227 mHistoryFrame.offsetTopAndBottom(top - mHistoryFrame.getTop());
228
229 for (DragCallback c : mDragCallbacks) {
230 // Top is between [-mVerticalRange, 0].
231 c.whileDragging(1f + (float) top / mVerticalRange);
232 }
233 }
234 });
Justin Klaassendec67e42017-01-09 08:11:51 -0800235 if (!toOpen) {
236 animator.addListener(new AnimatorListenerAdapter() {
237 @Override
238 public void onAnimationEnd(Animator animator) {
Justin Klaassen39297782016-12-19 09:11:38 -0800239 setClosed();
240 }
Justin Klaassendec67e42017-01-09 08:11:51 -0800241 });
242 }
Justin Klaassen39297782016-12-19 09:11:38 -0800243
244 return animator;
Annie Chin9a211132016-11-30 12:52:06 -0800245 }
246
247 public void setCloseCallback(CloseCallback callback) {
248 mCloseCallback = callback;
Annie Chin09547532016-10-14 10:59:07 -0700249 }
250
Annie Chind0f87d22016-10-24 09:04:12 -0700251 public void addDragCallback(DragCallback callback) {
252 mDragCallbacks.add(callback);
Annie Chin09547532016-10-14 10:59:07 -0700253 }
254
Annie Chind0f87d22016-10-24 09:04:12 -0700255 public void removeDragCallback(DragCallback callback) {
256 mDragCallbacks.remove(callback);
257 }
258
259 /**
Annie Chin9a211132016-11-30 12:52:06 -0800260 * Callback when the layout is closed.
261 * We use this to pop the HistoryFragment off the backstack.
262 * We can't use a method in DragCallback because we get ConcurrentModificationExceptions on
263 * mDragCallbacks when executePendingTransactions() is called for popping the fragment off the
264 * backstack.
265 */
266 public interface CloseCallback {
267 void onClose();
268 }
269
270 /**
Annie Chind0f87d22016-10-24 09:04:12 -0700271 * Callbacks for coordinating with the RecyclerView or HistoryFragment.
272 */
273 public interface DragCallback {
Annie Chin9a211132016-11-30 12:52:06 -0800274 // Callback when a drag to open begins.
275 void onStartDraggingOpen();
Annie Chin09547532016-10-14 10:59:07 -0700276
Justin Klaassen39297782016-12-19 09:11:38 -0800277 // Callback in onRestoreInstanceState.
278 void onInstanceStateRestored(boolean isOpen);
279
Annie Chind0f87d22016-10-24 09:04:12 -0700280 // Animate the RecyclerView text.
281 void whileDragging(float yFraction);
282
Annie Chind3443222016-12-07 17:19:07 -0800283 // Whether we should allow the view to be dragged.
284 boolean shouldCaptureView(View view, int x, int y);
Annie Chind0f87d22016-10-24 09:04:12 -0700285
286 int getDisplayHeight();
Annie Chin09547532016-10-14 10:59:07 -0700287 }
288
289 public class DragHelperCallback extends ViewDragHelper.Callback {
290 @Override
291 public void onViewDragStateChanged(int state) {
Justin Klaassen39297782016-12-19 09:11:38 -0800292 // The view stopped moving.
Justin Klaassendec67e42017-01-09 08:11:51 -0800293 if (state == ViewDragHelper.STATE_IDLE
294 && mDragHelper.getCapturedView().getTop() < -(mVerticalRange / 2)) {
295 setClosed();
Annie Chin09547532016-10-14 10:59:07 -0700296 }
Annie Chin09547532016-10-14 10:59:07 -0700297 }
298
299 @Override
300 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
Annie Chind0f87d22016-10-24 09:04:12 -0700301 for (DragCallback c : mDragCallbacks) {
Justin Klaassen39297782016-12-19 09:11:38 -0800302 // Top is between [-mVerticalRange, 0].
303 c.whileDragging(1f + (float) top / mVerticalRange);
Annie Chind0f87d22016-10-24 09:04:12 -0700304 }
Annie Chin09547532016-10-14 10:59:07 -0700305 }
306
307 @Override
308 public int getViewVerticalDragRange(View child) {
309 return mVerticalRange;
310 }
311
312 @Override
Annie Chind3443222016-12-07 17:19:07 -0800313 public boolean tryCaptureView(View view, int pointerId) {
314 final PointF point = mLastMotionPoints.get(pointerId);
315 if (point == null) {
316 return false;
317 }
318
319 final int x = (int) point.x;
320 final int y = (int) point.y;
321
322 for (DragCallback c : mDragCallbacks) {
323 if (!c.shouldCaptureView(view, x, y)) {
324 return false;
325 }
326 }
327 return true;
Annie Chin09547532016-10-14 10:59:07 -0700328 }
329
330 @Override
331 public int clampViewPositionVertical(View child, int top, int dy) {
Justin Klaassen39297782016-12-19 09:11:38 -0800332 return Math.max(Math.min(top, 0), -mVerticalRange);
333 }
334
335 @Override
336 public void onViewCaptured(View capturedChild, int activePointerId) {
337 super.onViewCaptured(capturedChild, activePointerId);
338
339 if (!mIsOpen) {
340 mIsOpen = true;
341 onStartDragging();
342 }
Annie Chin09547532016-10-14 10:59:07 -0700343 }
344
345 @Override
346 public void onViewReleased(View releasedChild, float xvel, float yvel) {
Justin Klaassen39297782016-12-19 09:11:38 -0800347 final boolean settleToOpen;
Annie Chin09547532016-10-14 10:59:07 -0700348 if (yvel > AUTO_OPEN_SPEED_LIMIT) {
349 // Speed has priority over position.
350 settleToOpen = true;
351 } else if (yvel < -AUTO_OPEN_SPEED_LIMIT) {
352 settleToOpen = false;
Justin Klaassen39297782016-12-19 09:11:38 -0800353 } else {
354 settleToOpen = releasedChild.getTop() > -(mVerticalRange / 2);
Annie Chin09547532016-10-14 10:59:07 -0700355 }
356
Justin Klaassen39297782016-12-19 09:11:38 -0800357 if (mDragHelper.settleCapturedViewAt(0, settleToOpen ? 0 : -mVerticalRange)) {
Annie Chin09547532016-10-14 10:59:07 -0700358 ViewCompat.postInvalidateOnAnimation(DragLayout.this);
359 }
360 }
361 }
Annie Chind3443222016-12-07 17:19:07 -0800362}