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?
@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