Complete
over 5 years ago

WatchUi.CancellAllAnimations() is not expected to function on a 3.1.6 device.

Animations don't run correctly on the real watch

Hi,

I started to include animations on some drawables by using the animate() method. This works good. But if I switch too fast between screens especially switch a screen if one animation didn't end on the hiding screen, the animation on the next screen doesn't finish and stops anywhere in the middle ending up with a messed screen. 

This happens only on my F6X pro and not in the sim. 

I tried to include an

Ui.cancelAllAnimations();

in the onHide() method, but this throws an error as if the function wouldn't exist at all:

Error: Symbol Not Found Error
Details: Could not find symbol 'cancelAllAnimations'

Are there any tricks to get a proper animation even if screens are switched fast?

  • Hi Stephen, Hi Travis,

    I implemented it now this way: I start a timer along with the animation and prevent any further animation from starting when the timer runs. This works pretty well. Saying that, I do this only for versions below 3.1.7. Otherwise the cancelAllAnimaitons() is used. Luckily I got yesterday 3.1.8 on my F6x Pro with the current beta version. 

    And what shall I say: it doesn't work either with cancelAllAnimationis(). The watch is still messed up if screens are switched too fast although I use cancelAllAninmation() in onHide().

  • Do you still believe there is a bug with WatchUi.CancelAllAnimations() ? or is the issue you were seeing because the device is connectIQVersion="3.1.6"?

    To me, it looks like expected behavior, but do not hesitate to ping me if you believe there is a bug.

  • I started this earlier in the day. Since you seemed interested, and I thought it might be helpful...

    using Toybox.Application;
    using Toybox.Lang;
    using Toybox.Math;
    using Toybox.Timer;
    using Toybox.System;
    using Toybox.WatchUi;
    
    (:ciq_3_1_7_or_later)
    module Animator
    {
        function animate(object, property, type, start, stop, period, callback) {
            return WatchUi.animate(object, property, type, start, stop, period, callback);
        }
        
        function cancelAllAnimations() {
            return WatchUi.cancelAllAnimations();
        }
    }
    
    (:not_ciq_3_1_7_or_later)
    module Animator
    {
        // DO NOT USE THIS CLASS. it is internal to the implementation.
        class Animation
        {
            hidden var object;
            hidden var property;
            hidden var type;
            hidden var startValue;
            hidden var endValue;
            hidden var period;
            hidden var callback;
    
            // first update indicator
            hidden var first;
    
            // calculated on first update
            hidden var startTime;
            hidden var endTime;
    
            function initialize(object, property, type, start, stop, period, callback) {
                self.object = object;
                self.property = property;
                self.type = type;
                self.startValue = start;
                self.endValue = stop;
                self.period = period * 1000; // input parameter is seconds, we want milliseconds
                self.first = true;
                self.callback = callback;
            }
    
            hidden function interpolateTime(absoluteTime) {
                var currentTime = absoluteTime - startTime;
    
                var percent0to1 = currentTime / ((endTime - startTime) * 1.0f);
                if (type == WatchUi.ANIM_TYPE_LINEAR) {
                    return percent0to1;
                } else if (type == WatchUi.ANIM_TYPE_EASE_IN) {
                    return Math.pow(percent0to1, 3);
                } else if (type == WatchUi.ANIM_TYPE_EASE_OUT) {
                    var negative1to0 = percent0to1 - 1;
                    return 1.0f * (Math.pow(negative1to0, 3) + 1.0f);
                } else if (type == WatchUi.ANIM_TYPE_EASE_IN_OUT) {
                    var percent0to2 = percent0to1 * 2;
                    if (percent0to1 < 0.5f) {
                        return 0.5f * Math.pow(percent0to2, 3);
                    } else {
                        var negative2to0 = percent0to2 - 2;
                        return 0.5f * ( Math.pow(negative2to0, 3) + 2.0f );
                    }
                }
    
                return percent0to1;
            }
    
            function update(absoluteTime) {
            
                if (first) {
                    first = false;
    
                    // set the start and end times
                    startTime = absoluteTime;
                    endTime = absoluteTime + period;
    
                    // set the initial value
                    object[property] = startValue;
                } else if (absoluteTime < endTime) {
                    var percent0to1 = interpolateTime(absoluteTime);
                    
                    var newValue;
                    if (startValue < endValue) {
                        newValue = startValue + ( endValue - startValue ) * percent0to1;
                    } else {
                        newValue = startValue - ( startValue - endValue ) * percent0to1;
                    }
                    
                    object[property] = newValue;                
                } else {
                    object[property] = endValue;
                    return false;
                }
    
                return true;
            }
    
            function cancel() {
                object[property] = endValue;
            }
    
            function notify() {
                if (callback != null) {
                    callback.invoke();
                }
    
                callback = null;
            }
        }
    
        // DO NOT ACCESS THIS. it is internal to the implementation.
        var _animations = [];
    
        // DO NOT ACCESS THIS. it is internal to the implementation.
        var _timer;
    
        function animate(object, property, type, start, stop, period, callback) {
            var animation = new Animation(object, property, type, start, stop, period, callback);
    
            // look to see if there is a hole to fill
            var i = 0;
            for (i = 0; i < _animations.size(); ++i) {
                if (_animations[i] == null) {
                    _animations[i] = animation;
                    break;
                }
            }
    
            // we didn't find a hole, so insert at the back
            if (_animations.size() == i) {
                _animations.add(animation);
            }
    
            _startTimer();
        }
    
        function cancelAllAnimations() {    
            _stopTimer();
    
            // cancel all of the animations
            for (var i = 0; i < _animations.size(); ++i) {
                var animation = _animations[i];
                if (animation != null) {
                    animation.cancel();
                }
            }
    
            _animations = [];
        }
    
        // DO NOT CALL THIS. it is internal to the implementation.
        function _startTimer() {
            if (_timer == null) {
                _timer = new Timer.Timer();
                _timer.start(new Lang.Method(Animator, :_onTimer), 50, true);
            }
        }
    
        // DO NOT CALL THIS. it is internal to the implementation.
        function _stopTimer() {
            if (_timer != null) {
                _timer.stop();
                _timer = null;
            }
        }
    
        // DO NOT CALL THIS. it is internal to the implementation.
        function _onTimer() {
            var absoluteTime = System.getTimer();
    
            for (var i = 0; i < _animations.size(); ++i) {
                var animation = _animations[i];
                if (animation != null && !animation.update(absoluteTime)) {
                    // remove the animation from our list
                    _animations[i] = null;
    
                    // invoke the callback
                    animation.notify();
                }
            }
            
            WatchUi.requestUpdate();
        }
    }
    
    class AnimatorView extends WatchUi.View {
    
        hidden var bitmapDrawables;
        hidden var minX;
        hidden var maxX;
    
        function initialize() {
            View.initialize();
        }
    
        function onLayout(dc) {
            var imageResource = WatchUi.loadResource(Rez.Drawables.LauncherIcon);
            var imageHeight = imageResource.getHeight();
            var imageWidth  = imageResource.getWidth();
            
            var offset = 5;
            
            minX = offset;
            maxX = dc.getWidth() - imageWidth - offset;
    
            bitmapDrawables = new [4];
            
            var locY = dc.getHeight() / 2;
            locY -= (bitmapDrawables.size() * imageHeight / 2);
     
            for (var i = 0; i < bitmapDrawables.size(); ++i) {
                var drawable = new WatchUi.Bitmap({
    	            :locX => minX,
    	            :locY => locY,
    	            :rezId => Rez.Drawables.LauncherIcon
                });
                
                bitmapDrawables[i] = drawable;
    
                locY += (imageHeight + offset);
            }
            
            setLayout(bitmapDrawables);
        }
        
        hidden var animators = 0;
        
        function animateOrCancel() {
            if (animators == 0) {
                for (var i = 0; i < bitmapDrawables.size(); ++i) {
                    ++animators;
                    Animator.animate(bitmapDrawables[i], :locX, i, minX, maxX, 5.0, self.method(:onAnimationComplete));
                }
            } else {
                Animator.cancelAllAnimations();
                animators = 0;
                
                WatchUi.requestUpdate();
            }
        }
    
        function onAnimationComplete() {
            --animators;
        }
    }
    
    
    class AnimatorDelegate extends WatchUi.BehaviorDelegate {
    
        hidden var view;
        
        function initialize(view) {
            BehaviorDelegate.initialize();
            self.view = view;
        }
        
        function onSelect() {
            view.animateOrCancel();
        }
    
    }
    
    class AnimatorApp extends Application.AppBase {
    
        function initialize() {
            AppBase.initialize();
        }
    
        function getInitialView() {
            var view = new AnimatorView();
            return [ view, new AnimatorDelegate(view) ];
        }
    
    }
    

    And then my stub jungle file... I'll add entries below as devices add support for 3.1.7... this saves ~2K of memory if you can use the WatchUi implementation.

    project.manifest = manifest.xml
    
    ##
    ## assume all devices don't support 3.1.7
    ##
    
    base.excludeAnnotations = ciq_3_1_7_or_later
    
    ##
    ## devices that are known to support 3.1.7 would go here
    ##
    
    #fenix6xpro.excludeAnnotations = not_ciq_3_1_7_or_later
    

  • Is there a code snippet for that how to handle this? Do you mean to start a timer and fire it every 20 ms
    > or so and firing a request update? This may probably be the best workaround then.

    Yeah, sort of. The minimum timer period is 50ms.

    If I were in your shoes, I'd implement something that looked exactly like the WatchUi.animate() and WatchUi.cancelAllAnimations() APIs. I'd create one version that wraps the provided APIs, and then my version to be used for devices that don't have the support you want, then I'd switch between the configurations using a jungle file. This should allow you to phase out your implementation and save memory on devices that don't need it. Of course you'd have to update the jungle file and upload new versions, but that shouldn't be a big issue.

    It shouldn't be that hard to code up.

  • Edit: Devices typically lag behind the current SDK version.