Skip to content

Commit

Permalink
day 1.5 progress
Browse files Browse the repository at this point in the history
  • Loading branch information
T-Dynamos committed Mar 3, 2024
1 parent a63e966 commit 973ac7a
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 58 deletions.
142 changes: 142 additions & 0 deletions kivymd/uix/carousel/arrangement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import math


class Arrangement:
MEDIUM_ITEM_FLEX_PERCENTAGE = 0.1

def __init__(
self,
priority,
target_small_size,
min_small_size,
max_small_size,
small_count,
target_medium_size,
medium_count,
target_large_size,
large_count,
available_space,
):
self.priority = priority
self.small_size = max(min(target_small_size, max_small_size), min_small_size)
self.small_count = small_count
self.medium_size = target_medium_size
self.medium_count = medium_count
self.large_size = target_large_size
self.large_count = large_count
self.fit(available_space, min_small_size, max_small_size, target_large_size)
self.cost = self.calculate_cost(target_large_size)

def __str__(self):
return (
f"Arrangement [priority={self.priority}, smallCount={self.small_count},"
f" smallSize={self.small_size}, mediumCount={self.medium_count},"
f" mediumSize={self.medium_size}, largeCount={self.large_count},"
f" largeSize={self.large_size}, cost={self.cost}]"
)

def get_space(self):
return (
(self.large_size * self.large_count)
+ (self.medium_size * self.medium_count)
+ (self.small_size * self.small_count)
)

def fit(self, available_space, min_small_size, max_small_size, target_large_size):
delta = available_space - self.get_space()
if self.small_count > 0 and delta > 0:
self.small_size += min(
delta / self.small_count, max_small_size - self.small_size
)
elif self.small_count > 0 and delta < 0:
self.small_size += max(
delta / self.small_count, min_small_size - self.small_size
)
self.small_size = self.small_size if self.small_count > 0 else 0
self.large_size = self.calculate_large_size(
available_space, min_small_size, max_small_size, target_large_size
)
self.medium_size = (self.large_size + self.small_size) / 2
if self.medium_count > 0 and self.large_size != target_large_size:
target_adjustment = (target_large_size - self.large_size) * self.large_count
available_medium_flex = (
self.medium_size * self.MEDIUM_ITEM_FLEX_PERCENTAGE
) * self.medium_count
distribute = min(abs(target_adjustment), available_medium_flex)
if target_adjustment > 0:
self.medium_size -= distribute / self.medium_count
self.large_size += distribute / self.large_count
else:
self.medium_size += distribute / self.medium_count
self.large_size -= distribute / self.large_count

def calculate_large_size(
self, available_space, min_small_size, max_small_size, target_large_size
):
small_size = self.small_size if self.small_count > 0 else 0
return (
available_space
- (
((float(self.small_count)) + (float(self.medium_count)) / 2)
* small_size
)
) / ((float(self.large_count)) + (float(self.medium_count)) / 2)

def is_valid(self):
if self.large_count > 0 and self.small_count > 0 and self.medium_count > 0:
return (
self.large_size > self.medium_size
and self.medium_size > self.small_size
)
elif self.large_count > 0 and self.small_count > 0:
return self.large_size > self.small_size
return True

def calculate_cost(self, target_large_size):
if not self.is_valid():
return float("inf")
return abs(target_large_size - self.large_size) * self.priority

@staticmethod
def find_lowest_cost_arrangement(
available_space,
target_small_size,
min_small_size,
max_small_size,
small_counts,
target_medium_size,
medium_counts,
target_large_size,
large_counts,
):
lowest_cost_arrangement = None
priority = 1

for large_count in large_counts:
for medium_count in medium_counts:
for small_count in small_counts:
arrangement = Arrangement(
priority,
target_small_size,
min_small_size,
max_small_size,
small_count,
target_medium_size,
medium_count,
target_large_size,
large_count,
available_space,
)

