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