blob: 9cb1f1eca974eea137761857521d8817abc77616 [file] [log] [blame]
Eric Erfanian938468d2017-10-24 14:05:52 -07001/*
2 * Copyright (C) 2017 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.newbubble;
18
19import android.content.Context;
20import android.graphics.Point;
21import android.support.animation.FloatPropertyCompat;
22import android.support.animation.SpringAnimation;
23import android.support.animation.SpringForce;
24import android.support.annotation.NonNull;
25import android.view.Gravity;
26import android.view.MotionEvent;
27import android.view.VelocityTracker;
28import android.view.View;
29import android.view.View.OnTouchListener;
30import android.view.ViewConfiguration;
31import android.view.WindowManager;
32import android.view.WindowManager.LayoutParams;
33import android.widget.Scroller;
34
35/** Handles touches and manages moving the bubble in response */
36class NewMoveHandler implements OnTouchListener {
37
38 // Amount the ViewConfiguration's minFlingVelocity will be scaled by for our own minVelocity
39 private static final int MIN_FLING_VELOCITY_FACTOR = 8;
40 // The friction multiplier to control how slippery the bubble is when flung
41 private static final float SCROLL_FRICTION_MULTIPLIER = 4f;
42
43 private final Context context;
44 private final WindowManager windowManager;
45 private final NewBubble bubble;
46 private final int minX;
47 private final int minY;
48 private final int maxX;
49 private final int maxY;
50 private final int bubbleSize;
yueg81a77ff2017-12-05 10:29:03 -080051 private final int bubbleShadowPaddingHorizontal;
52 private final int bubbleExpandedViewWidth;
Eric Erfanian938468d2017-10-24 14:05:52 -070053 private final float touchSlopSquared;
54
55 private boolean clickable = true;
56 private boolean isMoving;
57 private float firstX;
58 private float firstY;
59
60 private SpringAnimation moveXAnimation;
61 private SpringAnimation moveYAnimation;
62 private VelocityTracker velocityTracker;
63 private Scroller scroller;
64
65 private static float clamp(float value, float min, float max) {
66 return Math.min(max, Math.max(min, value));
67 }
68
69 // Handles the left/right gravity conversion and centering
70 private final FloatPropertyCompat<WindowManager.LayoutParams> xProperty =
71 new FloatPropertyCompat<LayoutParams>("xProperty") {
72 @Override
73 public float getValue(LayoutParams windowParams) {
74 int realX = windowParams.x;
yueg81a77ff2017-12-05 10:29:03 -080075 // Get bubble center position from real position
76 if (bubble.getDrawerVisibility() == View.INVISIBLE) {
77 realX += bubbleExpandedViewWidth / 2 + bubbleShadowPaddingHorizontal * 2;
78 } else {
79 realX += bubbleSize / 2 + bubbleShadowPaddingHorizontal;
80 }
Eric Erfanian938468d2017-10-24 14:05:52 -070081 if (relativeToRight(windowParams)) {
yueg81a77ff2017-12-05 10:29:03 -080082 // If gravity is right, get distant from bubble center position to screen right edge
Eric Erfanian938468d2017-10-24 14:05:52 -070083 int displayWidth = context.getResources().getDisplayMetrics().widthPixels;
84 realX = displayWidth - realX;
85 }
86 return clamp(realX, minX, maxX);
87 }
88
89 @Override
90 public void setValue(LayoutParams windowParams, float value) {
91 int displayWidth = context.getResources().getDisplayMetrics().widthPixels;
92 boolean onRight;
93 Integer gravityOverride = bubble.getGravityOverride();
94 if (gravityOverride == null) {
95 onRight = value > displayWidth / 2;
96 } else {
97 onRight = (gravityOverride & Gravity.RIGHT) == Gravity.RIGHT;
98 }
yueg81a77ff2017-12-05 10:29:03 -080099 // Get real position from bubble center position
100 int centeringOffset;
101 if (bubble.getDrawerVisibility() == View.INVISIBLE) {
102 centeringOffset = bubbleExpandedViewWidth / 2 + bubbleShadowPaddingHorizontal * 2;
103 } else {
104 centeringOffset = bubbleSize / 2 + bubbleShadowPaddingHorizontal;
105 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700106 windowParams.x =
107 (int) (onRight ? (displayWidth - value - centeringOffset) : value - centeringOffset);
108 windowParams.gravity = Gravity.TOP | (onRight ? Gravity.RIGHT : Gravity.LEFT);
109 if (bubble.isVisible()) {
110 windowManager.updateViewLayout(bubble.getRootView(), windowParams);
yueg81a77ff2017-12-05 10:29:03 -0800111 bubble.onLeftRightSwitch(onRight);
Eric Erfanian938468d2017-10-24 14:05:52 -0700112 }
113 }
114 };
115
116 private final FloatPropertyCompat<WindowManager.LayoutParams> yProperty =
117 new FloatPropertyCompat<LayoutParams>("yProperty") {
118 @Override
119 public float getValue(LayoutParams object) {
120 return clamp(object.y + bubbleSize, minY, maxY);
121 }
122
123 @Override
124 public void setValue(LayoutParams object, float value) {
125 object.y = (int) value - bubbleSize;
126 if (bubble.isVisible()) {
127 windowManager.updateViewLayout(bubble.getRootView(), object);
128 }
129 }
130 };
131
132 public NewMoveHandler(@NonNull View targetView, @NonNull NewBubble bubble) {
133 this.bubble = bubble;
134 context = targetView.getContext();
135 windowManager = context.getSystemService(WindowManager.class);
136
137 bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
yueg81a77ff2017-12-05 10:29:03 -0800138 bubbleShadowPaddingHorizontal =
139 context.getResources().getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal);
140 bubbleExpandedViewWidth =
141 context.getResources().getDimensionPixelSize(R.dimen.bubble_expanded_width);
142 // The following value is based on bubble center
Eric Erfanian938468d2017-10-24 14:05:52 -0700143 minX =
yueg81a77ff2017-12-05 10:29:03 -0800144 context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal)
Eric Erfanian938468d2017-10-24 14:05:52 -0700145 + bubbleSize / 2;
146 minY =
147 context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_vertical)
148 + bubbleSize / 2;
149 maxX = context.getResources().getDisplayMetrics().widthPixels - minX;
150 maxY = context.getResources().getDisplayMetrics().heightPixels - minY;
151
152 // Squared because it will be compared against the square of the touch delta. This is more
153 // efficient than needing to take a square root.
154 touchSlopSquared = (float) Math.pow(ViewConfiguration.get(context).getScaledTouchSlop(), 2);
155
156 targetView.setOnTouchListener(this);
157 }
158
159 public void setClickable(boolean clickable) {
160 this.clickable = clickable;
161 }
162
163 public boolean isMoving() {
164 return isMoving;
165 }
166
167 public void undoGravityOverride() {
168 LayoutParams windowParams = bubble.getWindowParams();
169 xProperty.setValue(windowParams, xProperty.getValue(windowParams));
170 }
171
172 public void snapToBounds() {
173 ensureSprings();
174
175 moveXAnimation.animateToFinalPosition(relativeToRight(bubble.getWindowParams()) ? maxX : minX);
176 moveYAnimation.animateToFinalPosition(yProperty.getValue(bubble.getWindowParams()));
177 }
178
yueg81a77ff2017-12-05 10:29:03 -0800179 public int getMoveUpDistance(int deltaAllowed) {
180 int currentY = (int) yProperty.getValue(bubble.getWindowParams());
181 int currentDelta = maxY - currentY;
182 return currentDelta >= deltaAllowed ? 0 : deltaAllowed - currentDelta;
183 }
184
Eric Erfanian938468d2017-10-24 14:05:52 -0700185 @Override
186 public boolean onTouch(View v, MotionEvent event) {
187 float eventX = event.getRawX();
188 float eventY = event.getRawY();
189 switch (event.getActionMasked()) {
190 case MotionEvent.ACTION_DOWN:
191 firstX = eventX;
192 firstY = eventY;
193 velocityTracker = VelocityTracker.obtain();
194 break;
195 case MotionEvent.ACTION_MOVE:
196 if (isMoving || hasExceededTouchSlop(event)) {
197 if (!isMoving) {
198 isMoving = true;
199 bubble.onMoveStart();
200 }
201
202 ensureSprings();
203
204 moveXAnimation.animateToFinalPosition(clamp(eventX, minX, maxX));
205 moveYAnimation.animateToFinalPosition(clamp(eventY, minY, maxY));
206 }
207
208 velocityTracker.addMovement(event);
209 break;
210 case MotionEvent.ACTION_UP:
211 if (isMoving) {
212 ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
213 velocityTracker.computeCurrentVelocity(
214 1000, viewConfiguration.getScaledMaximumFlingVelocity());
215 float xVelocity = velocityTracker.getXVelocity();
216 float yVelocity = velocityTracker.getYVelocity();
217 boolean isFling = isFling(xVelocity, yVelocity);
218
219 if (isFling) {
220 Point target =
221 findTarget(
222 xVelocity,
223 yVelocity,
224 (int) xProperty.getValue(bubble.getWindowParams()),
225 (int) yProperty.getValue(bubble.getWindowParams()));
226
227 moveXAnimation.animateToFinalPosition(target.x);
228 moveYAnimation.animateToFinalPosition(target.y);
229 } else {
230 snapX();
231 }
232 isMoving = false;
233 bubble.onMoveFinish();
234 } else {
235 v.performClick();
236 if (clickable) {
237 bubble.primaryButtonClick();
238 }
239 }
240 break;
241 default: // fall out
242 }
243 return true;
244 }
245
246 private void ensureSprings() {
247 if (moveXAnimation == null) {
248 moveXAnimation = new SpringAnimation(bubble.getWindowParams(), xProperty);
249 moveXAnimation.setSpring(new SpringForce());
250 moveXAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
yueg81a77ff2017-12-05 10:29:03 -0800251 // Moving when expanded makes expanded view INVISIBLE, and the whole view is not at the
252 // boundary. It's time to create a viewHolder.
253 moveXAnimation.addEndListener(
254 (animation, canceled, value, velocity) -> {
255 if (!isMoving && bubble.getDrawerVisibility() == View.INVISIBLE) {
256 bubble.replaceViewHolder();
257 }
258 });
Eric Erfanian938468d2017-10-24 14:05:52 -0700259 }
260
261 if (moveYAnimation == null) {
262 moveYAnimation = new SpringAnimation(bubble.getWindowParams(), yProperty);
263 moveYAnimation.setSpring(new SpringForce());
264 moveYAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
265 }
266 }
267
268 private Point findTarget(float xVelocity, float yVelocity, int startX, int startY) {
269 if (scroller == null) {
270 scroller = new Scroller(context);
271 scroller.setFriction(ViewConfiguration.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER);
272 }
273
274 // Find where a fling would end vertically
275 scroller.fling(startX, startY, (int) xVelocity, (int) yVelocity, minX, maxX, minY, maxY);
276 int targetY = scroller.getFinalY();
277 scroller.abortAnimation();
278
279 // If the x component of the velocity is above the minimum fling velocity, use velocity to
280 // determine edge. Otherwise use its starting position
281 boolean pullRight = isFling(xVelocity, 0) ? xVelocity > 0 : isOnRightHalf(startX);
282 return new Point(pullRight ? maxX : minX, targetY);
283 }
284
285 private boolean isFling(float xVelocity, float yVelocity) {
286 int minFlingVelocity =
287 ViewConfiguration.get(context).getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_FACTOR;
288 return getMagnitudeSquared(xVelocity, yVelocity) > minFlingVelocity * minFlingVelocity;
289 }
290
291 private boolean isOnRightHalf(float currentX) {
292 return currentX > (minX + maxX) / 2;
293 }
294
295 private void snapX() {
296 // Check if x value is closer to min or max
297 boolean pullRight = isOnRightHalf(xProperty.getValue(bubble.getWindowParams()));
298 moveXAnimation.animateToFinalPosition(pullRight ? maxX : minX);
299 }
300
301 private boolean relativeToRight(LayoutParams windowParams) {
302 return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
303 }
304
305 private boolean hasExceededTouchSlop(MotionEvent event) {
306 return getMagnitudeSquared(event.getRawX() - firstX, event.getRawY() - firstY)
307 > touchSlopSquared;
308 }
309
310 private float getMagnitudeSquared(float deltaX, float deltaY) {
311 return deltaX * deltaX + deltaY * deltaY;
312 }
313}