Joystick UIControl ui wrapper?
-
Inspired by @DoinStuffMobile request for a Joystick-like control for the scene module, I created a very basic joystick using the objc_util module. I’ve gotten it working to use custom actions, pass a few kwargs in, and such things without crashing.
I was curious as to if there’s a way to wrap the objc_instance of the Joystick with the ui module similar to how @omz has with the other ui views?
Also, I need suggestions! What should I change to be more efficient, elegant, pythonic, etc? What are some things you guys think I should add?
Code is kinda long:
from objc_util import * import ui class Joystick (ui.View): def __new__(cls,**kwargs): inst=create_objc_class( 'UIJoystick', ObjCClass('UIControl'), methods=[Joystick.beginTrackingWithTouch_withEvent_,Joystick.continueTrackingWithTouch_withEvent_,Joystick.endTrackingWithTouch_withEvent_,Joystick.action] ).new() Joystick.__init__(inst,**kwargs) return inst def __init__(self,**kwargs): for key,val in kwargs.items(): if key =='action': self.customAction=val elif eval(f'self.{key}()').__class__ != self.aabbbbbbb.__class__: key=key[0].upper()+key[1:] if key == 'Frame': val=CGRect(CGPoint(val[0],val[1]),CGSize(val[2],val[3])) elif key == 'BackgroundColor': val=ObjCClass('UIColor').alloc().initWithRed_green_blue_alpha_(val[0],val[1],val[2],val[3]) eval(f'self.set{key}_(val)') #can add more kwargs self.radius=self.size().height/2 self._setCornerRadius_(self.radius) stick=ui.View(bg_color='#acacac') stick.touch_enabled=False stick.frame=( 0, 0, self.size().width, self.size().height) self.originalStickCenter=stick.center stick.corner_radius=self.size().width/2 self.addSubview_(stick.objc_instance) self.layer().setBorderColor_(ObjCClass('UIColor').alloc().initWithRed_green_blue_alpha_(0.67, 0.67, 0.67,1.0).CGColor) self.layer().setBorderWidth_(1) return @staticmethod def beginTrackingWithTouch_withEvent_(_self,_cmd,touch,event): #called when a touch enters #control's bounds return True @staticmethod def continueTrackingWithTouch_withEvent_(_self,_cmd,touch,event): #called when a touch event is #updated. like dragging #add code self=ObjCInstance(_self) touchLoc=ObjCInstance(touch).preciseLocationInView_(self) stick=self.subviews()[0] touchVec=CGVector( touchLoc.x-self.radius, touchLoc.y-self.radius) touchVecMagn=(touchVec.dx**2 + touchVec.dy**2)**.5 unitTouchVec=CGVector( touchVec.dx/touchVecMagn, touchVec.dy/touchVecMagn) if touchVecMagn < self.radius and touchLoc.x < self.size().width and touchLoc.y < self.size().height: newLoc=touchLoc else: newLoc=CGPoint( self.radius*unitTouchVec.dx+self.radius, self.radius*unitTouchVec.dy+self.radius) stick.setCenter_(newLoc) self.touchVec=touchVec self.sendAction_to_forEvent_('action',self,None) return True @staticmethod def endTrackingWithTouch_withEvent_(_self,_cmd,touch,event): #called when touch ends #add code touch=ObjCInstance(touch) self=ObjCInstance(_self) stick=self.subviews()[0] stick.setCenter_(CGPoint(self.originalStickCenter.x,self.originalStickCenter.y)) return True @staticmethod def action(_self,_cmd): self=ObjCInstance(_self) self.customAction.__call__(ObjCInstance(_self)) return if __name__=='__main__': x,y=ui.get_screen_size() root=ui.View( frame=(0,0,x,y), bg_color='white') def closeRoot(sender): root.close() return closeButton=ui.Button( frame=(50,75,24,24), image=ui.Image('iob:close_24'), tint_color='#ff0000', action=closeRoot ) def moveButton(sender): btn=sender.superview().subviews()[1] btn.setCenter_(CGPoint(btn.center().x+sender.touchVec.dx//10,btn.center().y+sender.touchVec.dy//10)) return j=Joystick( frame=(x//2-125,y//2-200,250,250), action=moveButton ) root.objc_instance.addSubview_(j) root.add_subview(closeButton) root.present('popover',hide_title_bar=True)
-
@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
-
@mcriley821 Wow, I clearly did not look hard enough initially! That's amazing!