blob: 74b0a8ea54a2da7c1bd2317acdff56092249de1e [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;
Aurimas Liutikas8c43f062018-03-28 08:10:28 -070027import androidx.core.view.ViewCompat;
28import androidx.customview.widget.ViewDragHelper;
Annie Chin09547532016-10-14 10:59:07 -070029import 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 Chinb61d00b2017-01-31 12:33:25 -0800167 // 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
Annie Chind3443222016-12-07 17:19:07 -0800176 saveLastMotion(event);
Annie Chinb61d00b2017-01-31 12:33:25 -0800177
Annie Chinc5b6e4f2016-12-05 13:34:14 -0800178 mDragHelper.processTouchEvent(event);
Annie Chind3443222016-12-07 17:19:07 -0800179 return true;
Annie Chin09547532016-10-14 10:59:07 -0700180 }
181
182 @Override
183 public void computeScroll() {
184 if (mDragHelper.continueSettling(true)) {
185 ViewCompat.postInvalidateOnAnimation(this);
186 }
187 }
188
189 private void onStartDragging() {
Annie Chind0f87d22016-10-24 09:04:12 -0700190 for (DragCallback c : mDragCallbacks) {
Annie Chin9a211132016-11-30 12:52:06 -0800191 c.onStartDraggingOpen();
Annie Chind0f87d22016-10-24 09:04:12 -0700192 }
Annie Chin09547532016-10-14 10:59:07 -0700193 mHistoryFrame.setVisibility(VISIBLE);
194 }
195
Annie Chind3443222016-12-07 17:19:07 -0800196 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
Annie Chin09547532016-10-14 10:59:07 -0700202 public boolean isMoving() {
Justin Klaassen39297782016-12-19 09:11:38 -0800203 final int draggingState = mDragHelper.getViewDragState();
204 return draggingState == ViewDragHelper.STATE_DRAGGING
205 || draggingState == ViewDragHelper.STATE_SETTLING;
Annie Chin09547532016-10-14 10:59:07 -0700206 }
207
208 public boolean isOpen() {
209 return mIsOpen;
210 }
211
Annie Chinbdfd38c2017-11-02 16:47:43 -0700212 public void setClosed() {
213 mIsOpen = false;
214 mHistoryFrame.setVisibility(View.INVISIBLE);
215 if (mCloseCallback != null) {
216 mCloseCallback.onClose();
Justin Klaassen39297782016-12-19 09:11:38 -0800217 }
218 }
219
Justin Klaassendec67e42017-01-09 08:11:51 -0800220 public Animator createAnimator(boolean toOpen) {
221 if (mIsOpen == toOpen) {
Annie China8b31db2017-02-09 08:11:22 -0800222 return ValueAnimator.ofFloat(0f, 1f).setDuration(0L);
Justin Klaassendec67e42017-01-09 08:11:51 -0800223 }
224
Annie China8b31db2017-02-09 08:11:22 -0800225 mIsOpen = toOpen;
Justin Klaassen39297782016-12-19 09:11:38 -0800226 mHistoryFrame.setVisibility(VISIBLE);
227
Annie China8b31db2017-02-09 08:11:22 -0800228 final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
229 animator.addListener(new AnimatorListenerAdapter() {
Justin Klaassen39297782016-12-19 09:11:38 -0800230 @Override
Annie China8b31db2017-02-09 08:11:22 -0800231 public void onAnimationStart(Animator animation) {
232 mDragHelper.cancel();
233 mDragHelper.smoothSlideViewTo(mHistoryFrame, 0, mIsOpen ? 0 : -mVerticalRange);
Justin Klaassen39297782016-12-19 09:11:38 -0800234 }
235 });
Justin Klaassen39297782016-12-19 09:11:38 -0800236
237 return animator;
Annie Chin9a211132016-11-30 12:52:06 -0800238 }
239
240 public void setCloseCallback(CloseCallback callback) {
241 mCloseCallback = callback;
Annie Chin09547532016-10-14 10:59:07 -0700242 }
243
Annie Chind0f87d22016-10-24 09:04:12 -0700244 public void addDragCallback(DragCallback callback) {
245 mDragCallbacks.add(callback);
Annie Chin09547532016-10-14 10:59:07 -0700246 }
247
Annie Chind0f87d22016-10-24 09:04:12 -0700248 public void removeDragCallback(DragCallback callback) {
249 mDragCallbacks.remove(callback);
250 }
251
252 /**
Annie Chin9a211132016-11-30 12:52:06 -0800253 * 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 {
260 void onClose();
261 }
262
263 /**
Annie Chind0f87d22016-10-24 09:04:12 -0700264 * Callbacks for coordinating with the RecyclerView or HistoryFragment.
265 */
266 public interface DragCallback {
Annie Chin9a211132016-11-30 12:52:06 -0800267 // Callback when a drag to open begins.
268 void onStartDraggingOpen();
Annie Chin09547532016-10-14 10:59:07 -0700269
Justin Klaassen39297782016-12-19 09:11:38 -0800270 // Callback in onRestoreInstanceState.
271 void onInstanceStateRestored(boolean isOpen);
272
Annie Chind0f87d22016-10-24 09:04:12 -0700273 // Animate the RecyclerView text.
274 void whileDragging(float yFraction);
275
Annie Chind3443222016-12-07 17:19:07 -0800276 // Whether we should allow the view to be dragged.
277 boolean shouldCaptureView(View view, int x, int y);
Annie Chind0f87d22016-10-24 09:04:12 -0700278
279 int getDisplayHeight();
Annie Chin09547532016-10-14 10:59:07 -0700280 }
281
282 public class DragHelperCallback extends ViewDragHelper.Callback {
283 @Override
284 public void onViewDragStateChanged(int state) {
Justin Klaassen39297782016-12-19 09:11:38 -0800285 // The view stopped moving.
Justin Klaassendec67e42017-01-09 08:11:51 -0800286 if (state == ViewDragHelper.STATE_IDLE
287 && mDragHelper.getCapturedView().getTop() < -(mVerticalRange / 2)) {
288 setClosed();
Annie Chin09547532016-10-14 10:59:07 -0700289 }
Annie Chin09547532016-10-14 10:59:07 -0700290 }
291
292 @Override
293 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
Annie Chind0f87d22016-10-24 09:04:12 -0700294 for (DragCallback c : mDragCallbacks) {
Justin Klaassen39297782016-12-19 09:11:38 -0800295 // Top is between [-mVerticalRange, 0].
296 c.whileDragging(1f + (float) top / mVerticalRange);
Annie Chind0f87d22016-10-24 09:04:12 -0700297 }
Annie Chin09547532016-10-14 10:59:07 -0700298 }
299
300 @Override
301 public int getViewVerticalDragRange(View child) {
302 return mVerticalRange;
303 }
304
305 @Override
Annie Chind3443222016-12-07 17:19:07 -0800306 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;
Annie Chin09547532016-10-14 10:59:07 -0700321 }
322
323 @Override
324 public int clampViewPositionVertical(View child, int top, int dy) {
Justin Klaassen39297782016-12-19 09:11:38 -0800325 return Math.max(Math.min(top, 0), -mVerticalRange);
326 }
327
328 @Override
329 public void onViewCaptured(View capturedChild, int activePointerId) {
330 super.onViewCaptured(capturedChild, activePointerId);
331
332 if (!mIsOpen) {
333 mIsOpen = true;
334 onStartDragging();
335 }
Annie Chin09547532016-10-14 10:59:07 -0700336 }
337
338 @Override
339 public void onViewReleased(View releasedChild, float xvel, float yvel) {
Justin Klaassen39297782016-12-19 09:11:38 -0800340 final boolean settleToOpen;
Annie Chin09547532016-10-14 10:59:07 -0700341 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;
Justin Klaassen39297782016-12-19 09:11:38 -0800346 } else {
347 settleToOpen = releasedChild.getTop() > -(mVerticalRange / 2);
Annie Chin09547532016-10-14 10:59:07 -0700348 }
349
Annie Chinbdfd38c2017-11-02 16:47:43 -0700350 // If the view is not visible, then settle it closed, not open.
351 if (mDragHelper.settleCapturedViewAt(0, settleToOpen && mIsOpen ? 0
352 : -mVerticalRange)) {
Annie Chin09547532016-10-14 10:59:07 -0700353 ViewCompat.postInvalidateOnAnimation(DragLayout.this);
354 }
355 }
356 }
Annie Chind3443222016-12-07 17:19:07 -0800357}