|
| 1 | +import utime |
| 2 | + |
1 | 3 | from trezor import motor |
2 | 4 | from trezor.lvglui.i18n import gettext as _, keys as i18n_keys |
3 | 5 |
|
|
6 | 8 |
|
7 | 9 | MAX_VISIBLE_VALUE = 175 |
8 | 10 | MIN_VISIBLE_VALUE = 20 |
| 11 | +START_VALUE_TOLERANCE = 5 |
| 12 | +# slider 456px, knob 82px, pad 16px => track ~358px, range 155 units => ~2.3px/unit |
| 13 | +MAX_FORWARD_JUMP = 60 # ~138px/frame |
| 14 | +MIN_PROGRESS_STEPS = 3 # normal slide produces 15-60 events |
| 15 | +MAX_DRAG_DURATION_MS = 3000 # max 3s per drag, blocks slow random touch accumulation |
| 16 | +TOUCH_BOUNDS_MARGIN_HORIZONTAL_PX = 12 |
| 17 | +TOUCH_BOUNDS_MARGIN_VERTICAL_PX = 36 |
9 | 18 |
|
10 | 19 |
|
11 | 20 | class Slider(lv.slider): |
@@ -76,6 +85,18 @@ def __init__(self, parent, text, relative_y=-114) -> None: |
76 | 85 | self.add_event_cb(self.on_event, lv.EVENT.DRAW_PART_BEGIN, None) |
77 | 86 | self.add_event_cb(self.on_event, lv.EVENT.DRAW_PART_END, None) |
78 | 87 |
|
| 88 | + self._drag_active = False |
| 89 | + self._start_value = MIN_VISIBLE_VALUE |
| 90 | + self._last_value = MIN_VISIBLE_VALUE |
| 91 | + self._reached_end = False |
| 92 | + self._progress_count = 0 |
| 93 | + self._invalid_jump = False |
| 94 | + self._passed = False |
| 95 | + self._drag_start_ms = 0 |
| 96 | + self._blocked_until_release = False |
| 97 | + self._touch_point = lv.point_t() |
| 98 | + self._slider_area = lv.area_t() |
| 99 | + |
79 | 100 | def enable(self, enable: bool = True): |
80 | 101 | if enable: |
81 | 102 | self.disable = False |
@@ -120,41 +141,118 @@ def change_knob_style(self, level): |
120 | 141 | lv.PART.KNOB | lv.STATE.DEFAULT, |
121 | 142 | ) |
122 | 143 |
|
| 144 | + def _clamp_value(self, value): |
| 145 | + if value > MAX_VISIBLE_VALUE: |
| 146 | + self.set_value(MAX_VISIBLE_VALUE, lv.ANIM.OFF) |
| 147 | + return MAX_VISIBLE_VALUE |
| 148 | + if value < MIN_VISIBLE_VALUE: |
| 149 | + self.set_value(MIN_VISIBLE_VALUE, lv.ANIM.OFF) |
| 150 | + return MIN_VISIBLE_VALUE |
| 151 | + return value |
| 152 | + |
| 153 | + def _reset_drag_state(self, value): |
| 154 | + self._drag_active = True |
| 155 | + self._start_value = value |
| 156 | + self._last_value = value |
| 157 | + self._reached_end = value >= MAX_VISIBLE_VALUE |
| 158 | + self._progress_count = 0 |
| 159 | + self._invalid_jump = False |
| 160 | + self._passed = False |
| 161 | + self._drag_start_ms = utime.ticks_ms() |
| 162 | + self._blocked_until_release = False |
| 163 | + |
| 164 | + def _can_pass(self, current_value): |
| 165 | + return ( |
| 166 | + self._drag_active |
| 167 | + and self._start_value <= MIN_VISIBLE_VALUE + START_VALUE_TOLERANCE |
| 168 | + and current_value >= MAX_VISIBLE_VALUE |
| 169 | + and self._reached_end |
| 170 | + and self._progress_count >= MIN_PROGRESS_STEPS |
| 171 | + and not self._invalid_jump |
| 172 | + and utime.ticks_diff(utime.ticks_ms(), self._drag_start_ms) |
| 173 | + <= MAX_DRAG_DURATION_MS |
| 174 | + ) |
| 175 | + |
| 176 | + def _abort_drag(self): |
| 177 | + self._drag_active = False |
| 178 | + self._blocked_until_release = True |
| 179 | + self._invalid_jump = True |
| 180 | + self._passed = False |
| 181 | + self.tips.add_flag(lv.obj.FLAG.HIDDEN) |
| 182 | + self.set_value(MIN_VISIBLE_VALUE, lv.ANIM.ON) |
| 183 | + indev = lv.indev_get_act() |
| 184 | + if indev: |
| 185 | + indev.wait_release() |
| 186 | + |
123 | 187 | def on_event(self, event): |
124 | 188 | code = event.code |
125 | | - target = event.get_target() |
126 | | - current_value = target.get_value() |
127 | | - if code == lv.EVENT.PRESSED: |
| 189 | + current_value = event.get_target().get_value() |
| 190 | + if code == lv.EVENT.PRESSING: |
| 191 | + current_value = self._clamp_value(current_value) |
| 192 | + if self._blocked_until_release: |
| 193 | + return |
| 194 | + if not self._drag_active: |
| 195 | + return |
| 196 | + # timeout: reset slider immediately instead of waiting for RELEASED |
| 197 | + if ( |
| 198 | + utime.ticks_diff(utime.ticks_ms(), self._drag_start_ms) |
| 199 | + > MAX_DRAG_DURATION_MS |
| 200 | + ): |
| 201 | + self._abort_drag() |
| 202 | + return |
| 203 | + # check if touch is within slider component bounds (not knob-only hit_test) |
| 204 | + indev = lv.indev_get_act() |
| 205 | + if indev: |
| 206 | + indev.get_point(self._touch_point) |
| 207 | + self.get_coords(self._slider_area) |
| 208 | + if ( |
| 209 | + self._touch_point.x |
| 210 | + < self._slider_area.x1 - TOUCH_BOUNDS_MARGIN_HORIZONTAL_PX |
| 211 | + or self._touch_point.x |
| 212 | + > self._slider_area.x2 + TOUCH_BOUNDS_MARGIN_HORIZONTAL_PX |
| 213 | + or self._touch_point.y |
| 214 | + < self._slider_area.y1 - TOUCH_BOUNDS_MARGIN_VERTICAL_PX |
| 215 | + or self._touch_point.y |
| 216 | + > self._slider_area.y2 + TOUCH_BOUNDS_MARGIN_VERTICAL_PX |
| 217 | + ): |
| 218 | + self._abort_drag() |
| 219 | + return |
| 220 | + delta = current_value - self._last_value |
| 221 | + if delta > 0: |
| 222 | + self._progress_count += 1 |
| 223 | + if delta > MAX_FORWARD_JUMP: |
| 224 | + self._invalid_jump = True |
| 225 | + self._last_value = current_value |
| 226 | + if current_value >= MAX_VISIBLE_VALUE: |
| 227 | + self._reached_end = True |
| 228 | + elif code == lv.EVENT.PRESSED: |
128 | 229 | motor.vibrate(motor.WHISPER) |
129 | | - elif code == lv.EVENT.PRESSING: |
130 | | - if current_value > MAX_VISIBLE_VALUE: |
131 | | - self.set_value(MAX_VISIBLE_VALUE, lv.ANIM.OFF) |
132 | | - elif current_value < MIN_VISIBLE_VALUE: |
133 | | - self.set_value(MIN_VISIBLE_VALUE, lv.ANIM.OFF) |
| 230 | + current_value = self._clamp_value(current_value) |
| 231 | + self._reset_drag_state(current_value) |
134 | 232 | elif code == lv.EVENT.RELEASED: |
135 | | - if current_value < MAX_VISIBLE_VALUE: |
| 233 | + self._blocked_until_release = False |
| 234 | + self._passed = self._can_pass(current_value) |
| 235 | + if self._passed: |
| 236 | + self.tips.clear_flag(lv.obj.FLAG.HIDDEN) |
| 237 | + if self.has_flag(lv.obj.FLAG.CLICKABLE): |
| 238 | + self.clear_flag(lv.obj.FLAG.CLICKABLE) |
| 239 | + motor.vibrate(motor.SUCCESS) |
| 240 | + lv.event_send(self, lv.EVENT.READY, None) |
| 241 | + else: |
136 | 242 | motor.vibrate(motor.ERROR) |
137 | 243 | self.set_value(MIN_VISIBLE_VALUE, lv.ANIM.ON) |
| 244 | + self.tips.add_flag(lv.obj.FLAG.HIDDEN) |
| 245 | + self._drag_active = False |
138 | 246 | elif code == lv.EVENT.DRAW_PART_BEGIN: |
139 | 247 | dsc = lv.obj_draw_part_dsc_t.__cast__(event.get_param()) |
140 | | - if dsc.part == lv.PART.KNOB: |
141 | | - if dsc.id == 0: |
142 | | - if current_value < MAX_VISIBLE_VALUE: |
143 | | - # if self.disable: |
144 | | - # dsc.rect_dsc.bg_img_src = ( |
145 | | - # Slider.SLIDER_DISABLE_ARROW_IMG_SRC |
146 | | - # ) |
147 | | - # else: |
148 | | - dsc.rect_dsc.bg_img_src = self.arrow_img_src |
149 | | - else: |
150 | | - self.tips.clear_flag(lv.obj.FLAG.HIDDEN) |
151 | | - dsc.rect_dsc.bg_img_src = self.done_img_src |
152 | | - if self.has_flag(lv.obj.FLAG.CLICKABLE): |
153 | | - self.clear_flag(lv.obj.FLAG.CLICKABLE) |
154 | | - else: |
155 | | - return |
156 | | - motor.vibrate(motor.SUCCESS) |
157 | | - lv.event_send(self, lv.EVENT.READY, None) |
| 248 | + if dsc.part == lv.PART.KNOB and dsc.id == 0: |
| 249 | + show_done = self._passed or self._can_pass(current_value) |
| 250 | + if show_done: |
| 251 | + self.tips.clear_flag(lv.obj.FLAG.HIDDEN) |
| 252 | + dsc.rect_dsc.bg_img_src = self.done_img_src |
| 253 | + else: |
| 254 | + self.tips.add_flag(lv.obj.FLAG.HIDDEN) |
| 255 | + dsc.rect_dsc.bg_img_src = self.arrow_img_src |
158 | 256 | elif code == lv.EVENT.DRAW_PART_END: |
159 | 257 | dsc = lv.obj_draw_part_dsc_t.__cast__(event.get_param()) |
160 | 258 | if dsc.part == lv.PART.MAIN: |
|
0 commit comments