diff --git a/source/MenuView.mc b/source/MenuView.mc index a11bc09..af09571 100644 --- a/source/MenuView.mc +++ b/source/MenuView.mc @@ -2,26 +2,38 @@ import Toybox.Application; import Toybox.Graphics; import Toybox.Lang; import Toybox.Math; +import Toybox.Timer; import Toybox.WatchUi; // Rotating 10-icon ring modelled after the watch Controls menu. // Selection point is fixed at the 3 o'clock position (START/STOP -// button height). UP/DOWN input rotates the ring. +// button height). UP/DOWN input rotates the ring with a smooth +// animation. class MenuView extends WatchUi.View { const STORAGE_KEY = "menu_idx"; + const ANIM_DURATION_MS = 200; + const ANIM_TICK_MS = 16; // ~60fps private var _items as Array; private var _selectedIndex as Number = 0; private var _bitmaps as Dictionary = {}; + // Animation state. + private var _animTimer as Timer.Timer; + private var _animFrom as Number = 0; // previous index + private var _animProgress as Float = 1.0; // 0.0 → 1.0, 1.0 = done + private var _animStartMs as Number = 0; + function initialize() { View.initialize(); _items = Config.menuItems(); + _animTimer = new Timer.Timer(); var stored = Application.Storage.getValue(STORAGE_KEY); if (stored instanceof Number && stored >= 0 && stored < _items.size()) { _selectedIndex = stored; } + _animFrom = _selectedIndex; } function onLayout(dc as Dc) as Void { @@ -32,14 +44,31 @@ class MenuView extends WatchUi.View { } function rotateNext() as Void { + _startAnim(_selectedIndex); _selectedIndex = (_selectedIndex + 1) % _items.size(); Application.Storage.setValue(STORAGE_KEY, _selectedIndex); - WatchUi.requestUpdate(); } function rotatePrev() as Void { + _startAnim(_selectedIndex); _selectedIndex = (_selectedIndex - 1 + _items.size()) % _items.size(); Application.Storage.setValue(STORAGE_KEY, _selectedIndex); + } + + private function _startAnim(fromIndex as Number) as Void { + _animFrom = fromIndex; + _animProgress = 0.0; + _animStartMs = System.getTimer(); + _animTimer.start(method(:_animTick), ANIM_TICK_MS, true); + } + + function _animTick() as Void { + var elapsed = System.getTimer() - _animStartMs; + _animProgress = elapsed.toFloat() / ANIM_DURATION_MS.toFloat(); + if (_animProgress >= 1.0) { + _animProgress = 1.0; + _animTimer.stop(); + } WatchUi.requestUpdate(); } @@ -57,15 +86,38 @@ class MenuView extends WatchUi.View { var baseSize = LayoutMetrics.iconSize(dc); var selSize = LayoutMetrics.selectedIconSize(dc); var n = _items.size(); - var selectionAngle = Config.SELECTION_ANGLE_DEG * Math.PI / 180.0; + var step = 2.0 * Math.PI / n; + + // Ease-out for smooth deceleration. + var t = _animProgress; + var ease = 1.0 - (1.0 - t) * (1.0 - t); + + // Interpolate the rotation offset between old and new index. + var fromOffset = _animFrom.toFloat(); + var toOffset = _selectedIndex.toFloat(); + // Handle wrap-around (e.g. 9→0 or 0→9). + var diff = toOffset - fromOffset; + if (diff > n / 2.0) { diff = diff - n; } + if (diff < -n / 2.0) { diff = diff + n; } + var currentOffset = fromOffset + diff * ease; + for (var i = 0; i < n; i++) { - var offset = i - _selectedIndex; - var angle = selectionAngle + (2.0 * Math.PI * offset) / n; + var angle = selectionAngle + (i.toFloat() - currentOffset) * step; var x = (cx + radius * Math.cos(angle)).toNumber(); var y = (cy + radius * Math.sin(angle)).toNumber(); + + // Smoothly scale selected icon. var isSelected = (i == _selectedIndex); - var targetSize = isSelected ? selSize : baseSize; + var wasSelected = (i == _animFrom); + var targetSize = baseSize; + if (isSelected && wasSelected) { + targetSize = selSize; + } else if (isSelected) { + targetSize = (baseSize + (selSize - baseSize) * ease).toNumber(); + } else if (wasSelected) { + targetSize = (selSize - (selSize - baseSize) * ease).toNumber(); + } // Colored circle behind icon. var color = _items[i][:color] as Number; @@ -78,20 +130,22 @@ class MenuView extends WatchUi.View { _drawScaledIcon(dc, bmp, x, y, targetSize); } - if (isSelected) { + // Accent ring on selected item (also animated). + if (isSelected && ease > 0.5) { dc.setPenWidth(LayoutMetrics.accentPenWidth(dc)); dc.setColor(Config.COLOR_ACCENT, Config.COLOR_BG); dc.drawCircle(x, y, targetSize / 2 + 5); } } - var lines = _items[_selectedIndex][:lines] as Array; - dc.setColor(Config.COLOR_FG, Config.COLOR_BG); - TextUtils.drawCentered(dc, lines, cx, cy, Graphics.FONT_TINY); + // Only show label when animation is done to avoid flicker. + if (_animProgress >= 1.0) { + var lines = _items[_selectedIndex][:lines] as Array; + dc.setColor(Config.COLOR_FG, Config.COLOR_BG); + TextUtils.drawCentered(dc, lines, cx, cy, Graphics.FONT_TINY); + } } - // Scales a bitmap to the requested pixel size and draws it centered - // at (cx, cy). Uses drawBitmap2 so icons stay crisp on any resolution. private function _drawScaledIcon(dc as Dc, bmp as BitmapResource, cx as Number, cy as Number, targetSize as Number) as Void {