10

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:

enter image description here

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.

Community
  • 1
  • 1
Mittenchops
  • 18,633
  • 33
  • 128
  • 246

2 Answers2

5

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()
Nykakin
  • 8,657
  • 2
  • 29
  • 42
  • This is probably out of scope for this question, but do you know how I could add a touch event to the small circle at the top? I assigned it an on_touch sound event just to make sure I knew what I was doing, but it seems I can either have rotation /or/ I can have the on_touch for the child circle---I can't seem to have both on at the same time. – Mittenchops Dec 14 '13 at 20:54
  • You have to dispatch `on_touch_down` event further using `super`. I added the example. – Nykakin Dec 14 '13 at 22:01
  • That seems to be double-printing both events? – Mittenchops Dec 14 '13 at 23:08
  • What is your desired behaviour? – Nykakin Dec 15 '13 at 02:13
  • Oh sorry, I was imagining it printing the "small circle clicked" only when the click is in the small circle and not the big circle, and printing "big circle clicked" when it is in the part of the big circle that does not contain small circles. Wherever I click, it prints both statements. – Mittenchops Dec 15 '13 at 16:53
  • That's because big circle contains small circle so when you're clicking small circle you're actually clicking big one too. I updated code to have it working the way you want. – Nykakin Dec 15 '13 at 17:10
3

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)
'''))
qua-non
  • 4,152
  • 1
  • 21
  • 25
  • Looks super cool, but when I tried to run, even after installing the module, I got ``` from kivy.uix.behaviors import ButtonBehavior ImportError: No module named behaviors ``` – Mittenchops Dec 12 '13 at 18:58
  • You need the latest kivy from master. Take a look at the installation instructions from here http://kivy.org/docs/installation/installation.html#development-version – qua-non Dec 12 '13 at 19:00
  • Hmm, weird, yep, I did install from master and still no luck. – Mittenchops Dec 13 '13 at 00:09
  • If you have kivy installed from master then it should work, be sure to remove any previous installs of kivy beforehand, Behaviors were introduced in 1.8. So if you have it properly installed then it should work. Look for the first line on your console it should mention the kivy version being used when running any kivy app. – qua-non Dec 13 '13 at 08:18
  • I have installed from master, I re-pulled source, I compiled in place, I ran make---I'm getting the same error. – Mittenchops Dec 13 '13 at 14:17
  • I'm raising a separate SO question about that here: http://stackoverflow.com/questions/20573382/kivy-behaviors-import-error – Mittenchops Dec 13 '13 at 18:25
  • The widget has been updated to include a orientation property that can be set to 'clockwise' and 'anti-clockwise'. Update the widget by runnint garden install --upgrade geartick – qua-non Dec 14 '13 at 09:31
  • Got it, thanks. I had to include another import, too: `from kivy.lang import Builder` – Mittenchops Dec 14 '13 at 20:11