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