1 /* Dali Clock - a melting digital clock for Android.
2  * Copyright (c) 1991-2015 Jamie Zawinski <jwz@jwz.org>
3  *
4  * Permission to use, copy, modify, distribute, and sell this software and its
5  * documentation for any purpose is hereby granted without fee, provided that
6  * the above copyright notice appear in all copies and that both that
7  * copyright notice and this permission notice appear in supporting
8  * documentation.  No representations are made about the suitability of this
9  * software for any purpose.  It is provided "as is" without express or
10  * implied warranty.
11  *
12  * Ported to Android 2015 by Robin Müller-Cajar <robinmc@mailbox.org>
13  */
14 package org.jwz.daliclock;
15 
16 import android.content.Context;
17 import android.content.SharedPreferences;
18 import android.graphics.Canvas;
19 import android.graphics.Color;
20 import android.graphics.Paint;
21 import android.graphics.Rect;
22 import android.os.Handler;
23 import android.preference.PreferenceManager;
24 import android.util.Log;
25 import android.view.SurfaceHolder;
26 import android.view.SurfaceView;
27 import android.widget.LinearLayout;
28 import java.text.DateFormat;
29 import java.text.ParseException;
30 import java.text.SimpleDateFormat;
31 import java.util.Calendar;
32 import java.util.Date;
33 import java.util.Locale;
34 
35 public class DaliClock {
36   private SurfaceView	surfaceView;
37   private SurfaceHolder	canvasHolder;
38   private LinearLayout	clockbg;
39   private Font		font;
40   private Context	context;
41   private boolean	shown_p = false;
42   private float[]	fg_hsv = { 0, 0, 0 };
43   private float[]	bg_hsv = { 0, 0, 0 };
44   private int		ctx_fillStyle;
45   private int		bg_fillStyle;
46   private Runnable	color_timer_fn;
47   private Handler	color_timer_handler;
48   private Runnable	clock_timer_fn;
49   private Handler	clock_timer_handler;
50   private int		date_length;
51   private SharedPreferences newSettings;
52 
53   private int		clock_freq = 10;
54   private int		color_freq = 12;
55   private boolean	color_cycle = false;
56   private int		width;
57   private int		height;
58   private String	time_mode = "";
59   private int		orientation;
60   private boolean	vp_scaling_p;
61   private int		debug_digit = -1;
62   private String	date_mode = "";
63   private boolean	twelve_hour_p;
64   private boolean	show_date_p;
65   private int[][][][]	orig_frames;
66   private int[]		orig_digits;
67   private int[][][][]	current_frames;
68   private int[][][][]	target_frames;
69   private int[]		target_digits;
70   private int[]		canvas_size = new int[2];
71   private int		displayed_digits;
72   private int		last_secs = -1;
73   private int		current_msecs;
74 
75   private static final String LOG = "DaliClock";
76 
DaliClock(Context context)77   public DaliClock (Context context) {
78     this.context = context;
79   }
80 
setup(SurfaceView canvas_element, LinearLayout background_element, SharedPreferences settings)81   public void setup (SurfaceView canvas_element,
82                      LinearLayout background_element,
83                      SharedPreferences settings) {
84 
85     initialize_default_settings (settings);
86 
87     this.surfaceView = canvas_element;
88     this.clockbg = background_element;
89 
90     this.ctx_fillStyle = Color.HSVToColor(this.fg_hsv);
91     this.bg_fillStyle  = Color.HSVToColor(this.bg_hsv);
92     this.clockbg.setBackgroundColor(this.bg_fillStyle);
93 
94     this.canvasHolder = this.surfaceView.getHolder();
95 
96     this.changeSettings(settings);
97   }
98 
99 
100   /**
101    * If a value in our settings is unset, set it to the default.
102    * Some default values are derived from the locale.
103    */
initialize_default_settings(SharedPreferences settings)104   private void initialize_default_settings (SharedPreferences settings) {
105 
106     SharedPreferences.Editor editor = settings.edit();
107 
108     /* Try to determine the current locale's short date format, and
109        set our dateStyle based on that.  We do this by creating a date
110        object with a particular unambiguous date/time in the current
111        time zone, converting that to a string in the current locale,
112        and parsing that string.  There doesn't seem to be any other
113        way to get the answer to the questions, "what order are year,
114        month and day printed?", and "are hours printed mod 12 or 24?"
115    */
116     final String epoch = "2032-12-31 13:00";
117     SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
118     Date epoch_date = null;
119     try {
120       epoch_date = sdf.parse (epoch);
121     } catch (ParseException e) {
122     }
123 
124     DateFormat df = DateFormat.getDateTimeInstance (DateFormat.SHORT,
125                                                     DateFormat.SHORT,
126                                                     Locale.getDefault());
127     String loc = df.format(epoch_date);
128     String def_date_mode = "YYMMDD";
129     boolean def_twelve_p = true;
130 
131     // Locale.US      = 12/31/32 1:00 PM
132     // Locale.GERMANY = 31.12.32 13:00
133 
134     if (loc.indexOf("32") < loc.indexOf("12")) {	// year < month
135       if (loc.indexOf("12") < loc.indexOf("31")) {	//  month < dotm
136         def_date_mode = "YYMMDD";
137       } else {						//  month > dotm
138         def_date_mode = "YYDDMM";
139       }
140     } else {						// year > month
141       if (loc.indexOf("12") < loc.indexOf("31")) {	//  month < dotm
142         def_date_mode = "MMDDYY";
143       } else {						//  month > dotm
144         def_date_mode = "DDMMYY";
145       }
146     }
147 
148     def_twelve_p = (loc.indexOf("13") < 0);
149 
150     Log.d(LOG, "Date \"" + epoch + "\" localizes to " + "\"" + loc + "\", " +
151           def_date_mode + ", " + (def_twelve_p ? "12" : "24"));
152 
153     float[] def_fg = {200, 0.4f, 1.0f };
154     float[] def_bg = {128, 1.0f, 0.4f };
155     def_fg[0] += Math.floor(Math.random()*360);
156     def_bg[0] += Math.floor(Math.random()*360);
157 
158 
159     // Now that we know how the locale formats dates, store an entry in
160     // preferences for each preference key that does not already have a
161     // value.
162 
163     String[][] defaults = {
164       { "time_mode",     "S", "HHMMSS" },
165       { "date_mode",     "S", def_date_mode },
166       { "twelve_hour_p", "B", (def_twelve_p ? "true" : "false") },
167       { "show_date_p",   "B", "false" },
168       { "fps",           "I", "30" },
169       { "cps",           "I", "12" },
170       { "color_cycle",   "B", "true" },
171       { "vp_scaling_p",  "B", "true" },
172       { "debug_digit",   "I", "-1" },
173       { "fg",            "I", Integer.toString(Color.HSVToColor(def_fg)) },
174       { "bg",            "I", Integer.toString(Color.HSVToColor(def_bg)) },
175     };
176 
177     // Actually let's just store these all the time, to always track locale.
178     editor.remove ("date_mode");
179     editor.remove ("twelve_hour_p");
180     editor.apply();
181 
182     for (String[] pair: defaults) {
183       String key  = pair[0];
184       String type = pair[1];
185       String val  = pair[2];
186       if (settings.contains (key)) {
187         Log.d(LOG, "Already set: " + key + " = " +
188               (type.equals("S") ? settings.getString(key, "") :
189                type.equals("I") ? Integer.toString(settings.getInt(key, 0)) :
190                settings.getBoolean(key, false) ? "true" : "false"));
191       } else {
192         Log.d(LOG, "Default: " + key + " = " + val);
193         if (type.equals("S")) {
194           editor.putString (key, val);
195         } else if (type.equals("I")) {
196           editor.putInt (key, Integer.parseInt(val));
197         } else {
198           editor.putBoolean (key, (val.equals("true")));
199         }
200       }
201     }
202 
203     editor.apply();
204 
205     // Load the colors from preferences, so that when the app stops and
206     // starts up again, it continues from the colors it had last time.
207     //
208     Color.colorToHSV (settings.getInt ("fg", Color.WHITE), fg_hsv);
209     Color.colorToHSV (settings.getInt ("bg", Color.BLACK), bg_hsv);
210   }
211 
212 
213   /**
214    * For setup tasks that have to happen each time the window becomes visible.
215    */
show()216   public void show() {
217     if (this.shown_p) return;
218     this.shown_p = true;
219 
220     // Create the color timer.
221     color_timer_fn = new Runnable() {
222         @Override
223         public void run() {
224           if(shown_p) color_timer();
225         }
226       };
227     color_timer_handler = new Handler();
228 
229 
230     // Create the clock timer.
231     clock_timer_fn = new Runnable() {
232         @Override
233         public void run() {
234           if(shown_p) clock_timer();
235         }
236       };
237     clock_timer_handler = new Handler();
238 
239     this.canvasHolder.addCallback(new SurfaceHolder.Callback() {
240         @Override
241         public void surfaceCreated(SurfaceHolder holder) {
242           color_timer_handler.post(color_timer_fn);
243           clock_timer_handler.post(clock_timer_fn);
244         }
245 
246         @Override
247         public void surfaceChanged(SurfaceHolder holder,
248                                    int format,
249                                    int width, int height) {
250           SharedPreferences settings =
251             PreferenceManager.getDefaultSharedPreferences(context);
252           SharedPreferences.Editor editor = settings.edit();
253 
254           Rect rect = holder.getSurfaceFrame();
255           editor.putInt ("width",  (int) rect.width());
256           editor.putInt ("height", (int) rect.height());
257           editor.apply();
258 
259           changeSettings(settings);
260         }
261 
262         @Override
263         public void surfaceDestroyed(SurfaceHolder holder) {
264           hide();
265           canvasHolder.removeCallback(this);
266         }
267       });
268   }
269 
270 
271   /**
272    * Tasks that have to happen each time the window is hidden.
273    */
hide()274   public void hide() {
275     if (!this.shown_p) return;
276     this.shown_p = false;
277 
278     if (this.clock_timer_fn != null) {
279       this.clock_timer_handler.removeCallbacks(this.clock_timer_fn);
280       this.clock_timer_handler = null;
281       this.clock_timer_fn = null;
282     }
283     if (this.color_timer_fn != null) {
284       this.color_timer_handler.removeCallbacks(this.color_timer_fn);
285       this.color_timer_handler = null;
286       this.color_timer_fn = null;
287     }
288   }
289 
290 
291   /**
292    * About to exit.
293    */
cleanup()294   public void cleanup() {
295     this.hide();
296   }
297 
298 
clock_timer()299   private void clock_timer() {
300 
301     this.tick_sequence();
302     this.draw_clock();
303 
304 
305     if (this.show_date_p) {
306       this.date_length -= this.clock_freq;
307       if (this.date_length <= 0) {
308         this.show_date_p = false;
309         this.date_length = 0;
310       }
311     }
312 
313     // Re-trigger our timer.
314     this.clock_timer_handler.postDelayed(this.clock_timer_fn, this.clock_freq);
315   }
316 
317 
color_timer()318   private void color_timer() {
319     // cps == 0 means don't cycle colors. but the timer still goes off
320     // at least once a second in case cps has changed.
321     int when = this.color_freq;
322     if (when > 0)
323       this.tick_colors();
324     else
325       when = 2000;
326 
327     this.color_timer_handler.postDelayed(this.color_timer_fn, when);
328   }
329 
330 
tick_colors()331   private void tick_colors() {
332     this.ctx_fillStyle = Color.HSVToColor(this.fg_hsv);
333     this.bg_fillStyle  = Color.HSVToColor(this.bg_hsv);
334     this.clockbg.setBackgroundColor(this.bg_fillStyle);
335 
336     this.fg_hsv[0]++;
337     if (this.fg_hsv[0] >= 360) { this.fg_hsv[0] -= 360; }
338 
339     this.bg_hsv[0] += 0.91;
340     if (this.bg_hsv[0] >= 360) { this.bg_hsv[0] -= 360; }
341 
342     // Store the colors preferences, so that when the app stops and
343     // starts up again, it continues from the colors it had last time.
344     //
345     SharedPreferences settings =
346       PreferenceManager.getDefaultSharedPreferences(context);
347     SharedPreferences.Editor editor = settings.edit();
348     editor.putInt ("fg", this.ctx_fillStyle);
349     editor.putInt ("bg", this.bg_fillStyle);
350     editor.apply();
351   }
352 
353 
354   /**
355    *Change display settings at next second-tick.
356    */
changeSettings(SharedPreferences settings)357   public void changeSettings (SharedPreferences settings) {
358 
359     // We can process these immediately
360     if (settings != null) {
361       int fps = settings.getInt ("fps", 30);
362       this.clock_freq = (int) Math.round (1000.0 / fps);
363 
364       this.color_cycle = settings.getBoolean("color_cycle", false);
365       if (this.color_cycle) {
366         int cps = settings.getInt ("cps", 12);
367         this.color_freq = (int) Math.round(1000.0 / cps);
368       } else {
369         this.color_freq = 0;
370 
371         this.bg_fillStyle = Color.argb(255,0,0,0);
372         this.ctx_fillStyle = Color.argb(255,255,255,255);
373       }
374 
375     }
376 
377     if (this.clock_freq <= 0) this.clock_freq = 1;
378     if (this.color_freq <  0) this.color_freq = 1;
379 
380     // If the clock is hidden, we can process everything immediately.
381     if (!this.shown_p)
382       this.settings_changed (settings);
383     else
384       this.newSettings = settings;
385   }
386 
387 
388   /**
389    * Called at the start of each sequence if the swChangeSettings == true.
390    * All settings changes are delayed until the second-tick.
391    * The settings object contains:
392    *
393    *    width		size of clock display area
394    *    height		size of clock display area
395    *    time_mode	'HHMMSS' | 'HHMM' | 'SS'
396    *    date_mode	'MMDDYY' | 'DDMMYY' | 'YYMMDD'
397    *    twelve_hour_p	boolean, whether to display 12 or 24-hour time
398    *    show_date_p	boolean, whether to display date instead of time
399    *    fps		integer (frames per second)
400    *    cps		integer (color changes per second)
401    *    vp_scaling_p	whether surfaceView scaling works for antialiasing
402    *    debug_digit	-1 or 0-10
403    */
settings_changed(SharedPreferences settings)404   private void settings_changed(SharedPreferences settings) {
405 
406     // Changes to some settings require tearing down and rebuilding
407     // the clock.  Changes to others can be animated normally.
408     boolean reset_p =
409       (settings == null ||
410        this.width  != settings.getInt ("width",  0) ||
411        this.height != settings.getInt ("height", 0) ||
412        !this.time_mode.equals(settings.getString("time_mode", "")) ||
413        !this.date_mode.equals(settings.getString("date_mode", "")) ||
414        this.vp_scaling_p != settings.getBoolean("vp_scaling_p", false));
415 
416     if (settings != null) {
417       this.width	 = settings.getInt     ("width", 0);
418       this.height	 = settings.getInt     ("height", 0);
419       this.date_mode	 = settings.getString  ("date_mode", "");
420       this.time_mode	 = settings.getString  ("time_mode", "");
421       this.vp_scaling_p	 = settings.getBoolean ("vp_scaling_p", false);
422       this.show_date_p	 = settings.getBoolean ("show_date_p", false);
423       this.debug_digit	 = settings.getInt     ("debug_digit", -1);
424       this.twelve_hour_p = settings.getBoolean ("twelve_hour_p", false);
425     }
426 
427     // If date mode has been activated, deactivate it in 2 seconds.
428     if (this.show_date_p) this.date_length = 2000;
429 
430     if (reset_p) this.clock_reset();
431   }
432 
433 
434   /** Reset the animation when the settings (number of digits, orientation)
435    *  has changed.  We have to start over since the resolution is different.
436    */
clock_reset()437   private void clock_reset() {
438 
439     this.pick_font_size();
440 
441     this.orig_frames    = new int[8][][][];  // what was there
442     this.orig_digits    = new int[8];        // what was there
443     this.current_frames = new int[8][][][];  // current intermediate animation
444     this.target_frames  = new int[8][][][];  // where we are going
445     this.target_digits  = new int[8];        // where we are going
446 
447     for (int i = 0; i < this.current_frames.length; i++) {
448       boolean colonic_p = (i == 2 || i == 5);
449       int[][][] empty = (colonic_p
450                          ? this.font.getEmpty_colon()
451                          : this.font.getEmpty_frame());
452       this.orig_frames[i]    = empty;
453       this.orig_digits[i]    = -1;
454       this.target_frames[i]  = empty;
455       this.current_frames[i] = font.copy_frame(empty);
456     }
457 
458     int nn, cc;
459 
460     switch (this.time_mode) {
461     case "SS":   nn = 2; cc = 0; break;
462     case "HHMM": nn = 4; cc = 1; break;
463     default:     nn = 6; cc = 2; break;
464     }
465 
466     this.displayed_digits = nn + cc;
467   }
468 
469 
470   /** Find the largest font that fits in the surfaceView given the
471       current settings (number of digits and orientation).
472    */
pick_font_size()473   private void pick_font_size() {
474 
475     int nn, cc;
476 
477     switch (this.time_mode) {
478     case "SS":   nn = 2; cc = 0; break;
479     case "HHMM": nn = 4; cc = 1; break;
480     default:     nn = 6; cc = 2; break;
481     }
482 
483     int width  = this.width;
484     int height = this.height;
485 
486     if (this.vp_scaling_p) {   // double it, for anti-aliasing
487       width  *= 2;
488       height *= 2;
489     }
490 
491     if (this.orientation == LinearLayout.VERTICAL) {
492       int swap = width; width = height; height = swap;
493     }
494 
495     for (int i = Font.numFonts-1; i >= 0; i--) {
496       Font font = new Font(i, this.context);
497       int w = (font.getChar_width() * nn) + (font.getColon_width() * cc);
498       int h = font.getChar_height();
499 
500       if ((w <= width && h <= height) || i == 0) {
501         this.font          = font;
502         this.canvas_size[0] = w;
503         this.canvas_size[1] = h;
504         return;
505       }
506     }
507   }
508 
509 
510   // Gets the current wall clock and formats the display accordingly.
511   //
fill_target_digits(Calendar date)512   private void fill_target_digits(Calendar date) {
513 
514     int h = date.get(Calendar.HOUR_OF_DAY);
515     int m = date.get(Calendar.MINUTE);
516     int s = date.get(Calendar.SECOND);
517     int D = date.get(Calendar.DAY_OF_MONTH);
518     int M = date.get(Calendar.MONTH) + 1;
519     int Y = date.get(Calendar.YEAR) % 100;
520 
521     if (this.twelve_hour_p) {
522       if (h > 12) { h -= 12; }
523       else if (h == 0) { h = 12; }
524     }
525 
526     for (int i = 0; i < this.target_digits.length; i++) {
527       this.target_digits[i] = -1;
528     }
529 
530     if (this.debug_digit != -1) {
531       if (this.debug_digit < 0 || this.debug_digit > 11)
532         this.debug_digit = -1;
533       this.target_digits[0] = this.target_digits[1] =
534         this.target_digits[3] = this.target_digits[4] =
535         this.target_digits[6] = this.target_digits[7] = this.debug_digit;
536       this.debug_digit = -1;
537 
538     } else if (!this.show_date_p) {
539 
540       switch (this.time_mode) {
541       case "SS":
542         this.target_digits[0] = (s / 10);
543         this.target_digits[1] = (s % 10);
544         break;
545       case "HHMM":
546         this.target_digits[0] = (h / 10);
547         this.target_digits[1] = (h % 10);
548         this.target_digits[2] =	10;  // colon
549         this.target_digits[3] = (m / 10);
550         this.target_digits[4] =  (m % 10);
551         if (this.twelve_hour_p && this.target_digits[0] == 0) {
552           this.target_digits[0] = -1;
553         }
554         break;
555       default:
556         this.target_digits[0] = (h / 10);
557         this.target_digits[1] = (h % 10);
558         this.target_digits[2] =	10;  // colon
559         this.target_digits[3] = (m / 10);
560         this.target_digits[4] = (m % 10);
561         this.target_digits[5] =	10;  // colon
562         this.target_digits[6] = (s / 10);
563         this.target_digits[7] = (s % 10);
564         if (this.twelve_hour_p && this.target_digits[0] == 0) {
565           this.target_digits[0] = -1;
566         }
567         break;
568       }
569     } else {  // date mode
570 
571       switch (this.date_mode) {
572       case "MMDDYY":
573         switch (this.time_mode) {
574         case "SS":
575           this.target_digits[0] = (D / 10);
576           this.target_digits[1] = (D % 10);
577           break;
578         case "HHMM":
579           this.target_digits[0] = (M / 10);
580           this.target_digits[1] = (M % 10);
581           this.target_digits[2] =	11;  // dash
582           this.target_digits[3] = (D / 10);
583           this.target_digits[4] = (D % 10);
584           break;
585         default:  // HHMMSS
586           this.target_digits[0] = (M / 10);
587           this.target_digits[1] = (M % 10);
588           this.target_digits[2] =	11;  // dash
589           this.target_digits[3] = (D / 10);
590           this.target_digits[4] = (D % 10);
591           this.target_digits[5] =	11;  // dash
592           this.target_digits[6] = (Y / 10);
593           this.target_digits[7] = (Y % 10);
594           break;
595         }
596         break;
597       case "DDMMYY":
598         switch (this.time_mode) {
599         case "SS":
600           this.target_digits[0] = (D / 10);
601           this.target_digits[1] = (D % 10);
602           break;
603         case "HHMM":
604           this.target_digits[0] = (D / 10);
605           this.target_digits[1] = (D % 10);
606           this.target_digits[2] =	11;  // dash
607           this.target_digits[3] = (M / 10);
608           this.target_digits[4] = (M % 10);
609           break;
610         default:  // HHMMSS
611           this.target_digits[0] = (D / 10);
612           this.target_digits[1] = (D % 10);
613           this.target_digits[2] =	11;  // dash
614           this.target_digits[3] = (M / 10);
615           this.target_digits[4] = (M % 10);
616           this.target_digits[5] =	11;  // dash
617           this.target_digits[6] = (Y / 10);
618           this.target_digits[7] = (Y % 10);
619           break;
620         }
621         break;
622       default:
623         switch (this.time_mode) {
624         case "SS":
625           this.target_digits[0] = (D / 10);
626           this.target_digits[1] = (D % 10);
627           break;
628         case "HHMM":
629           this.target_digits[0] = (M / 10);
630           this.target_digits[1] = (M % 10);
631           this.target_digits[2] =	11;  // dash
632           this.target_digits[3] = (D / 10);
633           this.target_digits[4] = (D % 10);
634           break;
635         default:  // HHMMSS
636           this.target_digits[0] = (Y / 10);
637           this.target_digits[1] = (Y % 10);
638           this.target_digits[2] =	11;  // dash
639           this.target_digits[3] = (M / 10);
640           this.target_digits[4] = (M % 10);
641           this.target_digits[5] =	11;  // dash
642           this.target_digits[6] = (D / 10);
643           this.target_digits[7] = (D % 10);
644           break;
645         }
646         break;
647       }
648     }
649   }
650 
draw_frame(Canvas canvas, int[][][] frame, int x, int y, Paint paint)651   private void draw_frame (Canvas canvas,
652                            int[][][] frame,
653                            int x, int y,
654                            Paint paint) {
655     int ch = this.font.getChar_height();
656     for (int py = 0; py < ch; py++)
657       {
658         int[][] line  = frame[py];
659 
660         for(int px = 0; px < line.length; px++) {
661           canvas.drawRect (x + line[px][0],
662                            y + py,
663                            x + line[px][1],
664                            y + py + 1,
665                            paint);
666         }
667       }
668   }
669 
670   /** The second has ticked: we need a new set of digits to march toward.
671    */
start_sequence(Calendar date)672   public void start_sequence (Calendar date) {
673 
674     if (this.newSettings != null) {
675       this.settings_changed(this.newSettings);
676       this.newSettings = null;
677     }
678 
679     // Move the (old) current_frames into the (new) orig_frames,
680     // since that's what's on the screen now.
681     //
682     for (int i = 0; i < this.current_frames.length; i++) {
683       this.orig_frames[i] = this.current_frames[i];
684       this.orig_digits[i] = this.target_digits[i];
685     }
686 
687     // generate new target_digits
688     this.fill_target_digits (date);
689 
690     // Fill the (new) target_frames from the (new) target_digits.
691     for (int i = 0; i < this.target_frames.length; i++) {
692       boolean colonic_p = (i == 2 || i == 5);
693       int[][][] empty = (colonic_p
694                          ? this.font.getEmpty_colon()
695                          : this.font.getEmpty_frame());
696       int[][][] frame = (this.target_digits[i] == -1
697                          ? empty
698                          : this.font.getSegment(this.target_digits[i]));
699       this.target_frames[i] = frame;
700     }
701 
702     this.draw_clock();
703   }
704 
705 
one_step(int[][][] orig, int[][][] curr, int[][][] target, int msecs)706   private void one_step (int[][][] orig,
707                          int[][][] curr,
708                          int[][][] target,
709                          int msecs) {
710 
711     int ch = this.font.getChar_height();
712     double frac = msecs / 1000.0;
713 
714     for (int i = 0; i < ch; i++) {
715       int[][] oline = orig[i];
716       int[][] tline = target[i];
717       int osegs = oline.length;
718       int tsegs = tline.length;
719 
720       int segs = (osegs > tsegs ? osegs : tsegs);
721 
722       // orig and target might have different numbers of segments.
723       // current ends up with the maximal number.
724       curr[i] = new int[segs][2];
725       int[][] cline = curr[i];
726 
727       for (int j = 0; j < segs; j++) {
728         int[] oseg = oline[0];
729         if(j>0 && osegs>1)
730           oseg = oline[j];
731 
732         int[] cseg = cline[j];
733 
734         int[] tseg = tline[0];
735         if(j>0 && tsegs>1)
736           tseg = tline[j];
737 
738         cseg[0] = (int) (oseg[0] + Math.round (frac * (tseg[0] - oseg[0])));
739         cseg[1] = (int) (oseg[1] + Math.round (frac * (tseg[1] - oseg[1])));
740       }
741     }
742   }
743 
744   /** Compute the current animation state of each digit into target_frames
745    *  according to our current position within the current wall-clock second.
746    */
tick_sequence()747   private void tick_sequence() {
748 
749     Calendar now   = Calendar.getInstance();
750     int secs  = now.get(Calendar.SECOND);
751     int msecs = now.get(Calendar.MILLISECOND); // msec position within this sec
752 
753     if (this.last_secs == -1) {
754       this.last_secs = secs;   // fading in!
755     } else if (secs != this.last_secs) {
756       // End of the animation sequence; fill target_frames with the
757       // digits of the current time.
758       this.start_sequence(now);
759       this.last_secs = secs;
760     }
761 
762     // Linger for about 1/10th second at the end of each cycle.
763     msecs *= 1.2;
764     if (msecs > 1000) msecs = 1000;
765 
766     // Construct current_frames by interpolating between
767     // orig_frames and target_frames.
768     //
769     for (int i = 0; i < this.orig_frames.length; i++) {
770       this.one_step (this.orig_frames[i],
771                      this.current_frames[i],
772                      this.target_frames[i],
773                      msecs);
774     }
775 
776     this.current_msecs = msecs;
777   }
778 
779 
780   /** left_offset is so that the clock can be centered in the window
781    *  when the leftmost digit is hidden (in 12-hour mode when the hour
782    *  is 1-9).  When the hour rolls over from 9 to 10, or from 12 to 1,
783    * we animate the transition to keep the digits centered.
784    */
compute_left_offset()785   private int compute_left_offset() {
786     int left_offset;
787     if (this.target_digits[0] == -1 &&	          // Fading in to no digit
788         this.orig_digits[1] == -1)
789       left_offset = this.font.getChar_width() / 2;
790     else if (this.target_digits[0] != -1 &&      // Fading in to a digit
791              this.orig_digits[1] == -1)
792       left_offset = 0;
793     else if (this.orig_digits[0] != -1 &&	 // Fading out from digit
794              this.target_digits[1] == -1)
795       left_offset = 0;
796     else if (this.orig_digits[0] == -1 &&	 // Fading out from no digit
797              this.target_digits[1] == -1)
798       left_offset = this.font.getChar_width() / 2;
799     else if (this.orig_digits[0] == -1 &&	 // Anim no digit to digit.
800              this.target_digits[0] != -1)
801       left_offset = (this.font.getChar_width() *
802                      (1000 - this.current_msecs) / 2000);
803     else if (this.orig_digits[0] != -1 &&	 // Anim digit to no digit.
804              this.target_digits[0] == -1)
805       left_offset = this.font.getChar_width() * this.current_msecs / 2000;
806     else if (this.target_digits[0] == -1)	 // No anim, no digit.
807       left_offset = this.font.getChar_width() / 2;
808     else					 // No anim, digit.
809       left_offset = 0;
810 
811     return left_offset;
812   }
813 
814 
815   /** Render the current animation state of each digit into the canvas.
816    */
draw_clock()817   private void draw_clock() {
818 
819     Canvas canvas = canvasHolder.lockCanvas();
820     if (canvas == null) return;
821 
822     float left = this.compute_left_offset();
823     float ww = this.width;
824     float wh = this.height;
825     float cw = this.canvas_size[0];
826     float ch = this.canvas_size[1];
827 
828     float xscale = (float) ww / (float) cw;
829     float yscale = (float) wh / (float) ch;
830     float scale  = (xscale > yscale ? yscale : xscale);
831 
832     if (! this.vp_scaling_p) scale = 1;
833 
834     // Don't ever scale up, only scale down.
835     if (scale < 0.98) {
836       canvas.scale (scale, scale);
837       ww /= scale;
838       wh /= scale;
839     }
840 
841     int x = (int) (((ww - cw) / 2) - left);
842     int y = (int)  ((wh - ch) / 2);
843 
844     canvas.drawColor(this.bg_fillStyle);
845     Paint paint = new Paint();
846     paint.setColor(this.ctx_fillStyle);
847     for (int i = 0; i < this.displayed_digits; i++) {
848       this.draw_frame (canvas, this.current_frames[i], x, y, paint);
849       boolean colonic_p = (i == 2 || i == 5);
850       x += (colonic_p
851             ? this.font.getColon_width()
852             : this.font.getChar_width());
853     }
854 
855     canvasHolder.unlockCanvasAndPost(canvas);
856   }
857 
858 }
859