Skip to content

Commit f046082

Browse files
committed
add error handling and warning messages, update docstrings, clarify input parameter naming
1 parent 8a436b5 commit f046082

File tree

2 files changed

+121
-47
lines changed

2 files changed

+121
-47
lines changed

adafruit_displayio_layout/widgets/icon_animated.py

Lines changed: 117 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"""
2424
import gc
2525
import time
26+
from math import pi
2627
import bitmaptools
2728
from displayio import TileGrid, Bitmap, Palette
2829
import adafruit_imageload
@@ -43,16 +44,18 @@ class IconAnimated(IconWidget):
4344
4445
:param str label_text: the text that will be shown beneath the icon image.
4546
:param str icon: the filepath of the bmp image to be used as the icon.
46-
:param bool on_disk: if True use OnDiskBitmap instead of imageload.
47-
This can be helpful to save memory. Defaults to False
48-
49-
:param float max_scale: the maximum zoom during animation, set 1.0 for no animation
50-
a value of 1.4 is a good starting point (default: 1.0, no animation),
51-
``max_scale`` must be between 1.0 and 1.5.
52-
:param float max_angle: the maximum degrees of rotation during animation, positive values
53-
are clockwise, set 0 for no rotation, in degrees (default: 0 degrees)
47+
:param bool on_disk: if True use OnDiskBitmap instead of imageload to load static
48+
icon image. This can be helpful to save memory. (default: False) Note: Bitmap
49+
file must use indexed colors to allow animations in the IconAnimated widget.
50+
51+
:param float scale: the maximum zoom during animation, set 1.0 for no zoom.
52+
A value of 1.5 is a good starting point. The ``scale`` can be less than
53+
1.0 for shrink animations. (default: same as ``max_scale`` set in ``init_class``),
54+
55+
:param float angle: the maximum degrees of rotation during animation, positive values
56+
are clockwise, set 0 for no rotation, in degrees (default: 4 degrees)
5457
:param float animation_time: the time for the animation in seconds, set to 0.0 for
55-
no animation, a value of 0.15 is a good starting point (default: 0.0 seconds)
58+
no animation, a value of 0.15 is a good starting point (default: 0.15 seconds)
5659
5760
:param int x: x location the icon widget should be placed. Pixel coordinates.
5861
:param int y: y location the icon widget should be placed. Pixel coordinates.
@@ -62,8 +65,8 @@ class IconAnimated(IconWidget):
6265
:param int anchored_position: (x,y) pixel value for the location of the anchor_point
6366
:type anchored_position: Tuple[int, int]
6467
:param int max_size: (Optional) this will get passed through to the
65-
displayio.Group constructor. If omitted we default to
66-
grid_size width * grid_size height to make room for all (1, 1) sized cells.
68+
displayio.Group constructor. ``max_size`` should be set to the maximum number of
69+
graphical elements that will be held within the Group of this widget.
6770
"""
6871

6972
# pylint: disable=bad-super-call, too-many-instance-attributes, too-many-locals
@@ -75,33 +78,55 @@ class IconAnimated(IconWidget):
7578

