Skip to content

Commit

Permalink
uix: add MDCarousel
Browse files Browse the repository at this point in the history
  • Loading branch information
T-Dynamos committed Apr 6, 2024
1 parent 46be1e6 commit 62fe2f0
Show file tree
Hide file tree
Showing 9 changed files with 446 additions and 4 deletions.
6 changes: 3 additions & 3 deletions kivymd/_version.py
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"
2 changes: 2 additions & 0 deletions kivymd/factory_registers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
register("MDSegmentButtonLabel", module="kivymd.uix.segmentedbutton")
register("MDScrollView", module="kivymd.uix.scrollview")
register("MDRecycleView", module="kivymd.uix.recycleview")
register("MDCarousel", module="kivymd.uix.carousel")
register("MDCarouselImageItem", module="kivymd.uix.carousel")
register("MDResponsiveLayout", module="kivymd.uix.responsivelayout")
register("MDSliverAppbar", module="kivymd.uix.sliverappbar")
register("MDSliverAppbarContent", module="kivymd.uix.sliverappbar")
Expand Down
1 change: 1 addition & 0 deletions kivymd/uix/carousel/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .carousel import MDCarousel, MDCarouselImageItem
145 changes: 145 additions & 0 deletions kivymd/uix/carousel/arrangement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
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
22 changes: 22 additions & 0 deletions kivymd/uix/carousel/carousel.kv
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
79 changes: 79 additions & 0 deletions kivymd/uix/carousel/carousel.py
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):
pass


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)
widget.width = 0
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

def on_touch_move(self, touch):
self._strategy.touch_move(self.ids._container, touch)
super().on_touch_move(touch)
Loading

0 comments on commit 62fe2f0

Please sign in to comment.