-
Notifications
You must be signed in to change notification settings - Fork 0
/
circularlayout.py
192 lines (148 loc) · 6.69 KB
/
circularlayout.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
CircularLayout
==============
CircularLayout is a special layout that places widgets around a circle.
size_hint
---------
size_hint_x is used as an angle-quota hint (widget with higher
size_hint_x will be farther from each other, and viceversa), while
size_hint_y is used as a widget size hint (widgets with a higher size
hint will be bigger).size_hint_x cannot be None.
Widgets are all squares, unless you set size_hint_y to None (in that
case you'll be able to specify your own size), and their size is the
difference between the outer and the inner circle's radii. To make the
widgets bigger you can just decrease inner_radius_hint.
"""
__all__ = ('CircularLayout')
from kivy.uix.layout import Layout
from kivy.properties import NumericProperty, ReferenceListProperty, OptionProperty, \
BoundedNumericProperty, VariableListProperty, AliasProperty
from math import sin, cos, pi, radians, degrees
class CircularLayout(Layout):
'''Circular layout class. See module documentation for more information.
'''
padding = VariableListProperty([0, 0, 0, 0])
'''Padding between the layout box and it's children: [padding_left,
padding_top, padding_right, padding_bottom].
padding also accepts a two argument form [padding_horizontal,
padding_vertical] and a one argument form [padding].
.. versionchanged:: 1.7.0
Replaced NumericProperty with VariableListProperty.
:attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and
defaults to [0, 0, 0, 0].
'''
start_angle = NumericProperty(0)
'''Angle (in degrees) at which the first widget will be placed.
Start counting angles from the X axis, going counterclockwise.
:attr:`start_angle` is a :class:`~kivy.properties.NumericProperty` and
defaults to 0 (start from the right).
'''
circle_quota = BoundedNumericProperty(360, min=0, max=360)
'''Size (in degrees) of the part of the circumference that will actually
be used to place widgets.
:attr:`circle_quota` is a :class:`~kivy.properties.BoundedNumericProperty`
and defaults to 360 (all the circumference).
'''
direction = OptionProperty("ccw", options=("cw", "ccw"))
'''Direction of widgets in the circle.
:attr:`direction` is an :class:`~kivy.properties.OptionProperty` and
defaults to 'ccw'. Can be 'ccw' (counterclockwise) or 'cw' (clockwise).
'''
outer_radius_hint = NumericProperty(1)
'''Sets the size of the outer circle. A number greater than 1 will make the
widgets larger than the actual widget, a number smaller than 1 will leave
a gap.
:attr:`outer_radius_hint` is a :class:`~kivy.properties.NumericProperty` and
defaults to 1.
'''
inner_radius_hint = NumericProperty(.6)
'''Sets the size of the inner circle. A number greater than
:attr:`outer_radius_hint` will cause glitches. The closest it is to
:attr:`outer_radius_hint`, the smallest will be the widget in the layout.
:attr:`outer_radius_hint` is a :class:`~kivy.properties.NumericProperty` and
defaults to 1.
'''
radius_hint = ReferenceListProperty(inner_radius_hint, outer_radius_hint)
'''Combined :attr:`outer_radius_hint` and :attr:`inner_radius_hint` in a list
for convenience. See their documentation for more details.
:attr:`radius_hint` is a :class:`~kivy.properties.ReferenceListProperty`.
'''
def _get_delta_radii(self):
radius = min(self.width-self.padding[0]-self.padding[2], self.height-self.padding[1]-self.padding[3]) / 2.
outer_r = radius * self.outer_radius_hint
inner_r = radius * self.inner_radius_hint
return outer_r - inner_r
delta_radii = AliasProperty(_get_delta_radii, None, bind=("radius_hint", "padding", "size"))
def __init__(self, **kwargs):
super(CircularLayout, self).__init__(**kwargs)
self.bind(
start_angle=self._trigger_layout,
parent=self._trigger_layout,
# padding=self._trigger_layout,
children=self._trigger_layout,
size=self._trigger_layout,
radius_hint=self._trigger_layout,
pos=self._trigger_layout)
def do_layout(self, *largs):
# optimize layout by preventing looking at the same attribute in a loop
len_children = len(self.children)
if len_children == 0:
return
selfcx = self.center_x
selfcy = self.center_y
direction = self.direction
cquota = radians(self.circle_quota)
# selfw = self.width
# selfh = self.height
start_angle_r = radians(self.start_angle)
padding_left = self.padding[0]
padding_top = self.padding[1]
padding_right = self.padding[2]
padding_bottom = self.padding[3]
padding_x = padding_left + padding_right
padding_y = padding_top + padding_bottom
radius = min(self.width-padding_x, self.height-padding_y) / 2.
outer_r = radius * self.outer_radius_hint
inner_r = radius * self.inner_radius_hint
middle_r = radius * sum(self.radius_hint) / 2.
delta_r = outer_r - inner_r
# calculate maximum space used by size_hint
stretch_weight_angle = 0.
for w in self.children:
sha = w.size_hint_x
if sha is None:
raise ValueError("size_hint_x cannot be None in a CircularLayout")
else:
stretch_weight_angle += sha
sign = +1.
angle_offset = start_angle_r
if direction == 'cw':
angle_offset = 2 * pi - start_angle_r
sign = -1.
for c in reversed(self.children):
sha = c.size_hint_x
shs = c.size_hint_y
angle_quota = cquota / stretch_weight_angle * sha
angle = angle_offset + (sign * angle_quota / 2)
angle_offset += sign * angle_quota
# kived: looking it up, yes. x = cos(angle) * radius + centerx; y = sin(angle) * radius + centery
ccx = cos(angle) * middle_r + selfcx + padding_left - padding_right
ccy = sin(angle) * middle_r + selfcy + padding_bottom - padding_top
c.center_x = ccx
c.center_y = ccy
if shs:
s = delta_r * shs
c.width = s
c.height = s
if __name__ == "__main__":
from kivy.app import App
from kivy.uix.button import Button
class CircLayoutApp(App):
def build(self):
cly = CircularLayout(direction="cw", start_angle=-75, inner_radius_hint=.7, padding="20dp")
for i in xrange(1, 13):
cly.add_widget(Button(text=str(i), font_size="30dp"))
return cly
CircLayoutApp().run()