7679
@classmethod
7780
def init_class(
78-
cls, display=None, max_scale=1.5, max_size=(80, 80), max_color_depth=512
81+
cls, display=None, max_scale=1.5, max_icon_size=(80, 80), max_color_depth=256
7982
):
8083
"""
8184
Initializes the IconAnimated Class variables, including preallocating memory
82-
buffers for the icon zoom bitmap and icon palette.
85+
buffers for the icon zoom bitmap and icon zoom palette.
8386
8487
.. Note:: The `init_class` class function must be called before instancing any
8588
IconAnimated widgets. Usage example:
8689
``IconAnimated.init_class(display=board.DISPLAY, max_scale=1.5,
87-
max_size=(80,80), max_color_depth=256)``
90+
max_icon_size=(80,80), max_color_depth=256)``
8891
8992
:param displayio.Display display: The display where the icons will be displayed.
90-
:param float max_scale: The maximum zoom of the any of the icons, should be > 1.0,
93+
:param float max_scale: The maximum zoom of the any of the icons, should be >= 1.0,
9194
(default: 1.5)
92-
:param max_size: The maximum (x,y) pixel dimensions of any `IconAnimated` bitmap size
93-
that will be created (default: (80,80))
94-
:type max_size: Tuple[int,int]
95+
:param max_icon_size: The maximum (x,y) pixel dimensions of any `IconAnimated` bitmap size
96+
that will be created (default: (80,80)). Note: This is the original pixel size,
97+
before scaling
98+
:type max_icon_size: Tuple[int,int]
9599
:param int max_color_depth: The maximum color depth of any `IconAnimated`
96-
bitmap that will be created (default: 512)
100+
bitmap that will be created (default: 256)
97101
"""
98102
if display is None:
99-
raise ValueError("Must provide display parameter for IconAnimated.")
103+
raise ValueError(
104+
"IconAninmated.init_class: Must provide display parameter for IconAnimated."
105+
)
106+
107+
if (
108+
isinstance(max_icon_size, tuple)
109+
and len(max_icon_size) == 2 # validate max_icon_size input
110+
and isinstance(max_icon_size[0], int)
111+
and isinstance(max_icon_size[1], int)
112+
):
113+
pass
114+
else:
115+
raise ValueError(
116+
"IconAninmated.init_class: max_icon_size must be an (x,y) "
117+
"tuple of integer pixel sizes."
118+
)
119+
100120
cls.display = display
121+
if max_scale < 1.0:
122+
print(
123+
"Warning: IconAnimated.init_class - max_scale value was "
124+
"constrained to minimum of 1.0"
125+
)
101126
cls.max_scale = max(1.0, max_scale)
102127
cls.bitmap_buffer = Bitmap(
103-
round(max_scale * max_size[0]),
104-
round(max_scale * max_size[1]),
128+
round(cls.max_scale * max_icon_size[0]),
129+
round(cls.max_scale * max_icon_size[1]),
105130
max_color_depth + 1,
106131
)
107132
cls.palette_buffer = Palette(max_color_depth + 1)
@@ -111,8 +136,8 @@ def __init__(
111136
label_text,
112137
icon,
113138
on_disk=False,
114-
max_scale=1.4,
115-
max_angle=8,
139+
scale=None,
140+
angle=4,
116141
animation_time=0.15,
117142
**kwargs,
118143
):
@@ -125,14 +150,23 @@ def __init__(
125150
)
126151

127152
super().__init__(label_text, icon, on_disk, **kwargs) # initialize superclasses
153+
print("icon: {}".format(self._icon))
128154

129155
# constrain instance's maximum_scaling between 1.0 and the Class's max_scale
130-
self._max_scale = min(max(1.0, max_scale), self.__class__.max_scale)
131-
if max_scale == 1.0: # no animation
132-
self._animation_time = 0
156+
if scale is None:
157+
self._scale = self.__class__.max_scale
133158
else:
134-
self._animation_time = animation_time # in seconds
135-
self._angle = (max_angle / 360) * 2 * 3.14 # 5 degrees, convert to radians
159+
if scale > self.__class__.max_scale:
160+
print(
161+
"Warning - IconAnimated: max_scale is constrained by value of "
162+
"IconAnimated.max_scale set by IconAnimated.init_class(): {}".format(
163+
self.__class__.max_scale
164+
)
165+
)
166+
self._scale = max(0, min(scale, self.__class__.max_scale))
167+
168+
self._animation_time = animation_time # in seconds
169+
self._angle = (angle / 360) * 2 * pi # in degrees, convert to radians
136170
self._zoomed = False # state variable for zoom status
137171

