blob: 221ff2e0c5549c2fce71fa764a7351bef2b479cf [file] [log] [blame]
/*
* Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
*
* Author: Adrien Beraud <adrien.beraud@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* Additional permission under GNU GPL version 3 section 7:
*
* If you modify this program, or any covered work, by linking or
* combining it with the OpenSSL project's OpenSSL library (or a
* modified version of that library), containing parts covered by the
* terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
* grants you additional permission to convey the resulting work.
* Corresponding Source for a non-source form of such a combination
* shall include the source code for the parts of OpenSSL used as well
* as that of the covered work.
*/
package cx.ring.model;
import android.graphics.Bitmap;
import android.graphics.PointF;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
public class BubbleModel {
private static final String TAG = BubbleModel.class.getSimpleName();
public enum State {
None, Incoming, Outgoing, Incall
}
public interface ModelCallback {
public void bubbleGrabbed(Bubble b);
/**
* A bubble is put beyond view borders.
* @param b The bubble
* @return true if the bubble should be ejected, false if it should bounce.
*/
public boolean bubbleEjected(Bubble b);
}
public interface ActionGroupCallback {
/**
* Called when a bubble is on the "active" zone of the attractor.
*
* @param b The bubble that is on the attractor.
* @return true if the bubble should be removed from the model, false otherwise.
*/
public boolean onBubbleAction(Bubble b, int action);
}
static public class ActionGroup {
public boolean enabled = true;
private final ArrayList<Attractor> buttons = new ArrayList<Attractor>();
private final ActionGroupCallback callback;
final private float margin;
public Bubble bubble = null;
public long viewStart = 0;
private final float appearTime;
private final float disappearTime;
public ActionGroup(ActionGroupCallback cb, float btn_margin, float appear_time, float disapear_time) {
this.callback = cb;
this.margin = btn_margin;
appearTime = appear_time;
disappearTime = disapear_time;
}
public ArrayList<Attractor> getActions() {
return buttons;
}
public void addAction(final int id, Bitmap btn, String name, float size) {
final Attractor a = new Attractor(name, size, new Attractor.Callback() {
@Override
public boolean onBubbleSucked(Bubble b) {
if (!enabled) return false;
return callback.onBubbleAction(b, id);
}
}, btn);
buttons.add(a);
}
public void show(Bubble b) {
this.bubble = b;
long now = System.nanoTime();
double dt = (now - viewStart) / 1000000000.;
double r = 1. - Math.min(dt / disappearTime, 1.);
this.enabled = true;
this.viewStart = now - (long)(r * appearTime * 1000000000.);
}
public void hide() {
long now = System.nanoTime();
double dt = (now - viewStart) / 1000000000.;
double r = 1. - Math.min(dt / appearTime, 1.);
this.enabled = false;
viewStart = now - (long)(r * disappearTime * 1000000000.);
}
public float getVisibility(long now) {
double dt = (now - viewStart) / 1000000000.;
if (enabled)
return (float) Math.min(dt / appearTime, 1.);
else
return 1.f - (float) Math.min(dt / disappearTime, 1.);
}
public void order(int w, int h) {
int n = buttons.size();
if (n == 0) return;
//float y = h - 3 * buttons.get(0).radius;
float y = bubble.getPosY() - margin - bubble.radius;
final float WIDTH = 2 * buttons.get(0).radius;
float totw = n * WIDTH + (n-1) * margin;
float xs = (w - totw) / 2 + buttons.get(0).radius;
float xstep = WIDTH+margin;
for (int i=0; i<n; i++) {
buttons.get(i).setPos(xs + i*xstep, y);
}
}
}
private final ModelCallback callback;
private long lastUpdate = 0;
private int width, height;
private final ArrayList<Bubble> bubbles = new ArrayList<Bubble>();
private final ArrayList<Attractor> attractors = new ArrayList<Attractor>();
private ActionGroup actions = null;
private static final double BUBBLE_RETURN_TIME_HALF_LIFE = .3;
private static final double BUBBLE_RETURN_TIME_LAMBDA = Math.log(2) / BUBBLE_RETURN_TIME_HALF_LIFE;
private static final double FRICTION_VISCOUS = Math.log(2) / .2f; // Viscous friction factor
private static final float BUBBLE_MAX_SPEED = 2500.f; // px.s⁻¹ : Max target speed in px/sec
private static final float ATTRACTOR_SMOOTH_DIST = 50.f; // px : Size of the "gravity hole" around the attractor
private static final float ATTRACTOR_STALL_DIST = 15.f; // px : Size of the "gravity hole" flat bottom
private static final float ATTRACTOR_DIST_SUCK = 20.f; // px
private static final float BORDER_REPULSION = 60000; // px.s⁻²
private final float border_repulsion;
private final float bubble_max_speed;
private final float attractor_smooth_dist;
private final float attractor_stall_dist;
private final float attractor_dist_suck;
private final float density;
private float circle_radius;
private final PointF circle_center = new PointF();
public State curState = State.None;
public BubbleModel(float screen_density, ModelCallback cb) {
Log.d(TAG, "Creating BubbleModel");
callback = cb;
this.density = screen_density;
attractor_dist_suck = ATTRACTOR_DIST_SUCK * density;
bubble_max_speed = BUBBLE_MAX_SPEED * density;
attractor_smooth_dist = ATTRACTOR_SMOOTH_DIST * density;
attractor_stall_dist = ATTRACTOR_STALL_DIST * density;
border_repulsion = BORDER_REPULSION * density;
}
public void setSize(int w, int h, float bubble_sz)
{
width = w;
height = h;
for (Attractor a : attractors) {
a.setSize(w, h);
}
if (actions != null) {
actions.order(width, height);
}
circle_radius = Math.min(width, height) / 2 - bubble_sz;
circle_center.set(width/2, height/2);
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public float getCircleSize() {
return circle_radius;
}
public PointF getCircleCenter() {
return circle_center;
}
public void addBubble(Bubble b) {
bubbles.add(b);
}
public List<Bubble> getBubbles() {
return bubbles;
}
public Bubble getBubble(String call) {
for (Bubble b : bubbles) {
if (!b.isUser && b.callIDEquals(call))
return b;
}
return null;
}
public void removeBubble(SipCall sipCall) {
bubbles.remove(getBubble(sipCall.getCallId()));
}
public void addAttractor(Attractor a) {
attractors.add(a);
}
public List<Attractor> getAttractors() {
return attractors;
}
public void clearAttractors() {
attractors.clear();
}
public void setActions(Bubble b, ActionGroup actions) {
actions.show(b);
/*actions.enabled = true;
actions.viewStart = 0;*/
actions.order(width, height);
this.actions = actions;
}
public ActionGroup getActions() {
return actions;
}
public void clearActions() {
this.actions = null;
}
public Bubble getUser() {
for (Bubble b : bubbles) {
if (b.isUser)
return b;
}
return null;
}
public void clear() {
clearAttractors();
bubbles.clear();
}
public void grabBubble(Bubble b) {
b.grab();
callback.bubbleGrabbed(b);
}
public void ungrabBubble(Bubble b) {
b.ungrab();
}
public void ejectBubble(Bubble b) {
boolean eject = callback.bubbleEjected(b);
if (eject) {
b.close();
}
}
public void update() {
/* INFO: if you get a NullPointer or OutOfBounds exception here,
* you may have some wrong/missing locks. */
long now = System.nanoTime();
// Do nothing if lastUpdate is in the future.
if (lastUpdate > now)
return;
double ddt = Math.min((now - lastUpdate) / 1000000000.0, .2);
lastUpdate = now;
float dt = (float) ddt;
//int attr_n = attractors.size();
boolean actionAttr = false;
// Iterators should not be used in frequently called methods
// to avoid garbage collection glitches caused by iterator objects.
for (int i = 0, n = bubbles.size(); i < n; i++) {
Bubble b = bubbles.get(i);
if (b.markedToDie) {
b.update(dt);
continue;
}
float bx = b.getPosX(), by = b.getPosY();
Attractor attractor = null;
PointF attractor_pos = b.attractionPoint;
float attractor_dist = (attractor_pos.x - bx) * (attractor_pos.x - bx) + (attractor_pos.y - by) * (attractor_pos.y - by);
boolean actionGrp = actions != null && actions.enabled && actions.bubble == b;
final List<Attractor> attr = (actionGrp) ? actions.getActions() : attractors;
for (Attractor t : attr) {
float dx = t.pos.x - bx, dy = t.pos.y - by;
float adist = dx * dx + dy * dy;
if (adist < attractor_dist) {
attractor = t;
attractor_pos = t.pos;
attractor_dist = adist;
}
}
b.attractor = attractor;
if (!b.isGrabbed()) {
if (actionGrp) {
for (Attractor anAttr : attr) {
if (anAttr == attractor) {
actionAttr = true;
break;
}
}
}
// float friction_coef = 1.f-FRICTION_VISCOUS*dt;
double friction_coef = 1 + Math.expm1(-FRICTION_VISCOUS * ddt);
b.speed.x *= friction_coef;
b.speed.y *= friction_coef;
float target_speed;
float tdx = attractor_pos.x - bx, tdy = attractor_pos.y - by;
float dist = Math.max(1.f, (float) Math.sqrt(tdx * tdx + tdy * tdy));
if (dist > attractor_smooth_dist)
target_speed = bubble_max_speed;
else if (dist < attractor_stall_dist)
target_speed = 0;
else {
float a = (dist - attractor_stall_dist) / (attractor_smooth_dist - attractor_stall_dist);
target_speed = bubble_max_speed * a;
}
if (attractor != null) {
if (dist > attractor_smooth_dist)
b.setTargetScale(1.f);
else if (dist < attractor_stall_dist)
b.setTargetScale(2f);
else {
float a = (dist - attractor_stall_dist) / (attractor_smooth_dist - attractor_stall_dist);
b.setTargetScale(a * .8f + .2f);
}
}
// border repulsion
if (bx < 0 && b.speed.x < 0) {
b.speed.x += dt * border_repulsion;
} else if (bx > width && b.speed.x > 0) {
b.speed.x -= dt * border_repulsion;
}
if (by < 0 && b.speed.y < 0) {
b.speed.y += dt * border_repulsion;
} else if (by > height && b.speed.y > 0) {
b.speed.y -= dt * border_repulsion;
}
b.speed.x += dt * target_speed * tdx / dist;
b.speed.y += dt * target_speed * tdy / dist;
double edt = -Math.expm1(-BUBBLE_RETURN_TIME_LAMBDA * ddt);
double dx = (attractor_pos.x - bx) * edt + Math.min(bubble_max_speed, b.speed.x) * dt;
double dy = (attractor_pos.y - by) * edt + Math.min(bubble_max_speed, b.speed.y) * dt;
// Log.w(TAG, "update dx="+dt+" dy="+dy);
b.setPos((float) (bx + dx), (float) (by + dy));
/*if (b.isOnBorder(width, height)) {
ejectBubble(b);
}*/
if (attractor != null && attractor_dist < attractor_dist_suck * attractor_dist_suck) {
boolean removeBubble = attractor.callback.onBubbleSucked(b);
if (removeBubble) {
bubbles.remove(b);
n--;
} else {
b.setTargetScale(1.f);
}
if (actionGrp) {
actions.hide();
}
}
} else {
actionAttr = true;
}
b.update(dt);
}
if (actions != null && actions.enabled && !actionAttr) {
actions.hide();
}
}
}