/* * 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.newbubble; 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.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 NewMoveHandler 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 = 4f; private final Context context; private final WindowManager windowManager; private final NewBubble bubble; private final int minX; private final int minY; private final int maxX; private final int maxY; private final int bubbleSize; private final float touchSlopSquared; private boolean clickable = true; private boolean isMoving; private float firstX; private float firstY; private SpringAnimation moveXAnimation; private SpringAnimation moveYAnimation; private VelocityTracker velocityTracker; private Scroller scroller; private static float clamp(float value, float min, float max) { return Math.min(max, Math.max(min, value)); } // Handles the left/right gravity conversion and centering private final FloatPropertyCompat xProperty = new FloatPropertyCompat("xProperty") { @Override public float getValue(LayoutParams windowParams) { int realX = windowParams.x; realX = realX + bubbleSize / 2; if (relativeToRight(windowParams)) { int displayWidth = context.getResources().getDisplayMetrics().widthPixels; realX = displayWidth - realX; } return clamp(realX, minX, maxX); } @Override public void setValue(LayoutParams windowParams, float value) { int displayWidth = context.getResources().getDisplayMetrics().widthPixels; boolean onRight; Integer gravityOverride = bubble.getGravityOverride(); if (gravityOverride == null) { onRight = value > displayWidth / 2; } else { onRight = (gravityOverride & Gravity.RIGHT) == Gravity.RIGHT; } int centeringOffset = bubbleSize / 2; windowParams.x = (int) (onRight ? (displayWidth - value - centeringOffset) : value - centeringOffset); windowParams.gravity = Gravity.TOP | (onRight ? Gravity.RIGHT : Gravity.LEFT); if (bubble.isVisible()) { windowManager.updateViewLayout(bubble.getRootView(), windowParams); } } }; private final FloatPropertyCompat yProperty = new FloatPropertyCompat("yProperty") { @Override public float getValue(LayoutParams object) { return clamp(object.y + bubbleSize, minY, maxY); } @Override public void setValue(LayoutParams object, float value) { object.y = (int) value - bubbleSize; if (bubble.isVisible()) { windowManager.updateViewLayout(bubble.getRootView(), object); } } }; public NewMoveHandler(@NonNull View targetView, @NonNull NewBubble bubble) { this.bubble = bubble; context = targetView.getContext(); windowManager = context.getSystemService(WindowManager.class); bubbleSize = context.getResources().getDimensionPixelSize(R.dimen.bubble_size); minX = context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_horizontal) + bubbleSize / 2; minY = context.getResources().getDimensionPixelOffset(R.dimen.bubble_safe_margin_vertical) + 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 void setClickable(boolean clickable) { this.clickable = clickable; } public boolean isMoving() { return isMoving; } public void undoGravityOverride() { LayoutParams windowParams = bubble.getWindowParams(); xProperty.setValue(windowParams, xProperty.getValue(windowParams)); } public void snapToBounds() { ensureSprings(); moveXAnimation.animateToFinalPosition(relativeToRight(bubble.getWindowParams()) ? maxX : minX); moveYAnimation.animateToFinalPosition(yProperty.getValue(bubble.getWindowParams())); } @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(); } ensureSprings(); moveXAnimation.animateToFinalPosition(clamp(eventX, minX, maxX)); moveYAnimation.animateToFinalPosition(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(); } isMoving = false; bubble.onMoveFinish(); } else { v.performClick(); if (clickable) { bubble.primaryButtonClick(); } } break; default: // fall out } return true; } private void ensureSprings() { 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); } } 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; } }