Adds incoming bubble animations
This commit is contained in:
@@ -51,5 +51,6 @@ executable('kordophone',
|
||||
resources,
|
||||
dependencies : dependencies,
|
||||
vala_args: ['--pkg', 'posix'],
|
||||
link_args: ['-lm'],
|
||||
install : true
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
using GLib;
|
||||
using Gee;
|
||||
|
||||
public class Message : Object
|
||||
public class Message : Object, Comparable<Message>, Hashable<Message>
|
||||
{
|
||||
public string guid { get; set; default = ""; }
|
||||
public string text { get; set; default = ""; }
|
||||
@@ -9,6 +10,8 @@ public class Message : Object
|
||||
|
||||
public Attachment[] attachments { get; set; default = {}; }
|
||||
|
||||
public bool should_animate = false;
|
||||
|
||||
public bool from_me {
|
||||
get {
|
||||
// Hm, this may have been accidental.
|
||||
@@ -52,4 +55,20 @@ public class Message : Object
|
||||
|
||||
this.attachments = attachments.to_array();
|
||||
}
|
||||
|
||||
public int compare_to(Message other) {
|
||||
if (guid == other.guid) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public bool equal_to(Message other) {
|
||||
return guid == other.guid;
|
||||
}
|
||||
|
||||
public uint hash() {
|
||||
return guid.hash();
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ private abstract class BubbleLayout : Object, ChatItemLayout
|
||||
{
|
||||
public bool from_me { get; set; }
|
||||
public float vertical_padding { get; set; }
|
||||
public string id { get; set; }
|
||||
|
||||
protected float max_width;
|
||||
protected Widget parent;
|
||||
|
||||
@@ -4,6 +4,7 @@ interface ChatItemLayout : Object
|
||||
{
|
||||
public abstract bool from_me { get; set; }
|
||||
public abstract float vertical_padding { get; set; }
|
||||
public abstract string id { get; set; }
|
||||
|
||||
public abstract float get_height();
|
||||
public abstract float get_width();
|
||||
|
||||
@@ -3,6 +3,7 @@ using Gtk;
|
||||
class DateItemLayout : Object, ChatItemLayout {
|
||||
public bool from_me { get; set; }
|
||||
public float vertical_padding { get; set; }
|
||||
public string id { get; set; }
|
||||
|
||||
private Pango.Layout layout;
|
||||
private float max_width;
|
||||
@@ -14,7 +15,7 @@ class DateItemLayout : Object, ChatItemLayout {
|
||||
layout.set_font_description(Pango.FontDescription.from_string("Sans 9"));
|
||||
layout.set_alignment(Pango.Alignment.CENTER);
|
||||
}
|
||||
|
||||
|
||||
public float get_height() {
|
||||
Pango.Rectangle ink_rect, logical_rect;
|
||||
layout.get_pixel_extents(out ink_rect, out logical_rect);
|
||||
|
||||
@@ -6,6 +6,7 @@ private class SenderAnnotationLayout : Object, ChatItemLayout
|
||||
public float max_width;
|
||||
public bool from_me { get; set; default = false; }
|
||||
public float vertical_padding { get; set; default = 0.0f; }
|
||||
public string id { get; set; }
|
||||
|
||||
private Pango.Layout layout;
|
||||
private BubbleLayoutConstants constants;
|
||||
|
||||
@@ -63,7 +63,12 @@ public class MessageListModel : Object, ListModel
|
||||
}
|
||||
|
||||
public void load_messages() {
|
||||
var previous_messages = new HashSet<Message>();
|
||||
previous_messages.add_all(_messages);
|
||||
|
||||
try {
|
||||
bool first_load = _messages.size == 0;
|
||||
|
||||
Message[] messages = Repository.get_instance().get_messages(conversation_guid);
|
||||
|
||||
// Clear existing set
|
||||
@@ -81,9 +86,14 @@ public class MessageListModel : Object, ListModel
|
||||
|
||||
for (int i = 0; i < messages.length; i++) {
|
||||
var message = messages[i];
|
||||
_messages.add(message);
|
||||
participants.add(message.sender);
|
||||
|
||||
if (!first_load && !previous_messages.contains(message)) {
|
||||
// This is a new message according to the UI, schedule an animation for it.
|
||||
message.should_animate = true;
|
||||
}
|
||||
|
||||
_messages.add(message);
|
||||
position++;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Gtk;
|
||||
using Gee;
|
||||
using Gdk;
|
||||
|
||||
private class TranscriptDrawingArea : Widget
|
||||
{
|
||||
@@ -22,6 +23,8 @@ private class TranscriptDrawingArea : Widget
|
||||
private const float bubble_margin = 18.0f;
|
||||
|
||||
private const bool debug_viewport = false;
|
||||
private uint? _tick_callback_id = null;
|
||||
private HashMap<string, ChatItemAnimation> _animations = new HashMap<string, ChatItemAnimation>();
|
||||
|
||||
public TranscriptDrawingArea() {
|
||||
add_css_class("transcript-drawing-area");
|
||||
@@ -63,7 +66,7 @@ private class TranscriptDrawingArea : Widget
|
||||
recompute_message_layouts();
|
||||
}
|
||||
|
||||
public override void snapshot(Snapshot snapshot) {
|
||||
public override void snapshot(Gtk.Snapshot snapshot) {
|
||||
const int viewport_y_padding = 50;
|
||||
var viewport_rect = Graphene.Rect() {
|
||||
origin = Graphene.Point() { x = 0, y = ((int)viewport.value - viewport_y_padding) },
|
||||
@@ -104,9 +107,22 @@ private class TranscriptDrawingArea : Widget
|
||||
};
|
||||
|
||||
// Skip drawing if this item is not in the viewport
|
||||
float height_offset = 0.0f;
|
||||
if (viewport_rect.intersection(rect, null)) {
|
||||
snapshot.save();
|
||||
|
||||
var pushed_opacity = false;
|
||||
if (_animations.has_key(chat_item.id)) {
|
||||
var animation = _animations[chat_item.id];
|
||||
|
||||
var item_height_offset = (float) (-item_height * (1.0 - animation.progress));
|
||||
snapshot.translate(Graphene.Point() { x = 0, y = item_height_offset });
|
||||
height_offset = item_height_offset;
|
||||
|
||||
snapshot.push_opacity(animation.progress);
|
||||
pushed_opacity = true;
|
||||
}
|
||||
|
||||
// Translate to the correct position
|
||||
snapshot.translate(origin);
|
||||
|
||||
@@ -117,17 +133,64 @@ private class TranscriptDrawingArea : Widget
|
||||
snapshot.translate(Graphene.Point() { x = 0, y = -item_height });
|
||||
|
||||
chat_item.draw(snapshot);
|
||||
|
||||
if (pushed_opacity) {
|
||||
snapshot.pop();
|
||||
}
|
||||
|
||||
snapshot.restore();
|
||||
}
|
||||
|
||||
y_offset += item_height + chat_item.vertical_padding;
|
||||
y_offset += item_height + chat_item.vertical_padding + height_offset;
|
||||
}
|
||||
|
||||
animation_tick();
|
||||
}
|
||||
|
||||
private bool animation_tick() {
|
||||
HashSet<string> animations_to_remove = new HashSet<string>();
|
||||
_animations.foreach(entry => {
|
||||
var animation = entry.value;
|
||||
animation.tick_animation();
|
||||
|
||||
if ((animation.progress - 1.0).abs() < 0.000001) {
|
||||
animations_to_remove.add(entry.key);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
foreach (var key in animations_to_remove) {
|
||||
_animations.unset(key);
|
||||
}
|
||||
|
||||
if (_animations.size > 0) {
|
||||
queue_draw();
|
||||
}
|
||||
|
||||
return _animations.size > 0;
|
||||
}
|
||||
|
||||
private bool on_frame_clock_tick(Gtk.Widget widget, Gdk.FrameClock frame_clock) {
|
||||
return animation_tick();
|
||||
}
|
||||
|
||||
private void start_animation(string id) {
|
||||
if (_animations.has_key(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var animation = new ChatItemAnimation(get_frame_clock());
|
||||
animation.start_animation(0.18f);
|
||||
_animations.set(id, animation);
|
||||
|
||||
_tick_callback_id = add_tick_callback(on_frame_clock_tick);
|
||||
}
|
||||
|
||||
private void recompute_message_layouts() {
|
||||
var container_width = get_width();
|
||||
float max_width = container_width * 0.90f;
|
||||
|
||||
|
||||
DateTime? last_date = null;
|
||||
string? last_sender = null;
|
||||
ArrayList<ChatItemLayout> items = new ArrayList<ChatItemLayout>();
|
||||
@@ -147,8 +210,14 @@ private class TranscriptDrawingArea : Widget
|
||||
}
|
||||
|
||||
// Text Bubble
|
||||
var animate = message.should_animate;
|
||||
if (message.text.length > 0 && !message.is_attachment_marker) {
|
||||
var text_bubble = new TextBubbleLayout(message, this, max_width);
|
||||
text_bubble.id = @"text-$(message.guid)";
|
||||
if (animate) {
|
||||
start_animation(text_bubble.id);
|
||||
}
|
||||
|
||||
text_bubble.vertical_padding = (last_sender == message.sender) ? 4.0f : 10.0f;
|
||||
items.add(text_bubble);
|
||||
}
|
||||
@@ -164,6 +233,11 @@ private class TranscriptDrawingArea : Widget
|
||||
}
|
||||
|
||||
var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size);
|
||||
image_layout.id = @"image-$(attachment.guid)";
|
||||
if (animate) {
|
||||
start_animation(image_layout.id);
|
||||
}
|
||||
|
||||
image_layout.is_downloaded = attachment.preview_downloaded;
|
||||
items.add(image_layout);
|
||||
|
||||
@@ -191,3 +265,30 @@ private class TranscriptDrawingArea : Widget
|
||||
queue_resize();
|
||||
}
|
||||
}
|
||||
|
||||
private class ChatItemAnimation
|
||||
{
|
||||
public double progress = 0.0;
|
||||
private int64 start_time = 0;
|
||||
private double duration = 0.0;
|
||||
private Gdk.FrameClock frame_clock = null;
|
||||
|
||||
public ChatItemAnimation(Gdk.FrameClock frame_clock) {
|
||||
this.frame_clock = frame_clock;
|
||||
progress = 0.0f;
|
||||
}
|
||||
|
||||
public void start_animation(double duration) {
|
||||
this.duration = duration;
|
||||
this.progress = 0.0;
|
||||
this.start_time = frame_clock.get_frame_time();
|
||||
}
|
||||
|
||||
public void tick_animation() {
|
||||
progress = ease_out_quart((frame_clock.get_frame_time() - start_time) / (duration * 1000000.0));
|
||||
}
|
||||
|
||||
private static double ease_out_quart(double t) {
|
||||
return 1.0 - Math.pow(1.0 - t, 4);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user