Animate allowed to call draw()?
I'm still getting into some of the more advanced features of pythonista...and I was wondering what was legal/illegal in the ui.animate() function.
How does it work, exactly? Underneath the hood, is it creating an objective-C block to call in some other thread from the UI and main threads? If so, is it legal to call draw() from within a method on a widget which is called from animate()?
So if I have a setup like:
class MyWidget(ui.View): ... def animatedDrawAction(self, sender): def _draw(): ...set some internal state while animated... self.draw() # redraw to reflect that new state ui.animate(_draw, 1.0)
is that allowed? Or is calling draw() going to do bad things if it isn't within the main thread? Should I instead call set_needs_display() within the animation and the UI thread will hopefully update as the animation runs?
Or am I completely missing the point here?
You never call draw of a UI View, unless you are trying to draw to an image. Set_needs_display is how you trigger a draw.
Because draw doesn't actually draw to the screen it draws to an Image Context that the OS sets up for you when it calls draw.
Only certain things can be animated, like view position, frame, tint, etc. -- not view contents.
You could try @mikael 's scripter for advanced animations
I'll take a look, thanks.
Actually, perhaps someone has a suggestion for the right approach to this: what I'm really looking to do is have a custom View control which reacts to a touch event and keeps animating for some time after the touch is over. So:
- User touches the control
- Control reacts by, for example, pulsating
- Pulsation continues for a couple seconds after user ends the touch, slowly lessening the effect over that duration until returning to normal
- If the user touches the control again while it is already pulsating, it starts pulsating again, effectively ending the previous animation and starting a new one
@shinyformica pulsating can probably be achieved using the alpha attribute of the view, which is animatable. You would have to see a bunch of completions to go from on to off to on, etc. Scripter probably makes this a lot easier.
@JonB "pulsation" was just a concept, since it doesn't really have a specific meaning. What I'm asking for here is to have the control potentially completely redrawn over and over while it animates (not a single attribute change-over-time, but a modification of the entire object in potentially complex ways). I'll look at Scripter, see what it does...seems like I'll be making use of more brilliant @mikael work.
@shinyformica the other approach of course is to use
updatewhich gets called at a fixed fps rate that you define. You would update internal state, then call set_needs_display
@jonb yeah, I was originally thinking of just doing it that way...but I'm already using update() to monitor a network I/O thread and update the interface as stuff comes in, so I was afraid of overloading that with work to do. Seemed like animate() was a good way to avoid it, since it seems to be an alternate thread of execution.
Animate and update are actually both called on the main thread (UI thread). Either way you have to execute things quickly to maintain ui responsiveness.
Well that makes sense, and with some testing it's clear the load the animation puts on the system is very minimal, so I can leave it in an update(). Thanks @JonB !
@shinyformica, Scripter also uses
update()under the hood, and was created to avoid writing same kind of boilerplate animation controlling logic there over and over again.
@shinyformica, as to your example, Scripter contains a pulse effect, and supports cancelling a running animation if you want to restart it (but alas, no simple ”restart” option for now).
@shinyformica Not sure if that helps, little dirty script to perform animation in an independant thread for each control. You can even tap both with two fingers at the same moment. Sorry if not useless
import ui import threading import time class my_thread(threading.Thread): def __init__(self,ctrl): threading.Thread.__init__(self) self.ctrl = ctrl self.delay = 0.05 self.delta = 0.0 def run(self): self.on = True while self.delay > 0: time.sleep(self.delay) if self.on: self.ctrl.background_color = (1,0,0) else: self.ctrl.background_color = 'white' self.on = not self.on self.delay = self.delay + self.delta # same or slower if self.delay >= 1: self.delay = 0 self.ctrl.background_color = 'white' class my_ctrl(ui.View): def __init__(self,frame=None,name=None): self.frame = frame self.name = name self.ctrl = ui.Label(frame = (0,0,self.width,self.height)) self.ctrl.background_color = 'lightgray' self.ctrl.text = 'tap here' self.add_subview(self.ctrl) self.server_thread = my_thread(self.ctrl) self.server_thread.lock = threading.Lock() def touch_began(self,touch): self.server_thread.lock.acquire() if not self.server_thread.is_alive(): self.server_thread.start() else: self.server_thread.delay = 0.05 self.server_thread.delta = 0.0 self.server_thread.lock.release() def touch_ended(self,touch): self.server_thread.lock.acquire() self.server_thread.delta = 0.05 self.server_thread.lock.release() v = ui.View() ctrl1 = my_ctrl(frame=(10, 10,100,30),name='ctrl1') v.add_subview(ctrl1) ctrl2 = my_ctrl(frame=(10,190,100,30),name='ctrl2') v.add_subview(ctrl2) v.frame = (0,0,300,400) v.present('sheet')
Thanks for the pointers for Scripter, I have that code open for reference :)
@mikael. @cvp this is cool, though for the moment I'm just having the control's update() with update_interval set to non-zero do what I need...which is working well enough. In the above code example, I notice you are never calling set_needs_display() in either the thread changing the color or the main code. Does that mean that under the hood, the actual modification of certain attributes automatically causes the control to get redrawn? Is there a list of which attrs will cause that? Or is it basically any attribute which might affect appearance? (color, tint, font, etc.)
@shinyformica the doc says
View.set_needs_display() Marks the view as needing to be redrawn. This usually only makes sense for custom View subclasses that implement a draw method.
I never use it if I modify ui elements in another thread
Good point @cvp...neither do I, I just expect changing an attribute which affects appearance to cause the control to update. My own custom Views call self.set_needs_display() in whatever property setters or methods change appearance. (and look at you, being careful with your thread locks...even in a write-only/read-only case! :))
You only need to call it off you implement draw. Other display attributes get updated automatically.
@jonb totally right, I was only really thinking about my own custom view, I was aware that changing attributes on regular controls caused them to update. For my purposes, I was looking for the "right" way to have an animated custom view react to user interaction.
a) calling draw() directly is never done, since it has no meaning outside a scheduled drawing context
b) calling set_needs_display() is allowed from anywhere, since the actual drawing will be scheduled and executed on the UI thread
c) there is no "other thread" which can do drawing/updating of UI elements
so, basically however I go about it, just make sure my actual draw() is as efficient as possible so the UI never gets bogged down.
This is python, of course, so the GIL is still in play, and all threads have to finish their work as quickly as possible, or yield control, in order to not cause trouble. Actually, I should ask: under the hood, there is only one interpreter instance, right? The python code on the UI thread and the python code on the main thread are both being executed by the same interpreter process? There's no magical communication happening between multiple python interpreters which side-steps the GIL as you can with multiprocessing?
There is one interpreter for python 2.7 and one for 3.6. but just one process.
Depending on what you are doing there are ways to keep draw quick:
Drawing an image may be faster than stroking hundreds of paths. You can render static things to an ImageContext, then later draw that inside draw. For instance omz's sketch example does that every touch_ended.
For framebuffer type access, see the recent thread which dealt with both some real time audio generation and IOSurfaceWrapper enabling some low level image pixel manipulation and display