Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
Joystick UIControl ui wrapper?
-
Dude... you're amazing. I need to learn to figure things out like this! I'll definitely be tinkering with how to use and understand this :D
-
@DoinStuffMobile glad you like it 😂 the only kwargs it can take right now are frame, backgroundColor, and action, so be wary of that.
I’m working to get a few more things for it to recognize, but the wrapper (hopefully) will handle most of those. -
Turns out I like doing stuff the hard way, and all the capabilities to make a joystick already exist in the ui module. I’ve redone the code, and now it’s fully wrapped by the ui module and a ton easier to understand.
import ui class Joystick (ui.View): def __init__(self,**kwargs): self.action=None self.stick=ui.View(name='stick') self._tint_color=None for key,val in kwargs.items(): if key=='frame' and val[2]!=val[3]: raise ValueError('Joystick must be square') setattr(self,key,val) if not self.bg_color: self.bg_color='#ffffff' if not self.tint_color: self.tint_color='grey' self.radius=self.width/2 self.corner_radius=self.radius self.stick.corner_radius=self.corner_radius self.stick.touch_enabled=False self.stick.frame=(0,0,self.width,self.height) self.originalPosition=self.stick.center self.objc_instance.setClipsToBounds_(False) self.add_subview(self.stick) return def touch_moved(self,touch): touch=touch.location touchVec=ui.Vector2( touch.x-self.radius, touch.y-self.radius) vecMagn=(touchVec.x**2+touchVec.y**2)**.5 if vecMagn < self.radius and touch.x < self.width and touch.y < self.height: newLoc=touch else: unitVec=ui.Vector2( touchVec.x/vecMagn, touchVec.y/vecMagn) newLoc=ui.Point( self.radius*unitVec.x+self.radius, self.radius*unitVec.y+self.radius) self.stick.center=newLoc.as_tuple() if self.action: self.action.__call__(self,touchVec) return def touch_ended(self,touch): self.stick.center=self.originalPosition return @property def tint_color(self): return self._tint_color @tint_color.setter def tint_color(self,value): self._tint_color=value self.stick.bg_color=value return if __name__=='__main__' : x,y=ui.get_screen_size() j=Joystick(name='joystick',tint_color='red') j.frame=(x//2-50,y//2-150,100,100) root=ui.View(frame=(0,0,x,y)) root.bg_color='white' def closeRoot(sender): sender.superview.close() return btn=ui.Button(name='button') btn.frame=(50,50,25,25) btn.image=ui.Image('iob:close_32') btn.tint_color='red' btn.action=closeRoot label=ui.Label(name='label') label.frame=(x-175,50,200,20) label.text='x,y' def moveButton(self,touchVec): btn=self.superview.subviews[1] label=self.superview.subviews[2] btn.center=( btn.center.x+touchVec.x/10, btn.center.y+touchVec.y/10) xstr=str(btn.center.x).split('.') ystr=str(btn.center.y).split('.') xstr=xstr[0]+'.'+xstr[1][:4] ystr=ystr[0]+'.'+ystr[1][:4] label.text=f'{xstr},{ystr}' return j.action=moveButton root.add_subview(j) root.add_subview(btn) root.add_subview(label) root.present('popover',hide_title_bar=True)
-
@mcriley821, you must be the first who discovered and used objc_util before the ui module. 😁
One pythonic hint is just the styling for readability. If you select ”Check Style” in the tools (”wrench”) menu, you can see there is quite a lot it does not like. Luckily, there is also ”Reformat Code” available – just have ”Apply Style Guide” and PEP8 selected, and you not have to worry about style.
-
@mcriley821, shorter way to format floats:
label_text = f'{btn.center.x:.4f}, {btn.center.y:.4f}'
-
@mikael said:
shorter way to format floats: label_text = f'{btn.center.x:.4f}, {btn.center.y:.4f}'
This is amazing😂😂😂 i can’t tell you how many times I’ve done string slices. No more!
Also, does pythonic just mean style? I always thought pythonic meant being ‘elegant’ (if that makes sense) and efficient. As you can tell from my code, I’m not big on the style lmao. I just want to make sure it’s efficient (time complexity).
-
@mcriley821, style is one part of being elegant. Elegant code is understandable and readable, i.e. not wasting the reader’s time.
No need to use
__call__
, just call it:self.action(self, touchVec)
-
@mcriley821, no need to manually calculate vector magnitude:
vecMagn = (touchVec.x**2 + touchVec.y**2)**.5
... Vector2 has you covered:
vecMagn = abs(touchVec)
-
@mcriley821, no need to spend effort trying to make sure the coordinates are integers:
j.frame = (x // 2 - 50, y // 2 - 150, 100, 100)
Coordinates are floats, and the decimals are even somewhat meaningful on Retina displays.
-
@mcriley821, also no need to navigate to btn and label in moveButton, the variables are already available (in the function’s closure).
-
@mcriley821, instead of:
btn.center = (btn.center.x + touchVec.x / 10, btn.center.y + touchVec.y / 10)
... you should be able to just say:
btn.center += touchVec / 10
... but for some reason it does not seem to exactly work.
-
@mcriley821, ui.View conveniently sets all its kwargs, so we can just call super() with them, no need to iterate manually. Also, defaults can be set before. tint_color we handle separately, as it is a custom property. (In general I would advice against overriding the ui.View properties, it will come back to haunt you.)
def __init__(self, tint_color='grey', **kwargs): kwargs.setdefault('background_color', 'black') super().__init__(**kwargs) ...
-
@mikael I iterate manually to catch a non-square frame. Is there a different way to make sure this happens? I feel like I also need to override the frame setter to catch it after init
-
@mcriley821, sorry, forgot to include that bit. Seems enough to check it after super(). If you want to enforce it continuously, layout() is a good option since, in addition frame, size can change via bounds, width and height.
def __init__(self, tint_color='grey', **kwargs): kwargs.setdefault('background_color', 'black') super().__init__(**kwargs) if self.frame.width != self.frame.height: raise ValueError('Joystick must be square') ...
-
@mcriley821, also makes sense to set self.stick.flex = 'WH', so that it will resize with its superview.
-
@mcriley821, taking the arithmetic capabilities of ui.Point/Vector2, the calculations can collapse to:
def touch_moved(self, touch): touch = touch.location radius_vector = [self.radius]*2 touch_vector = touch - radius_vector magnitude = abs(touch_vector) if magnitude < self.radius: self.stick.center = touch else: self.stick.center = ( touch_vector / magnitude * self.radius + radius_vector ) if self.action: self.action(self, touch_vector) return
Thank you for your patience, I think I am done.
-
@mikael I really appreciate you taking the time to help me and the improvements! I’ve already updated a bunch of stuff from your suggestions and I’m currently working on using the ‘iob:pinpoint' ionicon to add a type of texture to the ‘joystick’!
-
@mcriley821, interesting! Careful placement to get just the head of the pin?
-
@mcriley821, if you need a bit more flexibility, here’s an objc CAGradientLayer version, credits for an original axial version to @cvp:
import ui import objc_util CAGradientLayer = objc_util.ObjCClass('CAGradientLayer') class ShadedCircle(ui.View): def __init__(self, color1='#ff8484', color2='red', **kwargs): super().__init__(**kwargs) o_color1 = objc_util.UIColor.colorWithRed_green_blue_alpha_( *ui.parse_color(color1)).CGColor() o_color2 = objc_util.UIColor.colorWithRed_green_blue_alpha_( *ui.parse_color(color2)).CGColor() layer = self.objc_instance.layer() grlayer = CAGradientLayer.layer() grlayer.setType('radial') grlayer.frame = layer.bounds() grlayer.setColors_([o_color1, o_color2]) layer.insertSublayer_atIndex_(grlayer, 0) grlayer.setStartPoint(objc_util.CGPoint(0.25, 0.25)) grlayer.setEndPoint(objc_util.CGPoint(1.0, 1.0)) grlayer.locations = [0.0, 1.0] def layout(self): self.height = self.width self.corner_radius = self.width / 2 if __name__ == '__main__': v = ui.View() c = ShadedCircle( center=v.bounds.center(), flex='RTBL', ) v.add_subview(c) v.present('fullscreen', animated=False)
-
@mikael
I just subclassed a ui.View object again and did some more stuff to check for changing the frame later with layout. Had to use layout or it wasn’t resizing correctly, or changing some of the attributes to make the stick/texture of the joystick not resize.I decided to start a github repo for Pythonista and posted it in there.
https://github.com/mcriley821/Pythonista/blob/master/UIJoystick.py