blob: 8b465dde29edd219ae76bcd1603f8f578b8b4a9b [file] [log] [blame]
Adrien Béraud04d822c2015-04-02 17:44:36 -04001/*
2 * Copyright (C) 2004-2014 Savoir-Faire Linux Inc.
3 *
4 * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
5 * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 *
21 * Additional permission under GNU GPL version 3 section 7:
22 *
23 * If you modify this program, or any covered work, by linking or
24 * combining it with the OpenSSL project's OpenSSL library (or a
25 * modified version of that library), containing parts covered by the
26 * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
27 * grants you additional permission to convey the resulting work.
28 * Corresponding Source for a non-source form of such a combination
29 * shall include the source code for the parts of OpenSSL used as well
30 * as that of the covered work.
31 */
32
33package cx.ring.model;
34
35import android.content.Context;
36import android.content.res.Resources;
37import android.graphics.*;
38import android.graphics.Paint.Align;
39import android.opengl.GLSurfaceView;
40import android.util.AttributeSet;
41import android.util.FloatMath;
42import android.util.Log;
43import android.view.GestureDetector;
44import android.view.GestureDetector.OnGestureListener;
45import android.view.MotionEvent;
46import android.view.SurfaceHolder;
47import android.view.View;
48import android.view.View.OnTouchListener;
49import android.view.animation.DecelerateInterpolator;
50import android.view.animation.Interpolator;
51import android.view.animation.OvershootInterpolator;
52import cx.ring.R;
53
54import java.util.List;
55
56public class BubblesView extends GLSurfaceView implements SurfaceHolder.Callback, OnTouchListener {
57 private static final String TAG = BubblesView.class.getSimpleName();
58
59 private BubblesThread thread = null;
60 private BubbleModel model;
61
62 private Paint black_name_paint = new Paint(Paint.ANTI_ALIAS_FLAG);
63 private Paint white_name_paint = new Paint(Paint.ANTI_ALIAS_FLAG);
64 private Paint canvas_paint = new Paint();
65 private Paint circle_paint = new Paint(Paint.ANTI_ALIAS_FLAG);
66 private Paint action_paint = new Paint();
67
68 static private final Interpolator interpolator = new OvershootInterpolator(2.f);
69 static private final Interpolator interpolator_dec = new DecelerateInterpolator();
70
71 private final Bitmap ic_bg;
72 private final Bitmap ic_bg_sel;
73
74 private GestureDetector gDetector;
75
76 //private float density;
77 private float textDensity;
78 private float bubbleActionTextDistMin;
79 private float bubbleActionTextDistMax;
80
81 private boolean dragging_bubble = false;
82
83 public BubblesView(Context context, AttributeSet attrs) {
84 super(context, attrs);
85
86 final Resources r = getResources();
87 //density = r.getDisplayMetrics().density;
88 textDensity = r.getDisplayMetrics().scaledDensity;
89 bubbleActionTextDistMin = r.getDimension(R.dimen.bubble_action_textdistmin);
90 bubbleActionTextDistMax = r.getDimension(R.dimen.bubble_action_textdistmax);
91
92 ic_bg = BitmapFactory.decodeResource(r, R.drawable.ic_bg);
93 ic_bg_sel = BitmapFactory.decodeResource(r, R.drawable.ic_bg_sel);
94
95 if (isInEditMode()) return;
96
97 SurfaceHolder holder = getHolder();
98 holder.addCallback(this);
99
100 this.setZOrderOnTop(true); // necessary
101 holder.setFormat(PixelFormat.TRANSLUCENT);
102 // create thread only; it's started in surfaceCreated()
103 createThread();
104
105 setOnTouchListener(this);
106 setFocusable(true);
107
108 black_name_paint.setTextSize(18 * textDensity);
109 black_name_paint.setColor(0xFF303030);
110 black_name_paint.setTextAlign(Align.CENTER);
111
112 white_name_paint.setTextSize(18 * textDensity);
113 white_name_paint.setColor(0xFFEEEEEE);
114 white_name_paint.setTextAlign(Align.CENTER);
115
116 circle_paint.setStyle(Paint.Style.STROKE);
117 circle_paint.setColor(r.getColor(R.color.darker_gray));
118 circle_paint.setXfermode(null);
119
120 gDetector = new GestureDetector(getContext(), new BubbleGestureListener());
121 gDetector.setIsLongpressEnabled(false);
122 }
123
124 private void createThread() {
125 if (thread != null)
126 return;
127 thread = new BubblesThread(getHolder(), getContext());
128 if (model != null)
129 thread.setModel(model);
130 }
131
132 public void setModel(BubbleModel model) {
133 this.model = model;
134 thread.setModel(model);
135 }
136
137 /*
138 * @Override public void onWindowFocusChanged(boolean hasWindowFocus) { if (!hasWindowFocus) { thread.pause(); } }
139 */
140
141 @Override
142 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
143 Log.w(TAG, "surfaceChanged " + width + "-" + height);
144 /*if (height < model.getHeight()) // probably showing the keyboard, don't move!
145 return;
146
147 thread.setSurfaceSize(width, height);*/
148 }
149
150 /*
151 * Callback invoked when the Surface has been created and is ready to be used.
152 */
153 @Override
154 public void surfaceCreated(SurfaceHolder holder) {
155 // start the thread here so that we don't busy-wait in run()
156 // waiting for the surface to be created
157 createThread();
158
159 Log.w(TAG, "surfaceCreated");
160 thread.setRunning(true);
161 thread.start();
162 }
163
164 /*
165 * Callback invoked when the Surface has been destroyed and must no longer be touched. WARNING: after this method returns, the Surface/Canvas must
166 * never be touched again!
167 */
168 @Override
169 public void surfaceDestroyed(SurfaceHolder holder) {
170 // we have to tell thread to shut down & wait for it to finish, or else
171 // it might touch the Surface after we return and explode
172 Log.w(TAG, "surfaceDestroyed");
173 boolean retry = true;
174 thread.setRunning(false);
175 thread.setPaused(false);
176 while (retry) {
177 try {
178 Log.w(TAG, "joining...");
179 thread.join();
180 retry = false;
181 } catch (InterruptedException ignored) {
182 }
183 }
184 Log.w(TAG, "done");
185 thread = null;
186 }
187
188 public boolean isDraggingBubble() {
189 return dragging_bubble;
190 }
191
192 class BubblesThread extends Thread {
193 private boolean running = false;
194 public boolean suspendFlag = false;
195 private SurfaceHolder surfaceHolder;
196
197 BubbleModel model = null;
198
199 public BubblesThread(SurfaceHolder holder, Context context) {
200 surfaceHolder = holder;
201 }
202
203 public void setModel(BubbleModel model) {
204 this.model = model;
205 }
206
207 @Override
208 public void run() {
209 while (running) {
210 Canvas c = null;
211 try {
212
213 if (suspendFlag) {
214 synchronized (this) {
215 while (suspendFlag) {
216 try {
217 wait();
218 } catch (InterruptedException e) {
219 // TODO Auto-generated catch block
220 e.printStackTrace();
221 }
222 }
223 }
224 } else {
225 c = surfaceHolder.lockCanvas(null);
226
227 // for the case the surface is destroyed while already in the loop
228 if (c == null || model == null)
229 continue;
230
231 synchronized (model) {
232 model.update();
233 }
234 synchronized (surfaceHolder) {
235 // Log.w(TAG, "Thread doDraw");
236 synchronized (model) {
237 doDraw(c);
238 }
239 }
240 }
241
242 } finally {
243 if (c != null)
244 surfaceHolder.unlockCanvasAndPost(c);
245 }
246 }
247 }
248
249 public void setPaused(boolean wantToPause) {
250 synchronized (this) {
251 suspendFlag = wantToPause;
252 notify();
253 }
254 }
255
256 public void setRunning(boolean b) {
257 running = b;
258 }
259
260 /**
261 * got multiple IndexOutOfBoundsException, when switching calls. //FIXME
262 *
263 * @param canvas
264 */
265 private void doDraw(Canvas canvas) {
266 List<Bubble> bubbles = model.getBubbles();
267 List<Attractor> attractors = model.getAttractors();
268 BubbleModel.ActionGroup actions = model.getActions();
269
270 long now = System.nanoTime();
271
272 canvas_paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
273 canvas.drawPaint(canvas_paint);
274 canvas_paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
275
276 if (model.curState == BubbleModel.State.Incall || model.curState == BubbleModel.State.Outgoing) {
277 PointF center = model.getCircleCenter();
278 canvas.drawCircle(center.x, center.y, model.getCircleSize(), circle_paint);
279 }
280
281 for (Attractor a : attractors) {
282 canvas.drawBitmap(a.getBitmap(), null, a.getBounds(), null);
283 }
284
285 Bubble drawLater = (actions == null) ? null : actions.bubble;
286 for (Bubble b : bubbles) {
287 if (b == drawLater) continue;
288 canvas.drawBitmap(b.getBitmap(), null, b.getBounds(), null);
289 canvas.drawText(b.getName(), b.getPosX(), b.getPosY() - b.getRetractedRadius() * 1.2f, getNamePaint(b));
290 }
291
292 if (actions != null) {
293 float t = actions.getVisibility(now);
294 if (!actions.enabled && t == .0f) {
295 model.clearActions();
296 }
297 float showed = interpolator.getInterpolation(t);
298 float dark = interpolator_dec.getInterpolation(t);
299 float dist_range = bubbleActionTextDistMax - bubbleActionTextDistMin;
300 action_paint.setAlpha((int) (255 * t));
301
302 List<Attractor> acts = actions.getActions();
303 Bubble b = actions.bubble;
304
305 canvas.drawARGB((int)(dark*128), 0, 0, 0);
306
307 white_name_paint.setTextSize(18 * textDensity);
308 boolean suck_bubble = false;
309 for (Attractor a : acts) {
310 if (b.attractor == a) {
311 canvas.drawBitmap(ic_bg_sel, null, a.getBounds(showed * 2.f, b.getPos(), showed), action_paint);
312 suck_bubble = true;
313 } else
314 canvas.drawBitmap(ic_bg, null, a.getBounds(showed * 2.f, b.getPos(), showed), action_paint);
315 canvas.drawBitmap(a.getBitmap(), null, a.getBounds(showed, b.getPos(), showed), null);
316 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));
317 float dist_min = a.radius + b.radius + bubbleActionTextDistMin;
318 float dist = Math.max(0, dist_raw - dist_min);
319 if (actions.enabled && dist < dist_range) {
320 white_name_paint.setAlpha(255 - (int) (255 * dist / dist_range));
321 canvas.drawText(a.name, a.getBounds().centerX(), a.getBounds().centerY() - a.radius * 2.2f, white_name_paint);
322 }
323 }
324 white_name_paint.setAlpha(255);
325
326 canvas.drawBitmap(drawLater.getBitmap(), null, drawLater.getBounds(), (!actions.enabled && suck_bubble)? action_paint : null);
327 }
328 }
329 }
330
331 private Paint getNamePaint(Bubble b) {
332 black_name_paint.setTextSize(18/* * b.targetScale */ * textDensity);
333 return black_name_paint;
334 }
335
336 @Override
337 public boolean onTouch(View v, MotionEvent event) {
338 // Log.w(TAG, "onTouch " + event.getAction());
339
340 int action = event.getActionMasked();
341
342 if (gDetector.onTouchEvent(event))
343 return true;
344
345 if (action == MotionEvent.ACTION_UP) {
346 if (thread.suspendFlag) {
347 Log.i(TAG, "Relaunch drawing thread");
348 thread.setPaused(false);
349 }
350 List<Bubble> bubbles = model.getBubbles();
351 for (Bubble b : bubbles) {
352 if (b.isGrabbed()) {
353 model.ungrabBubble(b);
354 }
355 }
356 dragging_bubble = false;
357 } else if (action != MotionEvent.ACTION_DOWN && !isDraggingBubble() && !thread.suspendFlag) {
358 Log.i(TAG, "Not dragging thread should be stopped");
359 thread.setPaused(true);
360 }
361 return true;
362 }
363
364 public void restartDrawing() {
365 if (thread != null && thread.suspendFlag) {
366 Log.i(TAG, "Relaunch drawing thread");
367 thread.setPaused(false);
368 }
369 }
370
371 public void stopThread() {
372 if (thread != null && thread.suspendFlag) {
373 Log.i(TAG, "Stop drawing thread");
374 thread.setPaused(true);
375 }
376 }
377
378 class BubbleGestureListener implements OnGestureListener {
379 @Override
380 public boolean onDown(MotionEvent event) {
381 synchronized (model) {
382 List<Bubble> bubbles = model.getBubbles();
383 for (Bubble b : bubbles) {
384 if (b.intersects(event.getX(), event.getY())) {
385 model.grabBubble(b);
386 b.setPos(event.getX(), event.getY());
387 dragging_bubble = true;
388 }
389 }
390 }
391 return true;
392 }
393
394 @Override
395 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
396 return false;
397 }
398
399 @Override
400 public void onLongPress(MotionEvent e) {
401 }
402
403 @Override
404 public boolean onScroll(MotionEvent e1, MotionEvent event, float distanceX, float distanceY) {
405 synchronized (model) {
406 List<Bubble> bubbles = model.getBubbles();
407 for (Bubble b : bubbles) {
408 if (b.isGrabbed()) {
409 b.drag(event.getX(), event.getY());
410 return true;
411 }
412 }
413 }
414 return false;
415 }
416
417 @Override
418 public void onShowPress(MotionEvent e) {
419 }
420
421 @Override
422 public boolean onSingleTapUp(MotionEvent e) {
423 return false;
424 }
425 }
426}