Forum Archive

ui.Animate wider usage

Phuket2

@omz, is it possible to fudge something so ui.animate works a little differently than it does at the moment.

It would be very convenient if you could call ui.animate with a start, finish and increment along with its duration parameter. And it passes back the value on each call to the defined function.

I am pretty sure ui.animate is written in objective c as I could not find it in the ui file.

It may simple to write for you, but not for me. But I think this functionality would be welcomed by many here.

But again, if there is a way to fudge it so I can rely on the same mechanism, that would be great.

Phuket2

I tried the below, but it appears you don't update the objects attrs as you do the transition. I just tried to fake it using the alpha value, but it's not updated
yes I see increments is a stupid argument now duration defines the increments

def animate(self, sender = None):
        print 'in animate...'
        def animation():
            self.alpha = 0 # fade out
            self.value = self.alpha
        ui.animate(animation, duration=2)
omz

Could you give me a concrete example of an effect you're trying to achieve?

Phuket2

@omz , just trying to animate/draw the class below. Meaning just set the value from 0.0 to 1.0 for the duration specified. I have tried this with a method with @ui.in_background. I got some strange results. I am sure it was just me doing something stupid, but just seems like the mechanism is already there.

class CircleProgress(ui.View):
    def __init__(self, *args , **kwargs):
        self.min = 0.
        self.max = 1.0
        self.v = 0.
        self.increments = .01
        self.margin = (0,0,0,0)
        self.drawing_on = True
        self.alpha = 1
        self.fill_color = 'red'
        self.stroke_color = 'black'

        self.font =('Arial Rounded MT Bold', 18)
        self.label_on = True

        self.set_attrs(**kwargs)

        btn = ui.Button(name = 'inc')
        btn.action = self.animate
        btn.width = self.width
        btn.height = self.height
        btn.flex = 'wh'
        self.add_subview(btn)
        btn.bring_to_front()

    def inc(self, sender = None):
        self.value += self.increments
        if self.value > self.max: self.value = self.min
        self.set_needs_display()

    def dec(self):
        self.value -= self.increments
        if self.value < self.min: self.value = self.max
        self.set_needs_display()

    def value(self, v):
        if v < self.min or v > self.max:
            return
        self.value = v

        self.set_needs_display()

    def draw(self):
        if not self.drawing_on : return 

        cr = ui.Rect(*self.bounds).inset(*self.margin)

        p = max(0.0, min(1.0, self.value))
        r = cr.width / 2
        path = ui.Path.oval(*cr)
        path.line_width = .5
        ui.set_color(self.stroke_color)
        path.stroke()
        center = ui.Point(r, r)
        path.move_to(center.x, center.y)
        start = radians(-90)
        end = start + p * radians(360)
        path.add_arc(r, r, r, start, end)
        path.close()
        ui.set_color(self.fill_color)
        path.eo_fill_rule = True
        path.fill()

        self.draw_label()

    def draw_label(self):
        if not self.drawing_on : return 
        if not self.label_on: return

        cr = ui.Rect(*self.bounds)
        ui.set_color('purple')
        s = str('{:.0%}'.format(self.v))

        dim = ui.measure_string(s, max_width=cr.width, font=self.font, alignment=ui.ALIGN_CENTER, line_break_mode=ui.LB_TRUNCATE_TAIL)

        lb_rect = ui.Rect(0,0, dim[0], dim[1])
        lb_rect.center(cr.center())

        ui.draw_string(s, lb_rect , font=self.font, color='black', alignment=ui.ALIGN_CENTER, line_break_mode=ui.LB_TRUNCATE_TAIL)

    def set_attrs(self, **kwargs):
        for k,v in kwargs.iteritems():
            if hasattr(self, k):
                setattr(self, k, v)

    def animate(self, sender = None):
        print 'in animate...'
        def animation():
            self.alpha = 0 # fade out
            self.value = self.alpha
            self.set_needs_display()
        ui.animate(animation, duration=2)

Phuket2

@omz this also relates. In Pythonista startup I have

from objc_util import *
UIView.beginAnimations_(None)
UIView.setAnimationDuration_(0)

From you.

It also stops ui.animate() animating , which makes sense. When I am testing now, i comment out these lines And shut down Pythonista , and restart. Is it possible to easily reverse this in code. I tried to pass UIView.beginAnimations_(None) 1 instead of None. Just crashed.

Again, just useful for testing. I prefer to have the Animations off most of the time. But if inside a .py file I can turn them on, then off again, that would be great.

JonB

UIView.commitAnimations()

will close out the beginAnimations and effectively undo the duration of none.

Your above code seems to mix value as a method, and value as a variable, no? Perhaps you are missing a property decorator?

Also, not sure you need or should put a set needs display inside the animation function. I think that would only be executed once. I think you want to set heeds display when the value changes, but no other times. If you are trying to animate just the alpha, then what you have works. If you are trying to animate changing value, this approach will not work, you will need to have your own thread, or call ui.delay which calls itself, etc. animate can only animate changes to properties, such as frame, colors, transforms, basically things that ios knows how to draw and knows how to interpolate. iOS does not onow how to interpolate a custom draw function.

omz

As @JonB already pointed out, ui.animate isn't really suitable for animating custom attributes/drawing.

You can get the effect you're looking for by combining ui.animate (for fading in/out) with ui.delay (for changing the progress and redrawing). I posted an example of a simple animation using ui.delay in this thread (not exactly what you want, but the technique would be similar).

Phuket2

@JonB , thanks. Yes I had changed the value property aend realized I need the set_needs_display() in the animate method. I had just been changing things around

