from kivy.app import App from kivy.lang import Builder from kivy.clock import Clock from kivy.properties import ListProperty from kivy.animation import Animation from kivy.metrics import dp KV = ''' #:import RGBA kivy.utils.rgba : size_hint: None, None size: self.texture_size canvas.before: PushMatrix Scale: origin: self.center x: .75 if self.state == 'down' else 1 y: .75 if self.state == 'down' else 1 canvas.after: PopMatrix BoxLayout: orientation: 'vertical' padding: dp(5), dp(5) RecycleView: id: rv data: app.messages viewclass: 'Message' do_scroll_x: False RecycleBoxLayout: id: box orientation: 'vertical' size_hint_y: None size: self.minimum_size default_size_hint: 1, None # magic value for the default height of the message default_size: 0, 38 key_size: '_size' FloatLayout: size_hint_y: None height: 0 Button: size_hint_y: None height: self.texture_size[1] opacity: 0 if not self.height else 1 text: ( 'go to last message' if rv.height < box.height and rv.scroll_y > 0 else '' ) pos_hint: {'pos': (0, 0)} on_release: app.scroll_bottom() BoxLayout: size_hint: 1, None size: self.minimum_size TextInput: id: ti size_hint: 1, None height: min(max(self.line_height, self.minimum_height), 150) multiline: False on_text_validate: app.send_message(self) ImageButton: source: 'data/logo/kivy-icon-48.png' on_release: app.send_message(ti) : message_id: -1 bg_color: '#223344' side: 'left' text: '' size_hint_y: None _size: 0, 0 size: self._size text_size: None, None opacity: min(1, self._size[0]) Label: text: root.text padding: 10, 10 size_hint: None, 1 size: self.texture_size text_size: root.text_size on_texture_size: app.update_message_size( root.message_id, self.texture_size, root.width, ) pos_hint: ( {'x': 0, 'center_y': .5} if root.side == 'left' else {'right': 1, 'center_y': .5} ) canvas.before: Color: rgba: RGBA(root.bg_color) RoundedRectangle: size: self.texture_size radius: dp(5), dp(5), dp(5), dp(5) pos: self.pos canvas.after: Color: Line: rounded_rectangle: self.pos + self.texture_size + [dp(5)] width: 1.01 ''' class MessengerApp(App): messages = ListProperty() def build(self): return Builder.load_string(KV) def add_message(self, text, side, color): # create a message for the recycleview self.messages.append({ 'message_id': len(self.messages), 'text': text, 'side': side, 'bg_color': color, 'text_size': [None, None], }) def update_message_size(self, message_id, texture_size, max_width): # when the label is updated, we want to make sure the displayed size is # proper if max_width == 0: return one_line = dp(50) # a bit of hack, YMMV # if the texture is too big, limit its size if texture_size[0] >= max_width * 2 / 3: self.messages[message_id] = { **self.messages[message_id], 'text_size': (max_width * 2 / 3, None), } # if it was limited, but is now too small to be limited, raise the limit elif texture_size[0] < max_width * 2 / 3 and \ texture_size[1] > one_line: self.messages[message_id] = { **self.messages[message_id], 'text_size': (max_width * 2 / 3, None), '_size': texture_size, } # just set the size else: self.messages[message_id] = { **self.messages[message_id], '_size': texture_size, } @staticmethod def focus_textinput(textinput): textinput.focus = True def send_message(self, textinput): text = textinput.text textinput.text = '' self.add_message(text, 'right', '#223344') self.focus_textinput(textinput) Clock.schedule_once(lambda *args: self.answer(text), 1) self.scroll_bottom() def answer(self, text, *args): self.add_message('do you really think so?', 'left', '#332211') def scroll_bottom(self): rv = self.root.ids.rv box = self.root.ids.box if rv.height < box.height: Animation.cancel_all(rv, 'scroll_y') Animation(scroll_y=0, t='out_quad', d=.5).start(rv) if __name__ == '__main__': MessengerApp().run()