blob: 06efbd4b871e9ce6f2fe7ebe0f58b7cd9914a9c7 [file] [log] [blame]
Eric Erfanian2ca43182017-08-31 06:57:16 -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
sail3bcea982017-09-03 13:57:22 -070017package com.android.bubble;
Eric Erfanian2ca43182017-08-31 06:57:16 -070018
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 MoveHandler 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 Bubble 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;
51 private final int shadowPaddingSize;
52 private final float touchSlopSquared;
53
54 private boolean isMoving;
55 private float firstX;
56 private float firstY;
57
58 private SpringAnimation moveXAnimation;
59 private SpringAnimation moveYAnimation;
60 private VelocityTracker velocityTracker;
61 private Scroller scroller;
62
63 private static float clamp(float value, float min, float max) {
64 return Math.min(max, Math.max(min, value));
65 }
66
67 // Handles the left/right gravity conversion and centering
68 private final FloatPropertyCompat<WindowManager.LayoutParams> xProperty =
69 new FloatPropertyCompat<LayoutParams>("xProperty") {
70 @Override
71 public float getValue(LayoutParams windowParams) {
72 int realX = windowParams.x;
73 realX = realX + bubbleSize / 2;
74 realX = realX + shadowPaddingSize;
75 if (relativeToRight(windowParams)) {
76 int displayWidth = context.getResources().getDisplayMetrics().widthPixels;
77 realX = displayWidth - realX;
78 }
79 return clamp(realX, minX, maxX);
80 }
81
82 @Override
83 public void setValue(LayoutParams windowParams, float value) {
84 boolean wasOnRight = (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
85 int displayWidth = context.getResources().getDisplayMetrics().widthPixels;
86 boolean onRight;
87 Integer gravityOverride = bubble.getGravityOverride();
88 if (gravityOverride == null) {
89 onRight = value > displayWidth / 2;
90 } else {
91 onRight = (gravityOverride & Gravity.RIGHT) == Gravity.RIGHT;
92 }
93 int centeringOffset = bubbleSize / 2 + shadowPaddingSize;
94 windowParams.x =
95 (int) (onRight ? (displayWidth - value - centeringOffset) : value - centeringOffset);
96 windowParams.gravity = Gravity.TOP | (onRight ? Gravity.RIGHT : Gravity.LEFT);
97 if (wasOnRight != onRight) {
98 bubble.onLeftRightSwitch(onRight);
99 }
100 if (bubble.isVisible()) {
101 windowManager.updateViewLayout(bubble.getRootView(), windowParams);
102 }
103 }
104 };
105
106 private final FloatPropertyCompat<WindowManager.LayoutParams> yProperty =
107 new FloatPropertyCompat<LayoutParams>("yProperty") {
108 @Override
109 public float getValue(LayoutParams object) {
110 return clamp(object.y + bubbleSize + shadowPaddingSize, minY, maxY);
111 }
112
113 @Override
114 public void setValue(LayoutParams object, float value) {
115 object.y = (int) value - bubbleSize - shadowPaddingSize;
116 if (bubble.isVisible()) {
117 windowManager.updateViewLayout(bubble.getRootView(), object);
118 }
119 }
120 };
121
122 public MoveHandler(@NonNull View targetView, @NonNull Bubble bubble) {
123 this.bubble = bubble;
124 context = targetView.getContext();
125 windowManager = context.getSystemService(WindowManager.class);
126
127 bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
128 shadowPaddingSize =
129 context.getResources().getDimensionPixelOffset(R.dimen.bubble_shadow_padding_size);
130 minX =
131 context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_x)
132 + bubbleSize / 2;
133 minY =
134 context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_y)
135 + bubbleSize / 2;
136 maxX = context.getResources().getDisplayMetrics().widthPixels - minX;
137 maxY = context.getResources().getDisplayMetrics().heightPixels - minY;
138
139 // Squared because it will be compared against the square of the touch delta. This is more
140 // efficient than needing to take a square root.
141 touchSlopSquared = (float) Math.pow(ViewConfiguration.get(context).getScaledTouchSlop(), 2);
142
143 targetView.setOnTouchListener(this);
144 }
145
146 public boolean isMoving() {
147 return isMoving;
148 }
149
150 public void undoGravityOverride() {
151 LayoutParams windowParams = bubble.getWindowParams();
152 xProperty.setValue(windowParams, xProperty.getValue(windowParams));
153 }
154
155 public void snapToBounds() {
156 ensureSprings();
157
158 moveXAnimation.animateToFinalPosition(relativeToRight(bubble.getWindowParams()) ? maxX : minX);
159 moveYAnimation.animateToFinalPosition(yProperty.getValue(bubble.getWindowParams()));
160 }
161
162 @Override
163 public boolean onTouch(View v, MotionEvent event) {
164 float eventX = event.getRawX();
165 float eventY = event.getRawY();
166 switch (event.getActionMasked()) {
167 case MotionEvent.ACTION_DOWN:
168 firstX = eventX;
169 firstY = eventY;
170 velocityTracker = VelocityTracker.obtain();
171 break;
172 case MotionEvent.ACTION_MOVE:
173 if (isMoving || hasExceededTouchSlop(event)) {
174 if (!isMoving) {
175 isMoving = true;
176 bubble.onMoveStart();
177 }
178
179 ensureSprings();
180
181 moveXAnimation.animateToFinalPosition(clamp(eventX, minX, maxX));
182 moveYAnimation.animateToFinalPosition(clamp(eventY, minY, maxY));
183 }
184
185 velocityTracker.addMovement(event);
186 break;
187 case MotionEvent.ACTION_UP:
188 if (isMoving) {
189 ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
190 velocityTracker.computeCurrentVelocity(
191 1000, viewConfiguration.getScaledMaximumFlingVelocity());
192 float xVelocity = velocityTracker.getXVelocity();
193 float yVelocity = velocityTracker.getYVelocity();
194 boolean isFling = isFling(xVelocity, yVelocity);
195
196 if (isFling) {
197 Point target =
198 findTarget(
199 xVelocity,
200 yVelocity,
201 (int) xProperty.getValue(bubble.getWindowParams()),
202 (int) yProperty.getValue(bubble.getWindowParams()));
203
204 moveXAnimation.animateToFinalPosition(target.x);
205 moveYAnimation.animateToFinalPosition(target.y);
206 } else {
207 snapX();
208 }
209 isMoving = false;
210 bubble.onMoveFinish();
211 } else {
212 v.performClick();
213 bubble.primaryButtonClick();
214 }
215 break;
216 default: // fall out
217 }
218 return true;
219 }
220
221 private void ensureSprings() {
222 if (moveXAnimation == null) {
223 moveXAnimation = new SpringAnimation(bubble.getWindowParams(), xProperty);
224 moveXAnimation.setSpring(new SpringForce());
225 moveXAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
226 }
227
228 if (moveYAnimation == null) {
229 moveYAnimation = new SpringAnimation(bubble.getWindowParams(), yProperty);
230 moveYAnimation.setSpring(new SpringForce());
231 moveYAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
232 }
233 }
234
235 private Point findTarget(float xVelocity, float yVelocity, int startX, int startY) {
236 if (scroller == null) {
237 scroller = new Scroller(context);
238 scroller.setFriction(ViewConfiguration.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER);
239 }
240
241 // Find where a fling would end vertically
242 scroller.fling(startX, startY, (int) xVelocity, (int) yVelocity, minX, maxX, minY, maxY);
243 int targetY = scroller.getFinalY();
244 scroller.abortAnimation();
245
246 // If the x component of the velocity is above the minimum fling velocity, use velocity to
247 // determine edge. Otherwise use its starting position
248 boolean pullRight = isFling(xVelocity, 0) ? xVelocity > 0 : isOnRightHalf(startX);
249 return new Point(pullRight ? maxX : minX, targetY);
250 }
251
252 private boolean isFling(float xVelocity, float yVelocity) {
253 int minFlingVelocity =
254 ViewConfiguration.get(context).getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_FACTOR;
255 return getMagnitudeSquared(xVelocity, yVelocity) > minFlingVelocity * minFlingVelocity;
256 }
257
258 private boolean isOnRightHalf(float currentX) {
259 return currentX > (minX + maxX) / 2;
260 }
261
262 private void snapX() {
263 // Check if x value is closer to min or max
264 boolean pullRight = isOnRightHalf(xProperty.getValue(bubble.getWindowParams()));
265 moveXAnimation.animateToFinalPosition(pullRight ? maxX : minX);
266 }
267
268 private boolean relativeToRight(LayoutParams windowParams) {
269 return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
270 }
271
272 private boolean hasExceededTouchSlop(MotionEvent event) {
273 return getMagnitudeSquared(event.getRawX() - firstX, event.getRawY() - firstY)
274 > touchSlopSquared;
275 }
276
277 private float getMagnitudeSquared(float deltaX, float deltaY) {
278 return deltaX * deltaX + deltaY * deltaY;
279 }
280}