-
-
Notifications
You must be signed in to change notification settings - Fork 674
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
286 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |