So what exactly is a Reveal Widget? Well, in a nutshell, its a kind of layout which has hidden widgets only viewable when you slide/swipe the widget. Complicated? Think of it like this, you have two cards, one on top of another, Initially you only see one card as the other is directly below the other. So to see the other card, you simply swipe the other card out of the way.
Sound Great, Check it out in action
All right, we know what we want, let's get it. Create a file called reveal.py and Add the following code to it
from kivy.app import App from kivy.uix.boxlayout import BoxLayout from kivy.uix.floatlayout import FloatLayout from kivy.uix.label import Label from kivy.uix.button import Button from kivy.animation import Animation from kivy.core.window import Window
And that gives us everything we need to code this badboy. We import the App so we can run our app (duh..), the next two lines give us the layouts we are going to need to make this work, The BoxLayout is the base class for our widget (I almost always start with that) and the FloatLayout is what we are going to use to easily arrange our widgets.
The Label and the Button are simply there to help us test our widget
The Animation module makes it easy for us to create simple subtle animations in kivy so it's a great choice to animate the sliding/swiping of our reveal widget and finally the Window class is imported as we are going to need to know the size of our window in order to correctly implement this widget
Ok, now that that is out of the way, let's dive deeper
In the same file, create a class and call it Reveal and add this code to it
class Reveal(BoxLayout): def __init__(self, **kwargs): super().__init__(**kwargs) self.size_hint_y = .1 self.__wrapper = FloatLayout() self.add_widget(self.__wrapper)
Here we've created our class and added one widget directly to that class: the self.__wrapper widget, which is nothing more than a FloatLayout. This is going to be our wrapper widget, like I said we have to cards so we need to contain both cards somewhere in one place where they'll both fit nicely
You'll notice We also set our widget's default size_hint_y to .1. I do this so that the widget looks good when used later, you are ofcourse free to change it to whatever you want
Moving on, let's add the two cards to our wrapper
class Reveal(BoxLayout): def __init__(self, **kwargs): super().__init__(**kwargs) ... self.__revealTop = RevealTop() self.__revealBottom = RevealBottom() self.__wrapper.add_widget(self.__revealBottom) self.__wrapper.add_widget(self.__revealTop)
Here we simply create two instances of some widgets: self.__revealTop and self.__revealBottom, then we take those widgets and add them to our wrapper.
If you've been in the Kivy ship for a while now you already know that those two widgets( RevealTop() and RevealBottom() don't exist, as such, running this code now will give us an error. Let's fix that.
Still in your reveal.py file. Create another class and call it RevealTop with the following contents
class RevealTop(BoxLayout): def __init__(self, **kwargs): super().__init__(**kwargs) self.__revealed = None self.__open = None self.__moved = None self.add_widget(Label(text="Samuelworks.org"))
Our class has a few variables as you can see. Let's explain:
- self.__revealed - This will be a marker that tells us if the widget has already been swiped, in other words, if the top(initially hidden) part is currently visible
- self.__open - Almost the same as self.__revealed but we use this one differently, you'll understand in a bit
- self.__moved - Al
the Label actually has nothing to do with the widget itself so you can remove it if you want but for illustration purposes I recommend you keep it there to be able to see clearly what this widget does
Ok, Let's get to the fun stuff. To swipe anywhere on your application, you need to bind to certain events, events you then use to calculate your swipes. So in a nutshell, when you click anywhere in your application, the on_touch_down event is fired, we can bind to that. When you move your finger or mouse on your application after clicking - the on_touch_move event is fired, we can also bind to that. Finally when you release your mouse or stop touching the screen in your application - the on_touch_move event is fired, we can likewise bind to that also. Ok let's work with those event now
In your RevealTop class, append the following code
class RevealTop(BoxLayout): def __init__(self, **kwargs): super().__init__(**kwargs) ... def on_touch_down(self, touch): if self.collide_point(touch.x, touch.y): if self.__open: return False touch.grab(self) return True return False
And that is all we need to do when the user clicks/touches our application. We check if where they touched/clicked was in our widget, basically we check if they clicked our widget(they could be clicking anywhere else). If they are indeed clicking our widget, we check if we are currently opening our widget, if that's the case then we simply ignore that touch otherwise we grab that touch, meaning we claim ownership of that touch from other widgets in our application. If the touch or click occured outside our widget then the user is not clicking our widget so again, we ignore the touch/click
Now that we are able to interact with a click/touch on our widget, let's check if the user is swiping, i.e, moving their mouse/finger while touching the widget
class RevealTop(BoxLayout): def __init__(self, **kwargs): super().__init__(**kwargs) ... def on_touch_move(self, touch): if self.collide_point(touch.x, touch.y): if self.__moved: return False if self.__open: self.__open = None else: self.__open = True self.reveal() self.__moved = True
Like I said, when the user swipes, we bind to the on_touch_move event. Here we also check if the user is clicking our widget. If they are we check if we've already registered this move event. (That's the reason we added that self.__moved variable in our constructor). Why do we need that? Because as long as you are still moving that finger/mouse in your application, this event will repeatedly keep firing. We DO NOT want that because it gives us a lot of unexpected behaviors, experiment with it and see. So if we've already registered that move, we want to ignore it. Simple.
The next if/else statement simply toggles between opening and closing our reveal widget, we need this when we actually start the opening/closing process, you'll understand in a sec.
Next we just do the actual revealing/unrevealing of our widget by calling the reveal function to do it for us. Don't fret, we'll create this function later. Finally we set self.__moved to True to accertain that we've indeed recognised this as a swipe and we've used it
Now before, we create the function to handle our actual revealing/unrevealing, let's do some house cleaning to make sure everything is all set for when we want to reveal/unreveal this widget in the future. Just under your on_touch_move function, add this:
class RevealTop(BoxLayout): def __init__(self, **kwargs): super().__init__(**kwargs) ... def on_touch_up(self, touch): if self.collide_point(touch.x, touch.y): touch.ungrab(self) self.__moved = None return True return False
Here, when the user is done interacting with our button, we want to set self.__moved to None, this basically resets that variable so we can use it again to register a swipe the next time the user interacts with our widget.We also release our widget as the owner of this touch since we no longer need it and we also return True to tell the rest of the widgets that we've dealt with this touch so they can ignore it
The last thing left to do on this widget is ofcourse make it actually slidable. So create that reveal function with the following contents
class RevealTop(BoxLayout): def __init__(self, **kwargs): super().__init__(**kwargs) ... def reveal(self): if self.__revealed: pos = 0 anim = Animation(x=pos, duration=0.3) anim.start(self) self.__revealed = False else: pos = -Window.width/2 anim = Animation(x=pos, duration=0.3) anim.start(self) self.__revealed = True
Remember our self.__revealed variable? we need it here. We check if the widget is already revealed, if it is then we set a variable pos to 0. this is what we use as the position to slide our widget to. Next we create a new Animation instance which simply set's the x position of a widget to whatever pos's value is within 0.3seconds. Then we simply call that animation on our widget.
If the widget is currently not revealed then obviously we want to reveal it so we set the pos variable to the negative value of the current viewport's width/2 since we dont want to completly remove the widget. Then we repeat the same thing we did above.
So to try to better explain, When we call this function, If the widget is closed(not revealed), we want to continuously change it's x coordinates until it gets to half the size of the window which effectly partly pushes it off screen. Since we have a widget under it, we can now see that widget as our widget no longer covers the whole widget. If the widget is already revealed however, we want to shift it's x coordinates to zero which will then bring it back fully on screen thereby hiding the widget underneath
We know we also had a RevealBottom class right, let's do that one next. In your reveal.py file, create the RevealBottom class with the following contents
... class RevealBottom(BoxLayout): def __init__(self, **kwargs): super().__init__(**kwargs) self.add_child(Button(text="Copy")) self.add_child(Button(text="Paste")) def add_child(self, child): self.ids.parent_bottom.add_widget(child)
We create the class which has a function add_child which takes a widget and adds it to a layout with an id of parent_bottom. We created this function because we need a way to access this parent_bottom layout outside our widget as it would be tedious to have to use ids and all that everytime we need to add a widget. In our constructor we also add two buttons, Agai, completely unrelated to the widget we are creating so you can remove those buttons but they are great for demonstartion purposes
We are referrencing a layout with an id of parent_bottom but currently there is no such layout, let's fix that
Create a file and name it reveal.kv and then add the following content to that file
< RevealBottom >: id: main_win canvas.before: Color: rgba: (1,1,1,1) Rectangle: pos: self.pos size: self.size AnchorLayout: anchor_x: "right" anchor_y: "top" BoxLayout: id: parent_bottom size_hint_x: .5
See that BoxLayout inside the AnchorLayout? Whenever we use add_child instead of add_widget, we will add the widget to it instead of adding the widget directly to our RevealBottom
Alright, let's also change the backgrounds of our widgets, so we can see things clearly
Still in your reveal.kv file, append the following content
< Reveal >: id: main_win canvas.before: Color: rgb: (1,.6,1) Rectangle: pos: self.pos size: self.size < RevealTop >: id: main_win canvas.before: Color: rgba: (0,0,.5,1) Rectangle: pos: self.pos size: self.size
And that does it for our Reveal Widget, let's wrap things up with a simple test. In your reveal.py file, append the following to build our widgets.
class RevealApp(App): def build(self): return Reveal()
Finally, let's go ahead and run this
if __name__ == '__main__': RevealApp().run()
And there you have it, click and drag the widget to the left and watch it slide. Pretty cool huh...Now would be a good time to let you know that this isn't perfect but it's a pretty quick implementation. For example, this widget uses a FloatLayout which has absolute positioning so, for example, if you add more than one of these widgets to a layout, chances are, one of them will be hidden below the other widget since the widgets don't follow the standard protocols for wiidget packing. Again, thoughts are very much appreciated especially towards fixing the problem above, As I type this I'm thinking maybe adding a RelativeLayout to wrap the FloatLayout might work? will try that next