blob: c00c10729c228039fd8b179cad7412e8cc72ebd3 [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;
yuegf473e1d2018-01-02 16:23:14 -080054 private final BottomActionViewController bottomActionViewController;
Eric Erfanian938468d2017-10-24 14:05:52 -070055
56 private boolean clickable = true;
57 private boolean isMoving;
58 private float firstX;
59 private float firstY;
60
61 private SpringAnimation moveXAnimation;
62 private SpringAnimation moveYAnimation;
63 private VelocityTracker velocityTracker;
64 private Scroller scroller;
65
66 private static float clamp(float value, float min, float max) {
67 return Math.min(max, Math.max(min, value));
68 }
69
70 // Handles the left/right gravity conversion and centering
71 private final FloatPropertyCompat<WindowManager.LayoutParams> xProperty =
72 new FloatPropertyCompat<LayoutParams>("xProperty") {
73 @Override
74 public float getValue(LayoutParams windowParams) {
75 int realX = windowParams.x;
yueg81a77ff2017-12-05 10:29:03 -080076 // Get bubble center position from real position
77 if (bubble.getDrawerVisibility() == View.INVISIBLE) {
78 realX += bubbleExpandedViewWidth / 2 + bubbleShadowPaddingHorizontal * 2;
79 } else {
80 realX += bubbleSize / 2 + bubbleShadowPaddingHorizontal;
81 }
Eric Erfanian938468d2017-10-24 14:05:52 -070082 if (relativeToRight(windowParams)) {
yueg81a77ff2017-12-05 10:29:03 -080083 // If gravity is right, get distant from bubble center position to screen right edge
Eric Erfanian938468d2017-10-24 14:05:52 -070084 int displayWidth = context.getResources().getDisplayMetrics().widthPixels;
85 realX = displayWidth - realX;
86 }
87 return clamp(realX, minX, maxX);
88 }
89
90 @Override
91 public void setValue(LayoutParams windowParams, float value) {
yueg87111362017-12-08 12:45:50 -080092 boolean wasOnRight = (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
Eric Erfanian938468d2017-10-24 14:05:52 -070093 int displayWidth = context.getResources().getDisplayMetrics().widthPixels;
94 boolean onRight;
95 Integer gravityOverride = bubble.getGravityOverride();
96 if (gravityOverride == null) {
97 onRight = value > displayWidth / 2;
98 } else {
99 onRight = (gravityOverride & Gravity.RIGHT) == Gravity.RIGHT;
100 }
yueg81a77ff2017-12-05 10:29:03 -0800101 // Get real position from bubble center position
102 int centeringOffset;
103 if (bubble.getDrawerVisibility() == View.INVISIBLE) {
104 centeringOffset = bubbleExpandedViewWidth / 2 + bubbleShadowPaddingHorizontal * 2;
105 } else {
106 centeringOffset = bubbleSize / 2 + bubbleShadowPaddingHorizontal;
107 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700108 windowParams.x =
109 (int) (onRight ? (displayWidth - value - centeringOffset) : value - centeringOffset);
110 windowParams.gravity = Gravity.TOP | (onRight ? Gravity.RIGHT : Gravity.LEFT);
111 if (bubble.isVisible()) {
112 windowManager.updateViewLayout(bubble.getRootView(), windowParams);
yueg87111362017-12-08 12:45:50 -0800113 if (onRight != wasOnRight) {
114 bubble.onLeftRightSwitch(onRight);
115 }
Eric Erfanian938468d2017-10-24 14:05:52 -0700116 }
117 }
118 };
119
120 private final FloatPropertyCompat<WindowManager.LayoutParams> yProperty =
121 new FloatPropertyCompat<LayoutParams>("yProperty") {
122 @Override
123 public float getValue(LayoutParams object) {
124 return clamp(object.y + bubbleSize, minY, maxY);
125 }
126
127 @Override
128 public void setValue(LayoutParams object, float value) {
129 object.y = (int) value - bubbleSize;
130 if (bubble.isVisible()) {
131 windowManager.updateViewLayout(bubble.getRootView(), object);
132 }
133 }
134 };
135
136 public NewMoveHandler(@NonNull View targetView, @NonNull NewBubble bubble) {
137 this.bubble = bubble;
138 context = targetView.getContext();
139 windowManager = context.getSystemService(WindowManager.class);
140
141 bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
yueg81a77ff2017-12-05 10:29:03 -0800142 bubbleShadowPaddingHorizontal =
143 context.getResources().getDimensionPixelSize(R.dimen.bubble_shadow_padding_size_horizontal);
144 bubbleExpandedViewWidth =
145 context.getResources().getDimensionPixelSize(R.dimen.bubble_expanded_width);
146 // The following value is based on bubble center
Eric Erfanian938468d2017-10-24 14:05:52 -0700147 minX =
yueg81a77ff2017-12-05 10:29:03 -0800148 context.getResources().getDimensionPixelOffset(R.dimen.bubble_off_screen_size_horizontal)
Eric Erfanian938468d2017-10-24 14:05:52 -0700149 + bubbleSize / 2;
150 minY =
151 context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_vertical)
152 + bubbleSize / 2;
153 maxX = context.getResources().getDisplayMetrics().widthPixels - minX;
154 maxY = context.getResources().getDisplayMetrics().heightPixels - minY;
155
156 // Squared because it will be compared against the square of the touch delta. This is more
157 // efficient than needing to take a square root.
158 touchSlopSquared = (float) Math.pow(ViewConfiguration.get(context).getScaledTouchSlop(), 2);
159
yuegf473e1d2018-01-02 16:23:14 -0800160 bottomActionViewController = new BottomActionViewController(context);
161
Eric Erfanian938468d2017-10-24 14:05:52 -0700162 targetView.setOnTouchListener(this);
163 }
164
165 public void setClickable(boolean clickable) {
166 this.clickable = clickable;
167 }
168
169 public boolean isMoving() {
170 return isMoving;
171 }
172
173 public void undoGravityOverride() {
174 LayoutParams windowParams = bubble.getWindowParams();
175 xProperty.setValue(windowParams, xProperty.getValue(windowParams));
176 }
177
178 public void snapToBounds() {
179 ensureSprings();
180
181 moveXAnimation.animateToFinalPosition(relativeToRight(bubble.getWindowParams()) ? maxX : minX);
182 moveYAnimation.animateToFinalPosition(yProperty.getValue(bubble.getWindowParams()));
183 }
184
yueg81a77ff2017-12-05 10:29:03 -0800185 public int getMoveUpDistance(int deltaAllowed) {
186 int currentY = (int) yProperty.getValue(bubble.getWindowParams());
187 int currentDelta = maxY - currentY;
188 return currentDelta >= deltaAllowed ? 0 : deltaAllowed - currentDelta;
189 }
190
Eric Erfanian938468d2017-10-24 14:05:52 -0700191 @Override
192 public boolean onTouch(View v, MotionEvent event) {
193 float eventX = event.getRawX();
194 float eventY = event.getRawY();
195 switch (event.getActionMasked()) {
196 case MotionEvent.ACTION_DOWN:
197 firstX = eventX;
198 firstY = eventY;
199 velocityTracker = VelocityTracker.obtain();
200 break;
201 case MotionEvent.ACTION_MOVE:
202 if (isMoving || hasExceededTouchSlop(event)) {
203 if (!isMoving) {
204 isMoving = true;
205 bubble.onMoveStart();
yuegf473e1d2018-01-02 16:23:14 -0800206 bottomActionViewController.createAndShowBottomActionView();
Eric Erfanian938468d2017-10-24 14:05:52 -0700207 }
yuegf473e1d2018-01-02 16:23:14 -0800208 bottomActionViewController.highlightIfHover(eventX, eventY);
Eric Erfanian938468d2017-10-24 14:05:52 -0700209
210 ensureSprings();
211
212 moveXAnimation.animateToFinalPosition(clamp(eventX, minX, maxX));
213 moveYAnimation.animateToFinalPosition(clamp(eventY, minY, maxY));
214 }
215
216 velocityTracker.addMovement(event);
217 break;
218 case MotionEvent.ACTION_UP:
219 if (isMoving) {
220 ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
221 velocityTracker.computeCurrentVelocity(
222 1000, viewConfiguration.getScaledMaximumFlingVelocity());
223 float xVelocity = velocityTracker.getXVelocity();
224 float yVelocity = velocityTracker.getYVelocity();
225 boolean isFling = isFling(xVelocity, yVelocity);
226
227 if (isFling) {
228 Point target =
229 findTarget(
230 xVelocity,
231 yVelocity,
232 (int) xProperty.getValue(bubble.getWindowParams()),
233 (int) yProperty.getValue(bubble.getWindowParams()));
234
235 moveXAnimation.animateToFinalPosition(target.x);
236 moveYAnimation.animateToFinalPosition(target.y);
yuegf473e1d2018-01-02 16:23:14 -0800237 } else if (bottomActionViewController.isDismissHighlighted()) {
238 bubble.bottomActionDismiss();
239 } else if (bottomActionViewController.isEndCallHighlighted()) {
240 bubble.bottomActionEndCall();
Eric Erfanian938468d2017-10-24 14:05:52 -0700241 } else {
242 snapX();
243 }
244 isMoving = false;
245 bubble.onMoveFinish();
yuegf473e1d2018-01-02 16:23:14 -0800246 bottomActionViewController.destroyBottomActionView();
Eric Erfanian938468d2017-10-24 14:05:52 -0700247 } else {
248 v.performClick();
249 if (clickable) {
250 bubble.primaryButtonClick();
251 }
252 }
253 break;
254 default: // fall out
255 }
256 return true;
257 }
258
259 private void ensureSprings() {
260 if (moveXAnimation == null) {
261 moveXAnimation = new SpringAnimation(bubble.getWindowParams(), xProperty);
262 moveXAnimation.setSpring(new SpringForce());
263 moveXAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
yueg81a77ff2017-12-05 10:29:03 -0800264 // Moving when expanded makes expanded view INVISIBLE, and the whole view is not at the
265 // boundary. It's time to create a viewHolder.
266 moveXAnimation.addEndListener(
267 (animation, canceled, value, velocity) -> {
268 if (!isMoving && bubble.getDrawerVisibility() == View.INVISIBLE) {
269 bubble.replaceViewHolder();
270 }
271 });
Eric Erfanian938468d2017-10-24 14:05:52 -0700272 }
273
274 if (moveYAnimation == null) {
275 moveYAnimation = new SpringAnimation(bubble.getWindowParams(), yProperty);
276 moveYAnimation.setSpring(new SpringForce());
277 moveYAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
278 }
279 }
280
281 private Point findTarget(float xVelocity, float yVelocity, int startX, int startY) {
282 if (scroller == null) {
283 scroller = new Scroller(context);
284 scroller.setFriction(ViewConfiguration.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER);
285 }
286
287 // Find where a fling would end vertically
288 scroller.fling(startX, startY, (int) xVelocity, (int) yVelocity, minX, maxX, minY, maxY);
289 int targetY = scroller.getFinalY();
290 scroller.abortAnimation();
291
292 // If the x component of the velocity is above the minimum fling velocity, use velocity to
293 // determine edge. Otherwise use its starting position
294 boolean pullRight = isFling(xVelocity, 0) ? xVelocity > 0 : isOnRightHalf(startX);
295 return new Point(pullRight ? maxX : minX, targetY);
296 }
297
298 private boolean isFling(float xVelocity, float yVelocity) {
299 int minFlingVelocity =
300 ViewConfiguration.get(context).getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_FACTOR;
301 return getMagnitudeSquared(xVelocity, yVelocity) > minFlingVelocity * minFlingVelocity;
302 }
303
304 private boolean isOnRightHalf(float currentX) {
305 return currentX > (minX + maxX) / 2;
306 }
307
308 private void snapX() {
309 // Check if x value is closer to min or max
310 boolean pullRight = isOnRightHalf(xProperty.getValue(bubble.getWindowParams()));
311 moveXAnimation.animateToFinalPosition(pullRight ? maxX : minX);
312 }
313
314 private boolean relativeToRight(LayoutParams windowParams) {
315 return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
316 }
317
318 private boolean hasExceededTouchSlop(MotionEvent event) {
319 return getMagnitudeSquared(event.getRawX() - firstX, event.getRawY() - firstY)
320 > touchSlopSquared;
321 }
322
323 private float getMagnitudeSquared(float deltaX, float deltaY) {
324 return deltaX * deltaX + deltaY * deltaY;
325 }
326}