Rotating an object on a touch event in kivy

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()

You can use GearTick from garden which is a rotating slider. It's not exactly what you need but can be adapted for your needs. "By default it allows rotation anti-clockwise you probably would need it to go clockwise"(Update: The widget now has a orientation property that can be set to 'clockwise' or 'anti-clockwise').

You would need to manage the spring back and stopping at the "finger stop".

The example at the ends manage spring back using animation, however you still need to manage/implement the finger stop functionality.

https://github.com/kivy-garden/garden.geartick

Usage::

Python::

from kivy.garden.geartick import GearTick
parent.add_widget(GearTick(range=(0, 100)))

kv::

BoxLayout:
    orientation: 'vertical'
    GearTick:
        id: gear_tick
        zoom_factor: 1.1
        # uncomment the following to use non default values
        #max: 100
        #background_image: 'background.png'
        #overlay_image: 'gear.png'
        #orientation: 'anti-clockwise'
        on_release:
            Animation.stop_all(self)
            Animation(value=0).start(self)
    Label:
        size_hint: 1, None
        height: '22dp'
        color: 0, 1, 0, 1
        text: ('value: {}').format(gear_tick.value)

enter image description here

To install::

pip install kivy-garden
garden install geartick

Working Example that you can copy paste::

from kivy.lang import Builder
from kivy.app import runTouchApp
from kivy.garden.geartick import GearTick
runTouchApp(Builder.load_string('''
#:import Animation kivy.animation.Animation
GridLayout:
    cols: 2
    canvas.before:
        Color:
            rgba: 1, 1, 1, 1
        Rectangle:
            size: self.size
            pos: self.pos
    BoxLayout:
        orientation: 'vertical'
        GearTick:
            id: gear_tick
            zoom_factor: 1.1
            # uncomment the following to use non default values
            #max: 100
            #background_image: 'background.png'
            #overlay_image: 'gear.png'
            #orientation: 'anti-clockwise'
            on_release:
                Animation.stop_all(self)
                Animation(value=0).start(self)
        Label:
            size_hint: 1, None
            height: '22dp'
            color: 0, 1, 0, 1
            text: ('value: {}').format(gear_tick.value)
'''))