Repository: kivy/kivy

Issue:
Carousel with two slides loops poorly.
When a Carousel has only two slides and looping set to True, then scrolling left from index 0 or right from index 1 doesn't animate/render as it should. Instead the prev/next slide is re-rendered.



Discussion:
My initial thought for fixing this was to change the _prev_slide and _next_slide to return the appropriate widget when loop is True (https://github.com/Ian-Foote/kivy/commit/bc1197d9c657772895de61ec4873027e0cb961a8). Unfortunately, this causes an exception:

`Exception kivy.uix.widget.WidgetException: WidgetException('Cannot add <kivy.uix.relativelayout.RelativeLayout object at 0xab8223c>, it already has a parent <__main__.Carousel object at 0xa73db6c>',) in 'kivy.properties.observable_list_dispatch' ignored`

this is because in the case of only 2 slides, the previous and next slides are the same widget, and we cannot add it twice.


Resolving patch:
diff --git a/kivy/uix/carousel.py b/kivy/uix/carousel.py
--- a/kivy/uix/carousel.py
+++ b/kivy/uix/carousel.py
@@ -262,26 +262,63 @@ def _insert_visible_slides(self):
             super(Carousel, self).add_widget(self._current)
 
     def _position_visible_slides(self, *args):
-        if self.direction in ['right', 'left']:
-            xoff = self.x + self._offset
-            x_prev = {'left': xoff + self.width, 'right': xoff - self.width}
-            x_next = {'left': xoff - self.width, 'right': xoff + self.width}
-            if self._prev:
-                self._prev.pos = (x_prev[self.direction], self.y)
-            if self._current:
-                self._current.pos = (xoff, self.y)
-            if self._next:
-                self._next.pos = (x_next[self.direction], self.y)
-        if self.direction in ['top', 'bottom']:
-            yoff = self.y + self._offset
-            y_prev = {'top': yoff - self.height, 'bottom': yoff + self.height}
-            y_next = {'top': yoff + self.height, 'bottom': yoff - self.height}
-            if self._prev:
-                self._prev.pos = (self.x, y_prev[self.direction])
-            if self._current:
-                self._current.pos = (self.x, yoff)
-            if self._next:
-                self._next.pos = (self.x, y_next[self.direction])
+        slides, index = self.slides, self.index
+        no_of_slides = len(slides) - 1
+        if not slides:
+            return
+        x, y, width, height = self.x, self.y, self.width, self.height
+        _offset, direction = self._offset, self.direction
+        _prev, _next, _current = self._prev, self._next, self._current
+        get_slide_container = self.get_slide_container
+        last_slide = get_slide_container(slides[-1])
+        first_slide = get_slide_container(slides[0])
+        skip_next = False
+
+        if direction in ['right', 'left']:
+            xoff = x + _offset
+            x_prev = {'left': xoff + width, 'right': xoff - width}
+            x_next = {'left': xoff - width, 'right': xoff + width}
+            if _prev:
+                _prev.pos = (x_prev[direction], y)
+            elif _next and index == 0:
+                # if first slide is moving to right with direction set to right
+                # or toward left with direction set to left
+                if ((_offset > 0 and direction == 'right') or
+                    (_offset < 0 and direction == 'left')):
+                    # put last_slide before first slide
+                    last_slide.pos = (x_prev[direction], y)
+                    skip_next = True
+            if _current:
+                _current.pos = (xoff, y)
+            if skip_next:
+                return
+            if _next:
+                _next.pos = (x_next[direction], y)
+            elif _prev and index == no_of_slides:
+                if ((_offset < 0 and direction == 'right') or
+                    (_offset > 0 and direction == 'left')):
+                    first_slide.pos = (x_next[direction], y)
+        if direction in ['top', 'bottom']:
+            yoff = y + _offset
+            y_prev = {'top': yoff - height, 'bottom': yoff + height}
+            y_next = {'top': yoff + height, 'bottom': yoff - height}
+            if _prev:
+                _prev.pos = (x, y_prev[direction])
+            elif _next and index == 0:
+                if ((_offset > 0 and direction == 'top') or
+                    (_offset < 0 and direction == 'bottom')):
+                    last_slide.pos = (x, y_prev[direction])
+                    skip_next = True
+            if _current:
+                _current.pos = (x, yoff)
+            if skip_next:
+                return
+            if _next:
+                _next.pos = (x, y_next[direction])
+            elif _prev and index == no_of_slides:
+                if ((_offset < 0 and direction == 'top') or
+                    (_offset > 0 and direction == 'bottom')):
+                    first_slide.pos = (x, y_next[direction])
 
     def on_size(self, *args):
         for slide in self.slides_container:
@@ -304,7 +341,7 @@ def on_slides(self, *args):
 
     def on__offset(self, *args):
         self._trigger_position_visible_slides()
-        #if reached full offset, switche index to next or prev
+        # if reached full offset, switch index to next or prev
         if self.direction == 'right':
             if self._offset <= -self.width:
                 self.index = self.index + 1
@@ -343,9 +380,11 @@ def _start_animation(self, *args):
         if new_offset == 0:
             dur = self.anim_cancel_duration
 
-        if not self.loop:  # detect edge cases if not looping
+        # detect edge cases if not looping
+        len_slides = len(self.slides)
+        if not self.loop or len_slides == 1:
             is_first = (self.index == 0)
-            is_last = (self.index == len(self.slides) - 1)
+            is_last = (self.index == len_slides - 1)
             if self.direction in ['right', 'top']:
                 towards_prev = (new_offset > 0)
                 towards_next = (new_offset < 0)
@@ -483,8 +522,9 @@ def clear_widgets(self):
     class Example1(App):
 
         def build(self):
-            carousel = Carousel(direction='right')
-            for i in range(10):
+            carousel = Carousel(direction='left',
+                                loop=True)
+            for i in range(2):
                 src = "http://placehold.it/480x270.png&text=slide-%d&.png" % i
                 image = Factory.AsyncImage(source=src, allow_stretch=True)
                 carousel.add_widget(image)


Test patch: