blob: c1063fa54ff2e283227ac5b652fd95c031a6c36e [file] [log] [blame]
/*
* Copyright (C) 2018 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.systemui.bubbles;
import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.view.View;
import android.view.WindowManager;
import com.android.systemui.bubbles.BubbleTouchHandler.FloatingView;
import java.util.Arrays;
/**
* Math and animators to move bubbles around the screen.
*
* TODO: straight up copy paste from old prototype -- consider physics, see if bubble & pip
* movements can be unified maybe?
*/
public class BubbleMovementHelper {
private static final int MAGNET_ANIM_TIME = 150;
public static final int EDGE_OVERLAP = 0;
private Context mContext;
private Point mDisplaySize;
public BubbleMovementHelper(Context context) {
mContext = context;
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mDisplaySize = new Point();
wm.getDefaultDisplay().getSize(mDisplaySize);
}
/**
* @return the distance between the two provided points.
*/
static double distance(Point p1, Point p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
/**
* @return the y value of a line defined by y = mx+b
*/
static float findY(float m, float b, float x) {
return (m * x) + b;
}
/**
* @return the x value of a line defined by y = mx+b
*/
static float findX(float m, float b, float y) {
return (y - b) / m;
}
/**
* Determines a point on the edge of the screen based on the velocity and position.
*/
public Point getPointOnEdge(View bv, Point p, float velX, float velY) {
// Find the slope and the y-intercept
velX = velX == 0 ? 1 : velX;
final float m = velY / velX;
final float b = p.y - m * p.x;
// There are two lines it can intersect, find the two points
Point pointHoriz = new Point();
Point pointVert = new Point();
if (velX > 0) {
// right
pointHoriz.x = mDisplaySize.x;
pointHoriz.y = (int) findY(m, b, mDisplaySize.x);
} else {
// left
pointHoriz.x = EDGE_OVERLAP;
pointHoriz.y = (int) findY(m, b, 0);
}
if (velY > 0) {
// bottom
pointVert.x = (int) findX(m, b, mDisplaySize.y);
pointVert.y = mDisplaySize.y - getNavBarHeight();
} else {
// top
pointVert.x = (int) findX(m, b, 0);
pointVert.y = EDGE_OVERLAP;
}
// Use the point that's closest to the start position
final double distanceToVertPoint = distance(p, pointVert);
final double distanceToHorizPoint = distance(p, pointHoriz);
boolean useVert = distanceToVertPoint < distanceToHorizPoint;
// Check if we're being flung along the current edge, use opposite point in this case
// XXX: on*Edge methods should actually use 'down' position of view and compare 'up' but
// this works well enough for now
if (onSideEdge(bv, p) && Math.abs(velY) > Math.abs(velX)) {
// Flinging along left or right edge, favor vert edge
useVert = true;
} else if (onTopBotEdge(bv, p) && Math.abs(velX) > Math.abs(velY)) {
// Flinging along top or bottom edge
useVert = false;
}
if (useVert) {
pointVert.x = capX(pointVert.x, bv);
pointVert.y = capY(pointVert.y, bv);
return pointVert;
}
pointHoriz.x = capX(pointHoriz.x, bv);
pointHoriz.y = capY(pointHoriz.y, bv);
return pointHoriz;
}
/**
* @return whether the view is on a side edge of the screen (i.e. left or right).
*/
public boolean onSideEdge(View fv, Point p) {
return p.x + fv.getWidth() + EDGE_OVERLAP <= mDisplaySize.x
- EDGE_OVERLAP
|| p.x >= EDGE_OVERLAP;
}
/**
* @return whether the view is on a top or bottom edge of the screen.
*/
public boolean onTopBotEdge(View bv, Point p) {
return p.y >= getStatusBarHeight() + EDGE_OVERLAP
|| p.y + bv.getHeight() + EDGE_OVERLAP <= mDisplaySize.y
- EDGE_OVERLAP;
}
/**
* @return constrained x value based on screen size and how much a view can overlap with a side
* edge.
*/
public int capX(float x, View bv) {
// Floating things can't stick to top or bottom edges, so figure out if it's closer to
// left or right and just use that side + the overlap.
final float centerX = x + bv.getWidth() / 2;
if (centerX > mDisplaySize.x / 2) {
// Right side
return mDisplaySize.x - bv.getWidth() - EDGE_OVERLAP;
} else {
// Left side
return EDGE_OVERLAP;
}
}
/**
* @return constrained y value based on screen size and how much a view can overlap with a top
* or bottom edge.
*/
public int capY(float y, View bv) {
final int height = bv.getHeight();
if (y < getStatusBarHeight() + EDGE_OVERLAP) {
return getStatusBarHeight() + EDGE_OVERLAP;
}
if (y + height + EDGE_OVERLAP > mDisplaySize.y - EDGE_OVERLAP) {
return mDisplaySize.y - height - EDGE_OVERLAP;
}
return (int) y;
}
/**
* Animation to translate the provided view.
*/
public AnimatorSet animateMagnetTo(final BubbleStackView bv) {
Point pos = bv.getPosition();
// Find the distance to each edge
final int leftDistance = pos.x;
final int rightDistance = mDisplaySize.x - leftDistance;
final int topDistance = pos.y;
final int botDistance = mDisplaySize.y - topDistance;
int smallest;
// Find the closest one
int[] distances = {
leftDistance, rightDistance, topDistance, botDistance
};
Arrays.sort(distances);
smallest = distances[0];
// Animate to the closest edge
Point p = new Point();
if (smallest == leftDistance) {
p.x = capX(EDGE_OVERLAP, bv);
p.y = capY(topDistance, bv);
}
if (smallest == rightDistance) {
p.x = capX(mDisplaySize.x, bv);
p.y = capY(topDistance, bv);
}
if (smallest == topDistance) {
p.x = capX(leftDistance, bv);
p.y = capY(0, bv);
}
if (smallest == botDistance) {
p.x = capX(leftDistance, bv);
p.y = capY(mDisplaySize.y, bv);
}
return getTranslateAnim(bv, p, MAGNET_ANIM_TIME);
}
/**
* Animation to fling the provided view.
*/
public AnimatorSet animateFlingTo(final BubbleStackView bv, float velX, float velY) {
Point pos = bv.getPosition();
Point endPos = getPointOnEdge(bv, pos, velX, velY);
endPos = new Point(capX(endPos.x, bv), capY(endPos.y, bv));
final double distance = Math.sqrt(Math.pow(endPos.x - pos.x, 2)
+ Math.pow(endPos.y - pos.y, 2));
final float sumVel = Math.abs(velX) + Math.abs(velY);
final int duration = Math.max(Math.min(200, (int) (distance * 1000f / (sumVel / 2))), 50);
return getTranslateAnim(bv, endPos, duration);
}
/**
* Animation to translate the provided view.
*/
public AnimatorSet getTranslateAnim(final FloatingView v, Point p, int duration) {
return getTranslateAnim(v, p, duration, 0);
}
/**
* Animation to translate the provided view.
*/
public AnimatorSet getTranslateAnim(final FloatingView v, Point p,
int duration, int startDelay) {
return getTranslateAnim(v, p, duration, startDelay, null);
}
/**
* Animation to translate the provided view.
*
* @param v the view to translate.
* @param p the point to translate to.
* @param duration the duration of the animation.
* @param startDelay the start delay of the animation.
* @param listener the listener to add to the animation.
*
* @return the animation.
*/
public static AnimatorSet getTranslateAnim(final FloatingView v, Point p, int duration,
int startDelay, AnimatorListener listener) {
Point curPos = v.getPosition();
final ValueAnimator animX = ValueAnimator.ofFloat(curPos.x, p.x);
animX.setDuration(duration);
animX.setStartDelay(startDelay);
animX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
v.setPositionX((int) value);
}
});
final ValueAnimator animY = ValueAnimator.ofFloat(curPos.y, p.y);
animY.setDuration(duration);
animY.setStartDelay(startDelay);
animY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
v.setPositionY((int) value);
}
});
if (listener != null) {
animY.addListener(listener);
}
AnimatorSet set = new AnimatorSet();
set.playTogether(animX, animY);
set.setInterpolator(FAST_OUT_SLOW_IN);
return set;
}
// TODO -- now that this is in system we should be able to get these better, but ultimately
// makes more sense to move to movement bounds style a la PIP
/**
* Returns the status bar height.
*/
public int getStatusBarHeight() {
Resources res = mContext.getResources();
int resourceId = res.getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
return res.getDimensionPixelSize(resourceId);
}
return 0;
}
/**
* Returns the status bar height.
*/
public int getNavBarHeight() {
Resources res = mContext.getResources();
int resourceId = res.getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId > 0) {
return res.getDimensionPixelSize(resourceId);
}
return 0;
}
}