if (
lowest_cost_arrangement is None
or arrangement.cost < lowest_cost_arrangement.cost
):
lowest_cost_arrangement = arrangement
if lowest_cost_arrangement.cost == 0:
return lowest_cost_arrangement
priority += 1
return lowest_cost_arrangement

def get_item_count(self):
return self.small_count + self.medium_count + self.large_count
73 changes: 15 additions & 58 deletions kivymd/uix/carousel/carousel.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os

from kivy.properties import ColorProperty, ListProperty
from kivy.properties import ColorProperty, ListProperty, BooleanProperty
from kivy.metrics import dp
from kivy.lang import Builder
from kivy.uix.image import AsyncImage
Expand All @@ -9,6 +9,7 @@
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.behaviors import StencilBehavior
from kivymd.uix.scrollview import MDScrollView
from kivymd.uix.carousel.carousel_strategy import MultiBrowseCarouselStrategy

with open(
os.path.join(uix_path, "carousel", "carousel.kv"), encoding="utf-8"
Expand All @@ -25,12 +26,8 @@ def __init__(self, *arg, **kwargs):

class MDCarousel(MDScrollView):
images = ListProperty([])

# Android default sizes
small_item_min = dp(40)
small_item_max = dp(56)
large_item = dp(120)

is_horizontal = BooleanProperty(True)
alignment = "none"
# Axis
axis = "x"

Expand All @@ -51,71 +48,31 @@ def __init__(self, *arg, **kwargs):
self._child_layout.size = self.size
self._child_layout.spacing = dp(8)
self._child_layout.padding = [dp(16), dp(8)]
self._child_layout.is_horizontal = self.is_horizontal
self._child_layout.alignment = self.alignment
self.add_widget(self._child_layout)

def get_max_items(self):
return max(2, 3 + round((self.width - self.small_item_min) / self.large_item))

def init_images(self, images, start_from=0):
"""Add items to view and set there initial size"""
max_items = self.get_max_items()
size_hint = "size_hint_{}".format(self.axis)

# clear previous ones
self._child_layout.clear_widgets()

# Add required widgets
while len(self._item_widgets) < max_items:
self._item_widgets.append(MDCarouselImageItem(**{size_hint: None}))
# Remove excess if any
while len(self._item_widgets) >= max_items:
self._item_widgets.pop()

distance_covered = 0
w_h = "width" if self.axis == "x" else "height"
_is_small = False
for item, widget in zip(images[start_from:max_items], self._item_widgets):
# set props
widget.source = item["source"]
if widget.parent:
self._child_layout.remove_widget(widget)

# set size
distance_left = getattr(self, w_h) - distance_covered
if distance_left > self.large_item * 2:
setattr(widget, size_hint, None)
setattr(widget, w_h, self.large_item)
distance_covered += getattr(widget, w_h)
elif not _is_small:
setattr(widget, size_hint, 1)
_is_small = True
else:
setattr(widget, size_hint, None)
setattr(widget, w_h, self.small_item_min)

self._child_layout.add_widget(widget)
for image in images:
self._child_layout.add_widget( MDCarouselImageItem(size_hint_x=None, width=dp(100), source = image["source"]) )

clas = MultiBrowseCarouselStrategy().on_first_child_measured_with_margins(
self._child_layout, MDCarouselImageItem(size_hint_x=None, width=dp(100))
)
print(clas)

def on_images(self, instance, images):
self.init_images(images)

def on_size(self, instance, size):
self._child_layout.size = self.size
self.init_images(self.images)

_last_touch_pos = []

def on_touch_down(self, touch):
self._last_touch_pos = list(touch.pos)
super().on_touch_down(touch)

def on_touch_move(self, touch):
super().on_touch_move(touch)

distance = touch.pos[0] - self._last_touch_pos[0]
print(self.scroll_x * self._child_layout.width)
for widget in self._child_layout.children[::-1]:
if widget.width < self.small_item_max:
if self.scroll_x * self._child_layout.width > self.small_item_min:
self._child_layout.remove_widget(widget)
break
widget.width += distance
break
129 changes: 129 additions & 0 deletions kivymd/uix/carousel/carousel_strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import math
from kivy.metrics import dp
from kivy.uix.widget import Widget
from kivymd.uix.carousel.arrangement import Arrangement


class CarouselStrategy:
small_size_min = dp(40)
small_size_max = dp(56)

def on_first_child_measured_with_margins(carousel: Widget, child: Widget):
pass

def get_child_mask_percentage(
masked_size: float, unmasked_size: float, child_margins: float
):
return 1 - ((masked_size - child_margins) / (unmasked_size - child_margins))

@staticmethod
def double_counts(count: list):
doubled_count = list()
for i in range(len(count)):
doubled_count[i] = count[i] * 2
return doubled_count

@staticmethod
def clamp(value, min_val=0, max_val=0):
return min(max(value, min_val), max_val)

def is_contained(self):
return True

def should_refresh_key_line_state(self, carousel: Widget, old_item_count: int):
return False

def set_small_item_size_min(self, min_small_item_size: float):
self.small_size_min = min_small_item_size

def set_small_item_size_max(self, max_small_item_size: float):
self.small_size_max = max_small_item_size


class MultiBrowseCarouselStrategy(CarouselStrategy):
SMALL_COUNTS = [1]
MEDIUM_COUNTS = [1, 0]

def on_first_child_measured_with_margins(self, carousel: Widget, child: Widget):
available_space = carousel.height if carousel.is_horizontal else carousel.width
measured_child_size = child.height if carousel.is_horizontal else child.width
small_child_size_min = self.small_size_min
small_child_size_max = max(self.small_size_max, small_child_size_min)
target_large_child_size = min(measured_child_size, available_space)
target_small_child_size = self.clamp(
measured_child_size / 3, small_child_size_min, small_child_size_max
)
target_medium_child_size = (
target_large_child_size + target_small_child_size
) / 2
small_counts = self.SMALL_COUNTS
if available_space < small_child_size_min * 2:
small_counts = [0]
medium_counts = self.MEDIUM_COUNTS

if carousel.alignment == "center":
small_counts = self.double_counts(small_counts)
medium_counts = self.double_counts(medium_counts)

min_available_large_space = (
available_space
- (target_medium_child_size * max(medium_counts))
- (small_child_size_max * max(small_counts))
)
large_count_min = max(1, min_available_large_space // target_large_child_size)
large_count_max = math.ceil(available_space / target_large_child_size)
large_counts = [
large_count_max - i for i in range(large_count_max - large_count_min + 1)
]
arrangement = Arrangement.find_lowest_cost_arrangement(
available_space,
target_small_child_size,
small_child_size_min,
small_child_size_max,
small_counts,
target_medium_child_size,
medium_counts,
target_large_child_size,
large_counts,
)

keyline_count = arrangement.get_item_count()
print( len(carousel.children) )
if self.ensure_arrangement_fits_item_count(arrangement, len(carousel.children)):
arrangement = Arrangement.find_lowest_cost_arrangement(
available_space,
target_small_child_size,
small_child_size_min,
small_child_size_max,
[arrangement.small_count],
target_medium_child_size,
[arrangement.medium_count],
target_large_child_size,
[arrangement.large_count],
)

return arrangement

def ensure_arrangement_fits_item_count(
self, arrangement: Arrangement, carousel_item_count: int
):
keyline_surplus = arrangement.get_item_count() - carousel_item_count
changed = keyline_surplus > 0 and (
arrangement.small_count > 0 or arrangement.medium_count > 1
)
while keyline_surplus > 0:
if arrangement.small_count > 0:
arrangement.small_count -= 1
elif arrangement.medium_count > 1:
arrangement.medium_count -= 1
keyline_surplus -= 1
return changed

def should_refresh_keyline_state(carousel: Widget, old_item_count: int):
return (
old_item_count < keyline_count
and carousel.get_item_count() >= keyline_count
) or (
old_item_count >= keyline_count
and carousel.get_item_count() < keyline_count
)

0 comments on commit 973ac7a

Please sign in to comment.