Phuket2

@JonB , thanks. UIView.commitAnimations() Works as expected.Very convenient.
I will try @omz code for the animate. For this class not part of the functionality, more use a built in test to be sure the min, max , v are working correctly. Will be more useful for other things though

Phuket2

@JonB , I know this question sort of seems stupid. But I was looking up flex on the forum. I have never really felt in control of it. Anyway, I remembered the animation you did a long time ago this_post
But if you had wanted to draw out realtime b.x, b.y, b.height , b.width for example this would be have been impossible as far as I can see. Using this method that is.
Look it's ok, I accept the way it is. Just my use case did not really give a clear idea about the possible uses.

But anyone reading this and have difficulties getting your head around flex, @JonB ' s code post at the link is visual and very helpful

Phuket2

@omz , @JonB or @anyoneelse.
I did the below just using ui.Delay based on the conversations above. Again, it's simple, but often the handiest things are.

So it's just a class trying to give a framework for a callback over a duration with the number range 0.0 to 1.0. Just trying to emulate part of ui.animate. The time for me is the tricky thing. But what I have done seems to accurate to amount the 100ths of a second.

Just overall I don't know if the approach is a good one or not. Maybe there are some really bad flaws in this approach. Any feedback welcome, scathing feedback is fine.

It's a little long for posting. I thought about a gist, but thought I have a better chance to get feedback if it's listed here

import ui
import time, warnings

class CallMeBack(object):
    '''
        CLASS - CallMeBack

        Purpose
        to call a user supplied function 100 times with simlar units of 
        elapased time between calls with a number between 0.0 and 1.0 for
        the passed duration.

        This is an attempt to mimick the part of @omz's animate function,
        that you can set and forget to get a series of numbers 
        between 0 - 1.0 over a specfic duration without binding to any 
        attributes. just passes the value to the supplied function/method

        its only work in progress. i am certin will have to refactor. i am guessing i will need to have some @classmethod decorators to 
        make it as flexible as @omz's ui.animate. this is at least a start.

        A pause method? maybe does not make sense

    '''

    def __init__(self, func, duration = 1.0, begin = True, 
                        on_completion = None, obj_ref = None):

        self.iterations = 0         # a counter of the num of iterations done
        self.duration = duration    # the time to spead 100 callbacks over
        self.time_unit = 0          # 1/100th of the duration
        self._value = 0             # current value

        self.func = func                # the callers func that is called

        # if supplied, calls this function after the 100 iterations  
        self.on_completion = on_completion

        self.start_time = 0         # is set once the start method is invoked
        self.finish_time = 0        # used for debug. measure how long we took 

        self.working = False        # a flag, to protect from being called
                                            # whilst running. we are not reenterant

        # so its possible to store a reference to a object, which you can
        # recover in the callback function
        self.obj_ref = obj_ref

        # start from init with begin = True *arg
        if begin:
            self.start()

    def start(self, duration = None):
        # the timing and process start method. can be called from __init__
        # or here externally

        # aviod being called while running.
        if self.working:
            warnings.warn('CallMeBack cannot be called whilst it is running.')
            return

        # block being called again until we finish
        self.working = True

        if duration:
            self.duration = duration

        self.start_time = time.time()
        self.time_unit = self.duration / 100.

        # would be nice to have exp, log etc... 
        ui.delay(self._work_linear(), 0)

    def _work_linear(self):
        # the method that is called 100 times at evenlyish time intervals
        # which in turns calls the callers function with numbers 0.0 to 1.0 

        # i know, this can be better. for now its ok to spell it out
        _expected_time = self.iterations  * self.time_unit
        _real_time =time.time() - self.start_time
        _diff = _expected_time - _real_time

        if self.iterations == 100:
            # hmmm, call last time
            self.func(self, 1.0)

            # we are finished
            self.finish_time = time.time()
            ui.cancel_delays()
            print 'duration {}, total time {}, difference {}'.format(self.duration , self.finish_time - self.start_time,
                    (self.finish_time - self.start_time) - self.duration)

            # if a completion routine has been defined, is called here
            if self.on_completion:
                self.on_completion(self)

            # reset some values, so calling multiple times work 
            self.reset()
            self.working = False
            return

        # call the callers function with ref to this object and the current
        # value
        self._value = self.iterations / 100.
        self.func(self, self._value)
        self.iterations += 1

        # call ui.delay with .95% of the time unit we have. 
        # this seems ok at the moment. if a lot of processing happens
        # in the callers function, it will start to fall behind
        # hofully _diff counteracts it
        ui.delay(self._work_linear, (self.time_unit ) + _diff)

    @property
    def value(self):
        return self._value

    @property
    def finished(self):
        return self.working

    def reset(self):
        # reset some vars, in the event we are started again
        self.iterations = 0
        self._value = 0
                self.working = False

    def cancel(self, ignore_completion = False, finish_with = None):
        # for manual cancelling
        # finish_with just allows your func to be called a last time
        # with a value like 1.0 Could provide a more appealling effect

        ui.cancel_delays()
        if finish_with:
            self.func(finish_with)

        if self.on_completion:
            if not ignore_completion:
                self.on_completion(self)

Phuket2

Was not sure how the below pattern would work with something real. It worked fine.

def test_animate(self, sender = None):
        def inc(sender, v):
            self.value(v)
        cb = CallMeBack(inc, duration = 5.0)
# another version 
def test_animate(self, sender = None):
        self.alpha = 0
        def inc(sender, v):
            self.value(v)
            self.alpha = v
        cb = CallMeBack(inc, duration = 5.0)