Rotating an object on a touch event in kivy
kivy, layout, python, user-interface
Solution
You can bind angle of canvas to `NumericProperty`, to change it from inside your code. All you need to do is to compute those angles correctly. After playing a bit with it I created following code:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.lang import Builder
from kivy.animation import Animation
from kivy.properties import NumericProperty
import math
kv = '''
<Dial>:
canvas:
Rotate:
angle: root.angle
origin: self.center
Color:
rgb: 1, 0, 0
Ellipse:
size: min(self.size), min(self.size)
pos: 0.5*self.size[0] - 0.5*min(self.size), 0.5*self.size[1] - 0.5*min(self.size)
Color:
rgb: 0, 0, 0
Ellipse:
size: 50, 50
pos: 0.5*root.size[0]-25, 0.9*root.size[1]-25
'''
Builder.load_string(kv)
class Dial(Widget):
angle = NumericProperty(0)
def on_touch_down(self, touch):
y = (touch.y - self.center[1])
x = (touch.x - self.center[0])
calc = math.degrees(math.atan2(y, x))
self.prev_angle = calc if calc > 0 else 360+calc
self.tmp = self.angle
def on_touch_move(self, touch):
y = (touch.y - self.center[1])
x = (touch.x - self.center[0])
calc = math.degrees(math.atan2(y, x))
new_angle = calc if calc > 0 else 360+calc
self.angle = self.tmp + (new_angle-self.prev_angle)%360
def on_touch_up(self, touch):
Animation(angle=0).start(self)
class DialApp(App):
def build(self):
return Dial()
if __name__ == "__main__":
DialApp().run()
I'm calculating difference between initial (after pressing mouse) and later angle in `on_touch_move`. Since angle is a property I can also modify it using `kivy.animation` to make dial spin back after releasing mouse button.
EDIT
`on_touch_down` event for child circle:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.floatlayout import FloatLayout
from kivy.lang import Builder
from kivy.animation import Animation
from kivy.properties import NumericProperty
import math
kv = '''
<Dial>:
circle_id: circle_id
size: root.size
pos: 0, 0
canvas:
Rotate:
angle: self.angle
origin: self.center
Color:
rgb: 1, 0, 0
Ellipse:
size: min(self.size), min(self.size)
pos: 0.5*self.size[0] - 0.5*min(self.size), 0.5*self.size[1] - 0.5*min(self.size)
Circle:
id: circle_id
size_hint: 0, 0
size: 50, 50
pos: 0.5*root.size[0]-25, 0.9*root.size[1]-25
canvas:
Color:
rgb: 0, 1, 0
Ellipse:
size: 50, 50
pos: self.pos
'''
Builder.load_string(kv)
class Circle(Widget):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
print "small circle clicked"
class Dial(Widget):
angle = NumericProperty(0)
def on_touch_down(self, touch):
if not self.circle_id.collide_point(*touch.pos):
print "big circle clicked"
y = (touch.y - self.center[1])
x = (touch.x - self.center[0])
calc = math.degrees(math.atan2(y, x))
self.prev_angle = calc if calc > 0 else 360+calc
self.tmp = self.angle
return super(Dial, self).on_touch_down(touch) # dispatch touch event futher
def on_touch_move(self, touch):
y = (touch.y - self.center[1])
x = (touch.x - self.center[0])
calc = math.degrees(math.atan2(y, x))
new_angle = calc if calc > 0 else 360+calc
self.angle = self.tmp + (new_angle-self.prev_angle)%360
def on_touch_up(self, touch):
Animation(angle=0).start(self)
class DialApp(App):
def build(self):
return Dial()
if __name__ == "__main__":
DialApp().run()
Problem
I'm working on making a circle that will spin kind of like a large dial. Currently, I have an arrow at the top to show which direction the dial is facing. I'd like its behavior to be kind of like an old timey rotary phone, such that while your finger/cursor is down you can rotate it, but it'll (slowly) yank back to top after you let go. Here's what my object looks like: And here's my code: ``` #!/usr/bin/kivy import kivy kivy.require('1.7.2') import math from random import random from kivy.app import App from kivy.uix.widget import Widget from kivy.uix.gridlayout import GridLayout from kivy.uix.anchorlayout import AnchorLayout from kivy.uix.relativelayout import RelativeLayout from kivy.graphics import Color, Ellipse, Rectangle class MinimalApp(App): title = 'My App' def build(self): root = RootLayout() return(root) class RootLayout(AnchorLayout): pass class Circley(RelativeLayout): angle = 0 def on_touch_down(self, touch): ud = touch.ud ud['group'] = g = str(touch.uid) return True def on_touch_move(self, touch): ud = touch.ud # print(touch.x, 0) # print(self.center) # print(0, touch.y) # print(touch.x - self.center[0], touch.y - self.center[1]) y = (touch.y - self.center[1]) x = (touch.x - self.center[0]) calc = math.degrees(math.atan2(y,x)) angle = calc if calc > 0 else (360 + calc) print(angle) def on_touch_up(self, touch): touch.ungrab(self) ud = touch.ud return True if __name__ == '__main__': MinimalApp().run() ``` And the kv: ``` #:kivy 1.7.2 #:import kivy kivy <RootLayout>: anchor_x: 'center' # I think this /is/ centered anchor_y: 'center' canvas.before: Color: rgba: 0.4, 0.4, 0.4, 1 Rectangle: pos: self.pos size: self.size Circley: anchor_x: 'center' # this is /not/ centered. anchor_y: 'center' canvas.before: PushMatrix Color: rgba: 0.94, 0.94, 0.94, 1 Rotate: angle: self.angle axis: 0, 0, 1 origin: self.center Ellipse: source: 'arrow.png' size: min(self.size), min(self.size) pos: 0.5*self.size[0] - 0.5*min(self.size), 0.5*self.size[1] - 0.5*min(self.size) Label: text: unicode(self.size) # this is /not/ appearing color: 1,0,0,1 canvas.after: PopMatrix ``` Parts of that are borrowed from the kivy touchtracer demo, and from this SO question. You can see I have a calculation that is correctly printing the angle between the origin of the circle and the touch event (not sure how this'll respond to multiple fingers, haven't thought that far through), but not sure how to integrate this into a "spinning" feedback event in the interface.