138172
def zoom_animation(self, touch_point):
@@ -144,14 +178,35 @@ def zoom_animation(self, touch_point):
144178
"""
145179

146180
if self._animation_time > 0:
147-
###
148-
## Update the zoom palette and bitmap buffers and append the tilegrid
149-
###
181+
try:
182+
_image, _palette = adafruit_imageload.load(self._icon)
183+
184+
if len(self.__class__.palette_buffer) < len(_palette) + 1:
185+
self._animation_time = 0 # skip any animation
186+
print(
187+
"Warning: IconAnimated - icon bitmap exceeds IconAnimated.max_color_depth;"
188+
" defaulting to no animation"
189+
)
190+
191+
except NotImplementedError:
192+
self._animation_time = 0 # skip any animation
193+
print(
194+
"Warning: IconAnimated - True color BMP unsupported for animation;"
195+
" defaulting to no animation"
196+
)
197+
198+
if self._animation_time > 0:
150199

151-
_image, _palette = adafruit_imageload.load(self._icon)
152200
animation_bitmap = self.__class__.bitmap_buffer
153201
animation_palette = self.__class__.palette_buffer
154202

203+
# store the current display refresh setting
204+
refresh_status = self.__class__.display.auto_refresh
205+
206+
###
207+
## Update the zoom palette and bitmap buffers and append the tilegrid
208+
###
209+
155210
# copy the image palette, add a transparent color at the end
156211
for i, color in enumerate(_palette):
157212
animation_palette[i] = color
@@ -172,29 +227,37 @@ def zoom_animation(self, touch_point):
172227
)
173228
animation_tilegrid.x = -(animation_bitmap.width - _image.width) // 2
174229
animation_tilegrid.y = -(animation_bitmap.height - _image.height) // 2
230+
231+
self.__class__.display.auto_refresh = False # set auto_refresh off
232+
self[0].hidden = True # hide the original icon
175233
self.append(animation_tilegrid) # add to the self group.
176234

177235
# Animation: zoom larger
178236
start_time = time.monotonic()
237+
179238
while True:
180239
elapsed_time = time.monotonic() - start_time
181240
position = min(
182241
1.0, easein(elapsed_time / self._animation_time)
183242
) # fractional position
243+
animation_bitmap.fill(len(animation_palette) - 1)
184244
bitmaptools.rotozoom(
185245
dest_bitmap=animation_bitmap,
186246
ox=animation_bitmap.width // 2,
187247
oy=animation_bitmap.height // 2,
188248
source_bitmap=_image,
189249
px=_image.width // 2,
190250
py=_image.height // 2,
191-
scale=1.0 # start scaling at 1.0
192-
+ position * (self._max_scale - 1.0),
193-
angle=position * self._angle / 2,
251+
scale=1.0 + position * (self._scale - 1.0), # start scaling at 1.0
252+
angle=position * self._angle,
194253
)
195254
self.__class__.display.refresh()
196255
if elapsed_time > self._animation_time:
197256
break
257+
258+
# set display.auto_refresh back to original value
259+
self.__class__.display.auto_refresh = refresh_status
260+
198261
del _image
199262
del _palette
200263
gc.collect()
@@ -209,11 +272,16 @@ def zoom_out_animation(self, touch_point):
209272
:return: None
210273
"""
211274

212-
_image, _palette = adafruit_imageload.load(self._icon)
213-
animation_bitmap = self.__class__.bitmap_buffer
214-
animation_palette = self.__class__.palette_buffer
215-
216275
if (self._animation_time > 0) and self._zoomed:
276+
_image, _palette = adafruit_imageload.load(self._icon)
277+
animation_bitmap = self.__class__.bitmap_buffer
278+
animation_palette = self.__class__.palette_buffer
279+
280+
# store the current display refresh setting
281+
refresh_status = self.__class__.display.auto_refresh
282+
283+
self.__class__.display.auto_refresh = False # set auto_refresh off
284+
217285
# Animation: shrink down to the original size
218286
start_time = time.monotonic()
219287
while True:
@@ -227,15 +295,21 @@ def zoom_out_animation(self, touch_point):
227295
source_bitmap=_image,
228296
px=_image.width // 2,
229297
py=_image.height // 2,
230-
scale=1.0 + position * (self._max_scale - 1.0),
231-
angle=position * self._angle / 2,
298+
scale=1.0 + position * (self._scale - 1.0),
299+
angle=position * self._angle,
232300
)
233301
self.__class__.display.refresh()
234302
if elapsed_time > self._animation_time:
235303
break
236304

237305
# clean up the zoom display elements
238-
self.pop(-1) # remove self from the group
306+
self[0].hidden = False # unhide the original icon
307+
self.pop(-1) # remove zoom tilegrid from the group
308+
self.__class__.display.refresh()
309+
310+
# set display.auto_refresh back to original value
311+
self.__class__.display.auto_refresh = refresh_status
312+
239313
del _image
240314
del _palette
241315
gc.collect()

adafruit_displayio_layout/widgets/icon_widget.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ class IconWidget(Widget, Control):
5050
:param int anchored_position: (x,y) pixel value for the location of the anchor_point
5151
:type anchored_position: Tuple[int, int]
5252
:param int max_size: (Optional) this will get passed through to the
53-
displayio.Group constructor. If omitted we default to
54-
grid_size width * grid_size height to make room for all (1, 1) sized cells.
53+
displayio.Group constructor. ``max_size`` should be set to the maximum number of
54+
graphical elements that will be held within the Group of this widget.
5555
5656
"""
5757

@@ -77,8 +77,8 @@ def __init__(self, label_text, icon, on_disk=False, **kwargs):
7777
)
7878
self.append(_label)
7979
self.touch_boundary = (
80-
self.x,
81-
self.y,
80+
0,
81+
0,
8282
image.width,
8383
image.height + _label.bounding_box[3],
8484
)

0 commit comments

Comments
 (0)