summaryrefslogtreecommitdiffstats
path: root/java/com/android/dialershared/bubble/MoveHandler.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/dialershared/bubble/MoveHandler.java')
-rw-r--r--java/com/android/dialershared/bubble/MoveHandler.java250
1 files changed, 250 insertions, 0 deletions
diff --git a/java/com/android/dialershared/bubble/MoveHandler.java b/java/com/android/dialershared/bubble/MoveHandler.java
new file mode 100644
index 000000000..8a21cd7e1
--- /dev/null
+++ b/java/com/android/dialershared/bubble/MoveHandler.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialershared.bubble;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.support.animation.FloatPropertyCompat;
+import android.support.animation.SpringAnimation;
+import android.support.animation.SpringForce;
+import android.support.annotation.NonNull;
+import android.support.v4.math.MathUtils;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.widget.Scroller;
+
+/** Handles touches and manages moving the bubble in response */
+class MoveHandler implements OnTouchListener {
+
+ // Amount the ViewConfiguration's minFlingVelocity will be scaled by for our own minVelocity
+ private static final int MIN_FLING_VELOCITY_FACTOR = 8;
+ // The friction multiplier to control how slippery the bubble is when flung
+ private static final float SCROLL_FRICTION_MULTIPLIER = 8f;
+
+ private final Context context;
+ private final WindowManager windowManager;
+ private final Bubble bubble;
+ private final int minX;
+ private final int minY;
+ private final int maxX;
+ private final int maxY;
+ private final int bubbleSize;
+ private final int shadowPaddingSize;
+ private final float touchSlopSquared;
+
+ private boolean isMoving;
+ private float firstX;
+ private float firstY;
+
+ private SpringAnimation moveXAnimation;
+ private SpringAnimation moveYAnimation;
+ private VelocityTracker velocityTracker;
+ private Scroller scroller;
+
+ // Handles the left/right gravity conversion and centering
+ private final FloatPropertyCompat<WindowManager.LayoutParams> xProperty =
+ new FloatPropertyCompat<LayoutParams>("xProperty") {
+ @Override
+ public float getValue(LayoutParams windowParams) {
+ int realX = windowParams.x;
+ realX = realX + bubbleSize / 2;
+ realX = realX + shadowPaddingSize;
+ if (relativeToRight(windowParams)) {
+ int displayWidth = context.getResources().getDisplayMetrics().widthPixels;
+ realX = displayWidth - realX;
+ }
+ return MathUtils.clamp(realX, minX, maxX);
+ }
+
+ @Override
+ public void setValue(LayoutParams windowParams, float value) {
+ int displayWidth = context.getResources().getDisplayMetrics().widthPixels;
+ boolean onRight = value > displayWidth / 2;
+ int centeringOffset = bubbleSize / 2 + shadowPaddingSize;
+ windowParams.x =
+ (int) (onRight ? (displayWidth - value - centeringOffset) : value - centeringOffset);
+ windowParams.gravity = Gravity.TOP | (onRight ? Gravity.RIGHT : Gravity.LEFT);
+ if (bubble.isShowing()) {
+ windowManager.updateViewLayout(bubble.getRootView(), windowParams);
+ }
+ }
+ };
+
+ private final FloatPropertyCompat<WindowManager.LayoutParams> yProperty =
+ new FloatPropertyCompat<LayoutParams>("yProperty") {
+ @Override
+ public float getValue(LayoutParams object) {
+ return MathUtils.clamp(object.y + bubbleSize + shadowPaddingSize, minY, maxY);
+ }
+
+ @Override
+ public void setValue(LayoutParams object, float value) {
+ object.y = (int) value - bubbleSize - shadowPaddingSize;
+ if (bubble.isShowing()) {
+ windowManager.updateViewLayout(bubble.getRootView(), object);
+ }
+ }
+ };
+
+ public MoveHandler(@NonNull View targetView, @NonNull Bubble bubble) {
+ this.bubble = bubble;
+ context = targetView.getContext();
+ windowManager = context.getSystemService(WindowManager.class);
+
+ bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size);
+ shadowPaddingSize =
+ context.getResources().getDimensionPixelOffset(R.dimen.bubble_shadow_padding_size);
+ minX =
+ context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_x)
+ + bubbleSize / 2;
+ minY =
+ context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_y)
+ + bubbleSize / 2;
+ maxX = context.getResources().getDisplayMetrics().widthPixels - minX;
+ maxY = context.getResources().getDisplayMetrics().heightPixels - minY;
+
+ // Squared because it will be compared against the square of the touch delta. This is more
+ // efficient than needing to take a square root.
+ touchSlopSquared = (float) Math.pow(ViewConfiguration.get(context).getScaledTouchSlop(), 2);
+
+ targetView.setOnTouchListener(this);
+ }
+
+ public boolean isMoving() {
+ return isMoving;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ float eventX = event.getRawX();
+ float eventY = event.getRawY();
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ firstX = eventX;
+ firstY = eventY;
+ velocityTracker = VelocityTracker.obtain();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (isMoving || hasExceededTouchSlop(event)) {
+ if (!isMoving) {
+ isMoving = true;
+ bubble.onMoveStart();
+ }
+
+ if (moveXAnimation == null) {
+ moveXAnimation = new SpringAnimation(bubble.getWindowParams(), xProperty);
+ moveXAnimation.setSpring(new SpringForce());
+ moveXAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
+ }
+ if (moveYAnimation == null) {
+ moveYAnimation = new SpringAnimation(bubble.getWindowParams(), yProperty);
+ moveYAnimation.setSpring(new SpringForce());
+ moveYAnimation.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
+ }
+
+ moveXAnimation.animateToFinalPosition(MathUtils.clamp(eventX, minX, maxX));
+ moveYAnimation.animateToFinalPosition(MathUtils.clamp(eventY, minY, maxY));
+ }
+
+ velocityTracker.addMovement(event);
+ break;
+ case MotionEvent.ACTION_UP:
+ if (isMoving) {
+ ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
+ velocityTracker.computeCurrentVelocity(
+ 1000, viewConfiguration.getScaledMaximumFlingVelocity());
+ float xVelocity = velocityTracker.getXVelocity();
+ float yVelocity = velocityTracker.getYVelocity();
+ boolean isFling = isFling(xVelocity, yVelocity);
+
+ if (isFling) {
+ Point target =
+ findTarget(
+ xVelocity,
+ yVelocity,
+ (int) xProperty.getValue(bubble.getWindowParams()),
+ (int) yProperty.getValue(bubble.getWindowParams()));
+
+ moveXAnimation.animateToFinalPosition(target.x);
+ moveYAnimation.animateToFinalPosition(target.y);
+ } else {
+ snapX();
+ }
+
+ bubble.onMoveFinish();
+ } else {
+ v.performClick();
+ bubble.primaryButtonClick();
+ }
+ isMoving = false;
+ break;
+ }
+ return true;
+ }
+
+ private Point findTarget(float xVelocity, float yVelocity, int startX, int startY) {
+ if (scroller == null) {
+ scroller = new Scroller(context);
+ scroller.setFriction(ViewConfiguration.getScrollFriction() * SCROLL_FRICTION_MULTIPLIER);
+ }
+
+ // Find where a fling would end vertically
+ scroller.fling(startX, startY, (int) xVelocity, (int) yVelocity, minX, maxX, minY, maxY);
+ int targetY = scroller.getFinalY();
+ scroller.abortAnimation();
+
+ // If the x component of the velocity is above the minimum fling velocity, use velocity to
+ // determine edge. Otherwise use its starting position
+ boolean pullRight = isFling(xVelocity, 0) ? xVelocity > 0 : isOnRightHalf(startX);
+ return new Point(pullRight ? maxX : minX, targetY);
+ }
+
+ private boolean isFling(float xVelocity, float yVelocity) {
+ int minFlingVelocity =
+ ViewConfiguration.get(context).getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_FACTOR;
+ return getMagnitudeSquared(xVelocity, yVelocity) > minFlingVelocity * minFlingVelocity;
+ }
+
+ private boolean isOnRightHalf(float currentX) {
+ return currentX > (minX + maxX) / 2;
+ }
+
+ private void snapX() {
+ // Check if x value is closer to min or max
+ boolean pullRight = isOnRightHalf(xProperty.getValue(bubble.getWindowParams()));
+ moveXAnimation.animateToFinalPosition(pullRight ? maxX : minX);
+ }
+
+ private boolean relativeToRight(LayoutParams windowParams) {
+ return (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
+ }
+
+ private boolean hasExceededTouchSlop(MotionEvent event) {
+ return getMagnitudeSquared(event.getRawX() - firstX, event.getRawY() - firstY)
+ > touchSlopSquared;
+ }
+
+ private float getMagnitudeSquared(float deltaX, float deltaY) {
+ return deltaX * deltaX + deltaY * deltaY;
+ }
+}