-
-
Notifications
You must be signed in to change notification settings - Fork 683
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
9 changed files
with
424 additions
and
4 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 |
---|---|---|
@@ -1,5 +1,5 @@ | ||
release = False | ||
__version__ = "2.0.1.dev0" | ||
__hash__ = "f7bde69707ac708a758a02d89f14997ee468d1ee" | ||
__short_hash__ = "f7bde69" | ||
__date__ = "2024-02-27" | ||
__hash__ = "072e6fd15a32c2ce8c9cdaa2a9f59c202b841b9e" | ||
__short_hash__ = "072e6fd" | ||
__date__ = "2024-03-17" |
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 @@ | ||
from .carousel import MDCarousel, MDCarouselImageItem |
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,144 @@ | ||
import math | ||
|
||
# Made in reference with | ||
# ~/material-components-android/lib/java/com/google/android/material/carousel/Arrangement.java | ||
|
||
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}, small_count={self.small_count}," | ||
f" small_size={self.small_size}, medium_count={self.medium_count}," | ||
f" medium_size={self.medium_size}, large_count={self.large_count}," | ||
f" large_size={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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<MDCarouselImageItem>: | ||
text:"" | ||
canvas: | ||
Color: | ||
rgba:[1,1,1,0.5] | ||
RoundedRectangle: | ||
size:self.size | ||
pos:self.pos | ||
radius:[dp(25)] * 4 | ||
MDLabel: | ||
text:root.text | ||
halign:"center" | ||
|
||
<MDCarousel>: | ||
MDBoxLayout: | ||
id:_container | ||
is_horizontal:root.is_horizontal | ||
alignment:root.alignment | ||
spacing:dp(8) | ||
padding:[0, dp(8)] | ||
size_hint:1,1 | ||
adaptive_width:True |
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,79 @@ | ||
import os | ||
from functools import partial | ||
|
||
from kivy.clock import Clock | ||
from kivy.lang import Builder | ||
from kivy.metrics import dp | ||
from kivy.properties import ( | ||
BooleanProperty, | ||
StringProperty, | ||
OptionProperty, | ||
NumericProperty, | ||
ListProperty, | ||
DictProperty, | ||
) | ||
from kivy.factory import Factory | ||
from kivy.uix.image import AsyncImage | ||
from kivy.uix.boxlayout import BoxLayout | ||
from kivy.uix.stencilview import StencilView | ||
|
||
from kivymd import uix_path | ||
from kivymd.uix.boxlayout import MDBoxLayout | ||
from kivymd.uix.carousel.carousel_strategy import AvaliableStrategies | ||
|
||
with open( | ||
os.path.join(uix_path, "carousel", "carousel.kv"), | ||
encoding="utf-8", | ||
) as kv_file: | ||
Builder.load_string(kv_file.read()) | ||
|
||
|
||
class MDCarouselImageItem(BoxLayout): | ||
def __init__(self, *arg, **kwargs): | ||
super().__init__(*arg, **kwargs) | ||
|
||
|
||
class MDCarousel(MDBoxLayout, StencilView): | ||
strategy = OptionProperty( | ||
"MultiBrowseCarouselStrategy", options=[AvaliableStrategies.avaliable] | ||
) | ||
is_horizontal = BooleanProperty(True) | ||
alignment = StringProperty("default") | ||
desired_item_size = NumericProperty(100) | ||
|
||
data = ListProperty([]) | ||
viewclass = StringProperty("MDCarouselImageItem") | ||
|
||
_strategy = None | ||
_variable_item_size = dp(50) | ||
|
||
def __init__(self, *arg, **kwargs): | ||
super().__init__(*arg, **kwargs) | ||
self.bind(data=self.update_strategy) | ||
self.bind(strategy=self.update_strategy) | ||
self.bind(size=self.update_strategy) | ||
|
||
def on_data(self, instance, data): | ||
for widget_data in data: | ||
widget = Factory.get( | ||
self.viewclass | ||
if "viewclass" not in widget_data.keys() | ||
else widget_data["viewclass"] | ||
)(size_hint_x=None) | ||
for key, value in widget_data.items(): | ||
setattr(widget, key, value) | ||
self.ids._container.add_widget(widget) | ||
|
||
def update_strategy(self, *args): | ||
|
||
if self.width <= 0: | ||
Clock.schedule_once(self.update_strategy) | ||
return | ||
|
||
if self._strategy.__class__.__name__ != self.strategy: | ||
self._strategy = AvaliableStrategies.get( | ||
self.strategy, len(self.data) | ||
) | ||
self._strategy.arrange(self.alignment, self.width, self.desired_item_size) | ||
Clock.schedule_once(partial(self._strategy.set_init_size, self.ids._container)) | ||
return self._strategy |
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,131 @@ | ||
import math | ||
from functools import partial | ||
|
||
from kivy.metrics import dp | ||
from kivy.uix.widget import Widget | ||
from kivy.clock import Clock | ||
|
||
from kivymd.uix.carousel.arrangement import Arrangement | ||
|
||
|
||
class CarouselStrategy: | ||
spacing = dp(8) | ||
|
||
small_size_min = dp(40) | ||
small_size_max = dp(56) | ||
|
||
item_len = 0 | ||
arrangement = None | ||
|
||
def __init__(self, item_len): | ||
self.item_len = item_len | ||
|
||
def arrange( | ||
self, | ||
alignment: str, | ||
available_space: int, | ||
measured_child_size: int, | ||
item_len: int, | ||
) -> Arrangement: | ||
"""Build arrangement based on size""" | ||
|
||
def update_init_widgets(self, container): | ||
... | ||
|
||
def set_init_size(self, container, *args) -> None: | ||
"""Set size of visible widgets initially""" | ||
|
||
@staticmethod | ||
def double_counts(count: list): | ||
doubled_count = [] | ||
for i in count: | ||
doubled_count.append(i * 2) | ||
return doubled_count | ||
|
||
@staticmethod | ||
def clamp(value, min_val=0, max_val=0): | ||
return min(max(value, min_val), max_val) | ||
|
||
|
||
class MultiBrowseCarouselStrategy(CarouselStrategy): | ||
small_counts = [1] | ||
medium_counts = [1, 0] | ||
|
||
def arrange( | ||
self, | ||
alignment: str, | ||
available_space: int, | ||
measured_child_size: int, | ||
) -> Arrangement: | ||
# append default padding | ||
measured_child_size += self.spacing | ||
|
||
small_child_size_min = self.small_size_min + self.spacing | ||
small_child_size_max = max( | ||
self.small_size_max + self.spacing, 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 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(int(large_count_max - large_count_min + 1)) | ||
] | ||
self.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, | ||
) | ||
|
||
def set_init_size(self, container, *args): | ||
if len(container.children) < self.arrangement.get_item_count(): | ||
# Reset the size and then retry | ||
for widget in container.children: | ||
widget.width = self.arrangement.large_size - dp(30) | ||
Clock.schedule_once(partial(self.set_init_size, container)) | ||
return | ||
|
||
item_index = 0 | ||
for type_item in ["large", "medium", "small"]: | ||
for _ in range(getattr(self.arrangement, "{}_count".format(type_item))): | ||
widget = container.children[::-1][item_index] | ||
widget.width = getattr( | ||
self.arrangement, "{}_size".format(type_item) | ||
) - dp(8) | ||
item_index += 1 | ||
|
||
|
||
class AvaliableStrategies: | ||
avaliable = ["MultiBrowseCarouselStrategy"] | ||
|
||
@staticmethod | ||
def get(strategy_name, item_len): | ||
return { | ||
"MultiBrowseCarouselStrategy": MultiBrowseCarouselStrategy, | ||
}[strategy_name](item_len) |
Oops, something went wrong.