blob: 101b62e447f2352c06f534484f3e7ffb9e4d6594 [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
19import android.content.Context;
Annie Chin09547532016-10-14 10:59:07 -070020import android.os.Bundle;
21import android.os.Parcelable;
22import android.support.v4.view.ViewCompat;
23import android.support.v4.widget.ViewDragHelper;
24import android.util.AttributeSet;
25import android.view.MotionEvent;
26import android.view.View;
27import android.widget.FrameLayout;
28import android.widget.RelativeLayout;
29
Annie Chind0f87d22016-10-24 09:04:12 -070030import java.util.ArrayList;
31import java.util.List;
32
Annie Chin09547532016-10-14 10:59:07 -070033public class DragLayout extends RelativeLayout {
34
Annie Chind0f87d22016-10-24 09:04:12 -070035 private static final String TAG = "DragLayout";
Annie Chin09547532016-10-14 10:59:07 -070036 private static final double AUTO_OPEN_SPEED_LIMIT = 800.0;
37 private static final String KEY_IS_OPEN = "IS_OPEN";
38 private static final String KEY_SUPER_STATE = "SUPER_STATE";
39
Annie Chin09547532016-10-14 10:59:07 -070040 private FrameLayout mHistoryFrame;
41 private ViewDragHelper mDragHelper;
42
Annie Chind0f87d22016-10-24 09:04:12 -070043 private final List<DragCallback> mDragCallbacks = new ArrayList<>();
Annie Chin09547532016-10-14 10:59:07 -070044
45 private int mDraggingState = ViewDragHelper.STATE_IDLE;
46 private int mDraggingBorder;
47 private int mVerticalRange;
48 private boolean mIsOpen;
49
Annie Chind0f87d22016-10-24 09:04:12 -070050 // Used to determine whether a touch event should be intercepted.
51 private float mInitialDownX;
52 private float mInitialDownY;
53
Annie Chin09547532016-10-14 10:59:07 -070054 public DragLayout(Context context, AttributeSet attrs) {
55 super(context, attrs);
56 }
57
58 @Override
59 protected void onFinishInflate() {
60 mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
61 mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame);
Annie Chin09547532016-10-14 10:59:07 -070062 super.onFinishInflate();
63 }
64
65 @Override
66 protected void onLayout(boolean changed, int l, int t, int r, int b) {
67 super.onLayout(changed, l, t, r, b);
68 if (changed) {
Annie Chind0f87d22016-10-24 09:04:12 -070069 for (DragCallback c : mDragCallbacks) {
70 c.onLayout(t-b);
71 }
Annie Chin09547532016-10-14 10:59:07 -070072 if (mIsOpen) {
73 setOpen();
Annie Chind0f87d22016-10-24 09:04:12 -070074 } else {
75 setClosed();
Annie Chin09547532016-10-14 10:59:07 -070076 }
77 }
78 }
79
80 @Override
81 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Annie Chind0f87d22016-10-24 09:04:12 -070082 int height = 0;
83 for (DragCallback c : mDragCallbacks) {
84 height += c.getDisplayHeight();
85 }
86 mVerticalRange = h - height;
Annie Chin09547532016-10-14 10:59:07 -070087 super.onSizeChanged(w, h, oldw, oldh);
88 }
89
90 @Override
91 protected Parcelable onSaveInstanceState() {
92 final Bundle bundle = new Bundle();
93 bundle.putParcelable(KEY_SUPER_STATE, super.onSaveInstanceState());
94 bundle.putBoolean(KEY_IS_OPEN, mIsOpen);
95 return bundle;
96 }
97
98 @Override
99 protected void onRestoreInstanceState(Parcelable state) {
100 if (state instanceof Bundle) {
101 final Bundle bundle = (Bundle) state;
102 mIsOpen = bundle.getBoolean(KEY_IS_OPEN);
103 state = bundle.getParcelable(KEY_SUPER_STATE);
104 }
105 super.onRestoreInstanceState(state);
106 }
107
108 @Override
109 public boolean onInterceptTouchEvent(MotionEvent event) {
Annie Chind0f87d22016-10-24 09:04:12 -0700110 // First verify that we don't have a large deltaX (that the user is not trying to
111 // horizontally scroll).
112 final float x = event.getX();
113 final float y = event.getY();
114 final int action = event.getAction();
115
116 switch (action) {
117 case MotionEvent.ACTION_DOWN:
118 mInitialDownX = x;
119 mInitialDownY = y;
120 break;
121 case MotionEvent.ACTION_MOVE:
122 final float deltaX = Math.abs(x - mInitialDownX);
123 final float deltaY = Math.abs(y - mInitialDownY);
124 final int slop = mDragHelper.getTouchSlop();
125 if (deltaY > slop && deltaX > deltaY) {
126 mDragHelper.cancel();
127 return false;
128 }
129 }
130
131 boolean doDrag = true;
132 for (DragCallback c : mDragCallbacks) {
133 doDrag &= c.allowDrag(event);
134 }
135 return doDrag && mDragHelper.shouldInterceptTouchEvent(event);
Annie Chin09547532016-10-14 10:59:07 -0700136 }
137
138 @Override
139 public boolean onTouchEvent(MotionEvent event) {
Annie Chind0f87d22016-10-24 09:04:12 -0700140 boolean doIntercept = true;
141 for (DragCallback c : mDragCallbacks) {
142 doIntercept &= c.shouldInterceptTouchEvent(event);
143 }
144 if (doIntercept || isMoving()) {
Annie Chin09547532016-10-14 10:59:07 -0700145 mDragHelper.processTouchEvent(event);
146 return true;
147 } else {
148 return super.onTouchEvent(event);
149 }
150 }
151
152 @Override
153 public void computeScroll() {
154 if (mDragHelper.continueSettling(true)) {
155 ViewCompat.postInvalidateOnAnimation(this);
156 }
157 }
158
159 private void onStartDragging() {
Annie Chind0f87d22016-10-24 09:04:12 -0700160 for (DragCallback c : mDragCallbacks) {
161 c.onStartDragging();
162 }
Annie Chin09547532016-10-14 10:59:07 -0700163 mHistoryFrame.setVisibility(VISIBLE);
164 }
165
Annie Chin09547532016-10-14 10:59:07 -0700166 public boolean isMoving() {
167 return mDraggingState == ViewDragHelper.STATE_DRAGGING
168 || mDraggingState == ViewDragHelper.STATE_SETTLING;
169 }
170
171 public boolean isOpen() {
172 return mIsOpen;
173 }
174
175 public void setOpen() {
176 mDragHelper.smoothSlideViewTo(mHistoryFrame, 0, mVerticalRange);
177 mIsOpen = true;
178 mHistoryFrame.setVisibility(VISIBLE);
179 }
180
181 public void setClosed() {
Annie Chind0f87d22016-10-24 09:04:12 -0700182 // Scroll the RecyclerView to the bottom.
183 for (DragCallback c : mDragCallbacks) {
184 c.onClosed();
185 }
Annie Chin09547532016-10-14 10:59:07 -0700186 mDragHelper.smoothSlideViewTo(mHistoryFrame, 0, 0);
Annie Chin09547532016-10-14 10:59:07 -0700187 mHistoryFrame.setVisibility(GONE);
Annie Chind0f87d22016-10-24 09:04:12 -0700188 mIsOpen = false;
Annie Chin09547532016-10-14 10:59:07 -0700189 }
190
Annie Chind0f87d22016-10-24 09:04:12 -0700191 public void addDragCallback(DragCallback callback) {
192 mDragCallbacks.add(callback);
Annie Chin09547532016-10-14 10:59:07 -0700193 }
194
Annie Chind0f87d22016-10-24 09:04:12 -0700195 public void removeDragCallback(DragCallback callback) {
196 mDragCallbacks.remove(callback);
197 }
198
199 /**
200 * Callbacks for coordinating with the RecyclerView or HistoryFragment.
201 */
202 public interface DragCallback {
Annie Chin09547532016-10-14 10:59:07 -0700203 // Callback when a drag in any direction begins.
204 void onStartDragging();
205
Annie Chind0f87d22016-10-24 09:04:12 -0700206 // Animate the RecyclerView text.
207 void whileDragging(float yFraction);
208
209 // Scroll the RecyclerView to the bottom before closing the frame.
210 void onClosed();
211
212 // Whether we should allow the drag to happen
213 boolean allowDrag(MotionEvent event);
214
215 // Whether we should intercept the touch event
216 boolean shouldInterceptTouchEvent(MotionEvent event);
217
218 int getDisplayHeight();
219
220 void onLayout(int translation);
Annie Chin09547532016-10-14 10:59:07 -0700221 }
222
223 public class DragHelperCallback extends ViewDragHelper.Callback {
224 @Override
225 public void onViewDragStateChanged(int state) {
226 if (state == mDraggingState) {
227 // No change.
228 return;
229 }
230 if ((mDraggingState == ViewDragHelper.STATE_DRAGGING
231 || mDraggingState == ViewDragHelper.STATE_SETTLING)
232 && state == ViewDragHelper.STATE_IDLE) {
233 // The view stopped moving.
234 if (mDraggingBorder == 0) {
235 setClosed();
236 } else if (mDraggingBorder == mVerticalRange) {
237 setOpen();
238 }
239 }
240 if (state == ViewDragHelper.STATE_DRAGGING) {
241 onStartDragging();
242 }
243 mDraggingState = state;
244 }
245
246 @Override
247 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
248 mDraggingBorder = top;
Annie Chind0f87d22016-10-24 09:04:12 -0700249
250 // Animate RecyclerView text.
251 for (DragCallback c : mDragCallbacks) {
252 c.whileDragging(top / (mVerticalRange * 1.0f));
253 }
Annie Chin09547532016-10-14 10:59:07 -0700254 }
255
256 @Override
257 public int getViewVerticalDragRange(View child) {
258 return mVerticalRange;
259 }
260
261 @Override
262 public boolean tryCaptureView(View view, int i) {
263 return view.getId() == R.id.history_frame;
264 }
265
266 @Override
267 public int clampViewPositionVertical(View child, int top, int dy) {
268 final int topBound = getPaddingTop();
269 final int bottomBound = mVerticalRange;
270 return Math.min(Math.max(top, topBound), bottomBound);
271 }
272
273 @Override
274 public void onViewReleased(View releasedChild, float xvel, float yvel) {
Annie Chin09547532016-10-14 10:59:07 -0700275 boolean settleToOpen = false;
276 final float threshold = mVerticalRange / 2;
277 if (yvel > AUTO_OPEN_SPEED_LIMIT) {
278 // Speed has priority over position.
279 settleToOpen = true;
280 } else if (yvel < -AUTO_OPEN_SPEED_LIMIT) {
281 settleToOpen = false;
282 } else if (mDraggingBorder > threshold) {
283 settleToOpen = true;
284 } else if (mDraggingBorder < threshold) {
285 settleToOpen = false;
286 }
287
288 if (mDragHelper.settleCapturedViewAt(0, settleToOpen ? mVerticalRange : 0)) {
289 ViewCompat.postInvalidateOnAnimation(DragLayout.this);
290 }
291 }
292 }
293}