/* | |
* Copyright (C) 2004-2014 Savoir-Faire Linux Inc. | |
* | |
* Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com> | |
* Author: Adrien BĂ©raud <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.content.Context; | |
import android.content.res.Resources; | |
import android.graphics.*; | |
import android.graphics.Paint.Align; | |
import android.opengl.GLSurfaceView; | |
import android.util.AttributeSet; | |
import android.util.FloatMath; | |
import android.util.Log; | |
import android.view.GestureDetector; | |
import android.view.GestureDetector.OnGestureListener; | |
import android.view.MotionEvent; | |
import android.view.SurfaceHolder; | |
import android.view.View; | |
import android.view.View.OnTouchListener; | |
import android.view.animation.DecelerateInterpolator; | |
import android.view.animation.Interpolator; | |
import android.view.animation.OvershootInterpolator; | |
import cx.ring.R; | |
import java.util.List; | |
public class BubblesView extends GLSurfaceView implements SurfaceHolder.Callback, OnTouchListener { | |
private static final String TAG = BubblesView.class.getSimpleName(); | |
private BubblesThread thread = null; | |
private BubbleModel model; | |
private Paint black_name_paint = new Paint(Paint.ANTI_ALIAS_FLAG); | |
private Paint white_name_paint = new Paint(Paint.ANTI_ALIAS_FLAG); | |
private Paint canvas_paint = new Paint(); | |
private Paint circle_paint = new Paint(Paint.ANTI_ALIAS_FLAG); | |
private Paint action_paint = new Paint(); | |
static private final Interpolator interpolator = new OvershootInterpolator(2.f); | |
static private final Interpolator interpolator_dec = new DecelerateInterpolator(); | |
private final Bitmap ic_bg; | |
private final Bitmap ic_bg_sel; | |
private GestureDetector gDetector; | |
//private float density; | |
private float textDensity; | |
private float bubbleActionTextDistMin; | |
private float bubbleActionTextDistMax; | |
private boolean dragging_bubble = false; | |
public BubblesView(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
final Resources r = getResources(); | |
//density = r.getDisplayMetrics().density; | |
textDensity = r.getDisplayMetrics().scaledDensity; | |
bubbleActionTextDistMin = r.getDimension(R.dimen.bubble_action_textdistmin); | |
bubbleActionTextDistMax = r.getDimension(R.dimen.bubble_action_textdistmax); | |
ic_bg = BitmapFactory.decodeResource(r, R.drawable.ic_bg); | |
ic_bg_sel = BitmapFactory.decodeResource(r, R.drawable.ic_bg_sel); | |
if (isInEditMode()) return; | |
SurfaceHolder holder = getHolder(); | |
holder.addCallback(this); | |
this.setZOrderOnTop(true); // necessary | |
holder.setFormat(PixelFormat.TRANSLUCENT); | |
// create thread only; it's started in surfaceCreated() | |
createThread(); | |
setOnTouchListener(this); | |
setFocusable(true); | |
black_name_paint.setTextSize(18 * textDensity); | |
black_name_paint.setColor(0xFF303030); | |
black_name_paint.setTextAlign(Align.CENTER); | |
white_name_paint.setTextSize(18 * textDensity); | |
white_name_paint.setColor(0xFFEEEEEE); | |
white_name_paint.setTextAlign(Align.CENTER); | |
circle_paint.setStyle(Paint.Style.STROKE); | |
circle_paint.setColor(r.getColor(R.color.darker_gray)); | |
circle_paint.setXfermode(null); | |
gDetector = new GestureDetector(getContext(), new BubbleGestureListener()); | |
gDetector.setIsLongpressEnabled(false); | |
} | |
private void createThread() { | |
if (thread != null) | |
return; | |
thread = new BubblesThread(getHolder(), getContext()); | |
if (model != null) | |
thread.setModel(model); | |
} | |
public void setModel(BubbleModel model) { | |
this.model = model; | |
thread.setModel(model); | |
} | |
/* | |
* @Override public void onWindowFocusChanged(boolean hasWindowFocus) { if (!hasWindowFocus) { thread.pause(); } } | |
*/ | |
@Override | |
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { | |
Log.w(TAG, "surfaceChanged " + width + "-" + height); | |
/*if (height < model.getHeight()) // probably showing the keyboard, don't move! | |
return; | |
thread.setSurfaceSize(width, height);*/ | |
} | |
/* | |
* Callback invoked when the Surface has been created and is ready to be used. | |
*/ | |
@Override | |
public void surfaceCreated(SurfaceHolder holder) { | |
// start the thread here so that we don't busy-wait in run() | |
// waiting for the surface to be created | |
createThread(); | |
Log.w(TAG, "surfaceCreated"); | |
thread.setRunning(true); | |
thread.start(); | |
} | |
/* | |
* Callback invoked when the Surface has been destroyed and must no longer be touched. WARNING: after this method returns, the Surface/Canvas must | |
* never be touched again! | |
*/ | |
@Override | |
public void surfaceDestroyed(SurfaceHolder holder) { | |
// we have to tell thread to shut down & wait for it to finish, or else | |
// it might touch the Surface after we return and explode | |
Log.w(TAG, "surfaceDestroyed"); | |
boolean retry = true; | |
thread.setRunning(false); | |
thread.setPaused(false); | |
while (retry) { | |
try { | |
Log.w(TAG, "joining..."); | |
thread.join(); | |
retry = false; | |
} catch (InterruptedException ignored) { | |
} | |
} | |
Log.w(TAG, "done"); | |
thread = null; | |
} | |
public boolean isDraggingBubble() { | |
return dragging_bubble; | |
} | |
class BubblesThread extends Thread { | |
private boolean running = false; | |
public boolean suspendFlag = false; | |
private SurfaceHolder surfaceHolder; | |
BubbleModel model = null; | |
public BubblesThread(SurfaceHolder holder, Context context) { | |
surfaceHolder = holder; | |
} | |
public void setModel(BubbleModel model) { | |
this.model = model; | |
} | |
@Override | |
public void run() { | |
while (running) { | |
Canvas c = null; | |
try { | |
if (suspendFlag) { | |
synchronized (this) { | |
while (suspendFlag) { | |
try { | |
wait(); | |
} catch (InterruptedException e) { | |
// TODO Auto-generated catch block | |
e.printStackTrace(); | |
} | |
} | |
} | |
} else { | |
c = surfaceHolder.lockCanvas(null); | |
// for the case the surface is destroyed while already in the loop | |
if (c == null || model == null) | |
continue; | |
synchronized (model) { | |
model.update(); | |
} | |
synchronized (surfaceHolder) { | |
// Log.w(TAG, "Thread doDraw"); | |
synchronized (model) { | |
doDraw(c); | |
} | |
} | |
} | |
} finally { | |
if (c != null) | |
surfaceHolder.unlockCanvasAndPost(c); | |
} | |
} | |
} | |
public void setPaused(boolean wantToPause) { | |
synchronized (this) { | |
suspendFlag = wantToPause; | |
notify(); | |
} | |
} | |
public void setRunning(boolean b) { | |
running = b; | |
} | |
/** | |
* got multiple IndexOutOfBoundsException, when switching calls. //FIXME | |
* | |
* @param canvas | |
*/ | |
private void doDraw(Canvas canvas) { | |
List<Bubble> bubbles = model.getBubbles(); | |
List<Attractor> attractors = model.getAttractors(); | |
BubbleModel.ActionGroup actions = model.getActions(); | |
long now = System.nanoTime(); | |
canvas_paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); | |
canvas.drawPaint(canvas_paint); | |
canvas_paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); | |
if (model.curState == BubbleModel.State.Incall || model.curState == BubbleModel.State.Outgoing) { | |
PointF center = model.getCircleCenter(); | |
canvas.drawCircle(center.x, center.y, model.getCircleSize(), circle_paint); | |
} | |
for (Attractor a : attractors) { | |
canvas.drawBitmap(a.getBitmap(), null, a.getBounds(), null); | |
} | |
Bubble drawLater = (actions == null) ? null : actions.bubble; | |
for (Bubble b : bubbles) { | |
if (b == drawLater) continue; | |
canvas.drawBitmap(b.getBitmap(), null, b.getBounds(), null); | |
canvas.drawText(b.getName(), b.getPosX(), b.getPosY() - b.getRetractedRadius() * 1.2f, getNamePaint(b)); | |
} | |
if (actions != null) { | |
float t = actions.getVisibility(now); | |
if (!actions.enabled && t == .0f) { | |
model.clearActions(); | |
} | |
float showed = interpolator.getInterpolation(t); | |
float dark = interpolator_dec.getInterpolation(t); | |
float dist_range = bubbleActionTextDistMax - bubbleActionTextDistMin; | |
action_paint.setAlpha((int) (255 * t)); | |
List<Attractor> acts = actions.getActions(); | |
Bubble b = actions.bubble; | |
canvas.drawARGB((int)(dark*128), 0, 0, 0); | |
white_name_paint.setTextSize(18 * textDensity); | |
boolean suck_bubble = false; | |
for (Attractor a : acts) { | |
if (b.attractor == a) { | |
canvas.drawBitmap(ic_bg_sel, null, a.getBounds(showed * 2.f, b.getPos(), showed), action_paint); | |
suck_bubble = true; | |
} else | |
canvas.drawBitmap(ic_bg, null, a.getBounds(showed * 2.f, b.getPos(), showed), action_paint); | |
canvas.drawBitmap(a.getBitmap(), null, a.getBounds(showed, b.getPos(), showed), null); | |
float dist_raw = FloatMath.sqrt((b.pos.x - a.pos.x) * (b.pos.x - a.pos.x) + (b.pos.y - a.pos.y) * (b.pos.y - a.pos.y)); | |
float dist_min = a.radius + b.radius + bubbleActionTextDistMin; | |
float dist = Math.max(0, dist_raw - dist_min); | |
if (actions.enabled && dist < dist_range) { | |
white_name_paint.setAlpha(255 - (int) (255 * dist / dist_range)); | |
canvas.drawText(a.name, a.getBounds().centerX(), a.getBounds().centerY() - a.radius * 2.2f, white_name_paint); | |
} | |
} | |
white_name_paint.setAlpha(255); | |
canvas.drawBitmap(drawLater.getBitmap(), null, drawLater.getBounds(), (!actions.enabled && suck_bubble)? action_paint : null); | |
} | |
} | |
} | |
private Paint getNamePaint(Bubble b) { | |
black_name_paint.setTextSize(18/* * b.targetScale */ * textDensity); | |
return black_name_paint; | |
} | |
@Override | |
public boolean onTouch(View v, MotionEvent event) { | |
// Log.w(TAG, "onTouch " + event.getAction()); | |
int action = event.getActionMasked(); | |
if (gDetector.onTouchEvent(event)) | |
return true; | |
if (action == MotionEvent.ACTION_UP) { | |
if (thread.suspendFlag) { | |
Log.i(TAG, "Relaunch drawing thread"); | |
thread.setPaused(false); | |
} | |
List<Bubble> bubbles = model.getBubbles(); | |
for (Bubble b : bubbles) { | |
if (b.isGrabbed()) { | |
model.ungrabBubble(b); | |
} | |
} | |
dragging_bubble = false; | |
} else if (action != MotionEvent.ACTION_DOWN && !isDraggingBubble() && !thread.suspendFlag) { | |
Log.i(TAG, "Not dragging thread should be stopped"); | |
thread.setPaused(true); | |
} | |
return true; | |
} | |
public void restartDrawing() { | |
if (thread != null && thread.suspendFlag) { | |
Log.i(TAG, "Relaunch drawing thread"); | |
thread.setPaused(false); | |
} | |
} | |
public void stopThread() { | |
if (thread != null && thread.suspendFlag) { | |
Log.i(TAG, "Stop drawing thread"); | |
thread.setPaused(true); | |
} | |
} | |
class BubbleGestureListener implements OnGestureListener { | |
@Override | |
public boolean onDown(MotionEvent event) { | |
synchronized (model) { | |
List<Bubble> bubbles = model.getBubbles(); | |
for (Bubble b : bubbles) { | |
if (b.intersects(event.getX(), event.getY())) { | |
model.grabBubble(b); | |
b.setPos(event.getX(), event.getY()); | |
dragging_bubble = true; | |
} | |
} | |
} | |
return true; | |
} | |
@Override | |
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { | |
return false; | |
} | |
@Override | |
public void onLongPress(MotionEvent e) { | |
} | |
@Override | |
public boolean onScroll(MotionEvent e1, MotionEvent event, float distanceX, float distanceY) { | |
synchronized (model) { | |
List<Bubble> bubbles = model.getBubbles(); | |
for (Bubble b : bubbles) { | |
if (b.isGrabbed()) { | |
b.drag(event.getX(), event.getY()); | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
@Override | |
public void onShowPress(MotionEvent e) { | |
} | |
@Override | |
public boolean onSingleTapUp(MotionEvent e) { | |
return false; | |
} | |
} | |
} |