Use lottie for voice/round message icon.

This commit is contained in:
John Preston
2026-01-19 18:13:04 +04:00
committed by 23rd
parent f40064333e
commit 6dce4e360f
11 changed files with 168 additions and 53 deletions
@@ -17,6 +17,8 @@
<file alias="stats_earn.tgs">../../animations/stats_earn.tgs</file>
<file alias="voice_ttl_idle.tgs">../../animations/voice_ttl_idle.tgs</file>
<file alias="voice_ttl_start.tgs">../../animations/voice_ttl_start.tgs</file>
<file alias="chat/voice_to_video.tgs">../../animations/chat/voice_to_video.tgs</file>
<file alias="chat/video_to_voice.tgs">../../animations/chat/video_to_voice.tgs</file>
<file alias="palette.tgs">../../animations/palette.tgs</file>
<file alias="sleep.tgs">../../animations/sleep.tgs</file>
<file alias="greeting.tgs">../../animations/greeting.tgs</file>
@@ -153,10 +153,7 @@ SendButton {
sendIconFillPadding: pixels;
inner: IconButton;
stars: RoundButton;
record: icon;
recordOver: icon;
round: icon;
roundOver: icon;
recordSize: size;
sendDisabledFg: color;
}
@@ -1287,18 +1284,11 @@ historyRecordVoiceFgOver: historyComposeIconFgOver;
historyRecordVoiceFgInactive: attentionButtonFg;
historyRecordVoiceFgActive: windowBgActive;
historyRecordVoiceFgActiveIcon: windowFgActive;
historyRecordVoice: icon {{ "chat/input_record", historyRecordVoiceFg }};
historyRecordVoiceOver: icon {{ "chat/input_record", historyRecordVoiceFgOver }};
historyRecordVoiceOnceBg: icon {{ "voice_lock/audio_once_bg", historySendIconFg }};
historyRecordVoiceOnceBgOver: icon {{ "voice_lock/audio_once_bg", historySendIconFgOver }};
historyRecordVoiceOnceFg: icon {{ "voice_lock/audio_once_number", windowFgActive }};
historyRecordVoiceOnceFgOver: icon {{ "voice_lock/audio_once_number", windowFgActive }};
historyRecordVoiceOnceInactive: icon {{ "chat/audio_once", windowSubTextFg }};
historyRecordVoiceActive: icon {{ "chat/input_record_filled", historyRecordVoiceFgActiveIcon }};
historyRecordRound: icon {{ "chat/input_video", historyRecordVoiceFg }};
historyRecordRoundOver: icon {{ "chat/input_video", historyRecordVoiceFgOver }};
historyRecordRoundActive: icon {{ "chat/input_video", historyRecordVoiceFgActiveIcon }};
historyRecordRoundIconPosition: point(0px, 0px);
historyRecordSendIconPosition: point(2px, 0px);
historyRecordVoiceRippleBgActive: lightButtonBgOver;
historyRecordSignalRadius: 5px;
@@ -1411,12 +1401,10 @@ historySend: SendButton {
textTop: 5px;
width: -8px;
}
record: historyRecordVoice;
recordOver: historyRecordVoiceOver;
round: historyRecordRound;
roundOver: historyRecordRoundOver;
recordSize: size(26px, 26px);
sendDisabledFg: historyComposeIconFg;
}
historyRecordFrameIndex: 30;
defaultComposeFilesMenu: IconButton(defaultIconButton) {
width: 48px;
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/controls/history_view_voice_record_button.h"
#include "lottie/lottie_icon.h"
#include "ui/paint/blobs.h"
#include "ui/painter.h"
#include "styles/style_chat.h"
@@ -24,6 +25,8 @@ constexpr auto kBlobAlpha = 76. / 255.;
constexpr auto kBlobMaxSpeed = 5.0;
constexpr auto kLevelDuration = 100. + 500. * 0.33;
constexpr auto kBlobsScaleEnterDuration = crl::time(250);
constexpr auto kVoiceIconIndex = 0;
constexpr auto kRoundIconIndex = 1;
auto Blobs() {
return std::vector<Ui::Paint::Blobs::BlobData>{
@@ -135,22 +138,28 @@ void VoiceRecordButton::init() {
p.scale(scale, scale);
}
const auto state = *currentState;
const auto icon = (state == Type::Send)
? st::historySendIcon
: (state == Type::Record)
? st::historyRecordVoiceActive
: st::historyRecordRoundActive;
const auto position = (state == Type::Send)
? st::historyRecordSendIconPosition
: (state == Type::Record)
? QPoint(0, 0)
: st::historyRecordRoundIconPosition;
icon.paint(
p,
-icon.width() / 2 + position.x(),
-icon.height() / 2 + position.y(),
0,
st::historyRecordVoiceFgActiveIcon->c);
if (state == Type::Send) {
const auto icon = st::historySendIcon;
const auto position = st::historyRecordSendIconPosition;
icon.paint(
p,
-icon.width() / 2 + position.x(),
-icon.height() / 2 + position.y(),
0,
st::historyRecordVoiceFgActiveIcon->c);
} else {
const auto index = (state == Type::Record)
? kVoiceIconIndex
: kRoundIconIndex;
auto &icon = _voiceRoundIcons[index];
if (!icon) {
initVoiceRoundIcon(index);
}
icon->paintInCenter(
p,
rect().translated(-_center, -_center),
st::historyRecordVoiceFgActiveIcon->c);
}
}
}, lifetime());
@@ -205,6 +214,18 @@ void VoiceRecordButton::init() {
}, lifetime());
}
void VoiceRecordButton::initVoiceRoundIcon(int index) {
Expects(index >= 0 && index < 2);
_voiceRoundIcons[index] = Lottie::MakeIcon({
.path = ((index == kVoiceIconIndex)
? u":/animations/chat/voice_to_video.tgs"_q
: u":/animations/chat/video_to_voice.tgs"_q),
.sizeOverride = st::historySend.recordSize,
.colorizeUsingAlpha = true,
});
}
rpl::producer<bool> VoiceRecordButton::actives() const {
return events(
) | rpl::filter([=](not_null<QEvent*> e) {
@@ -15,6 +15,10 @@ namespace style {
struct RecordBar;
} // namespace style
namespace Lottie {
class Icon;
} // namespace Lottie
namespace Ui::Paint {
class Blobs;
} // namespace Ui::Paint
@@ -47,8 +51,10 @@ public:
private:
void init();
void initVoiceRoundIcon(int index);
std::unique_ptr<Ui::Paint::Blobs> _blobs;
std::array<std::unique_ptr<Lottie::Icon>, 2> _voiceRoundIcons;
crl::time _lastUpdateTime = 0;
crl::time _blobsHideLastTime = 0;
@@ -493,10 +493,6 @@ storiesLike: IconButton(storiesAttach) {
icon: icon {{ "chat/input_like", storiesComposeGrayIcon }};
iconOver: icon {{ "chat/input_like", storiesComposeGrayIcon }};
}
storiesRecordVoice: icon {{ "chat/input_record", storiesComposeGrayIcon }};
storiesRecordVoiceOver: icon {{ "chat/input_record", storiesComposeGrayIcon }};
storiesRecordRound: icon {{ "chat/input_video", storiesComposeGrayIcon }};
storiesRecordRoundOver: icon {{ "chat/input_video", storiesComposeGrayIcon }};
storiesEditStars: IconButton(storiesAttach) {
icon: icon{{ "chat/input_paid", storiesComposeGrayIcon }};
iconOver: icon{{ "chat/input_paid", storiesComposeGrayIcon }};
@@ -727,10 +723,6 @@ storiesComposeControls: ComposeControls(defaultComposeControls) {
rippleAreaSize: 40px;
rippleAreaPosition: point(1px, 1px);
}
record: storiesRecordVoice;
recordOver: storiesRecordVoiceOver;
round: storiesRecordRound;
roundOver: storiesRecordRoundOver;
sendDisabledFg: storiesComposeGrayText;
}
attach: storiesAttach;
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/controls/send_button.h"
#include "lang/lang_tag.h"
#include "lottie/lottie_icon.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
@@ -19,7 +20,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Ui {
namespace {
constexpr int kWideScale = 5;
constexpr auto kWideScale = 5;
constexpr auto kVoiceToRoundIndex = 0;
constexpr auto kRoundToVoiceIndex = 1;
} // namespace
@@ -29,19 +32,34 @@ SendButton::SendButton(QWidget *parent, const style::SendButton &st)
updateSize();
}
SendButton::~SendButton() = default;
void SendButton::setState(State state) {
if (_state == state) {
return;
}
const auto previousType = _state.type;
const auto newType = state.type;
const auto voiceRoundTransition = isVoiceRoundTransition(
previousType,
newType);
const auto hasSlowmode = (_state.slowmodeDelay > 0);
const auto hasSlowmodeChanged = hasSlowmode != (state.slowmodeDelay > 0);
auto withSameSlowmode = state;
withSameSlowmode.slowmodeDelay = _state.slowmodeDelay;
const auto animate = hasSlowmodeChanged
|| (!hasSlowmode && withSameSlowmode != _state);
if (animate) {
if (animate && !voiceRoundTransition) {
_contentFrom = grabContent();
}
if (_voiceRoundAnimating && !voiceRoundTransition) {
_voiceRoundAnimating = false;
}
if (_state.slowmodeDelay != state.slowmodeDelay) {
const auto seconds = state.slowmodeDelay;
const auto minutes = seconds / 60;
@@ -60,7 +78,29 @@ void SendButton::setState(State state) {
kMarkupTextOptions);
}
_state = state;
if (animate) {
if (voiceRoundTransition) {
_voiceRoundAnimating = true;
const auto toRound = (newType == Type::Round);
const auto index = toRound ? kVoiceToRoundIndex : kRoundToVoiceIndex;
auto &icon = _voiceRoundIcons[index];
if (!icon) {
initVoiceRoundIcon(index);
}
icon->animate([=, raw = icon.get()] {
update();
if (!raw->animating()) {
_voiceRoundAnimating = false;
}
}, 0, icon->framesCount() - 1);
auto &after = _voiceRoundIcons[1 - index];
if (!after) {
initVoiceRoundIcon(1 - index);
} else if (after->frameIndex() != 0) {
after->jumpTo(0, nullptr);
}
} else if (animate) {
_stateChangeFromWidth = width();
_stateChangeAnimation.stop();
updateSize();
@@ -86,6 +126,12 @@ void SendButton::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto over = (isDown() || isOver());
if (_voiceRoundAnimating) {
paintVoiceRoundIcon(p, over);
return;
}
auto changed = _stateChangeAnimation.value(1.);
if (changed < 1.) {
PainterHighQualityEnabler hq(p);
@@ -153,11 +199,7 @@ void SendButton::paintRecord(QPainter &p, bool over) {
(width() - _st.inner.rippleAreaSize) / 2,
_st.inner.rippleAreaPosition.y());
}
const auto &icon = (isDisabled() || !over)
? _st.record
: _st.recordOver;
icon.paintInCenter(p, rect());
paintLottieIcon(p, kVoiceToRoundIndex, over);
}
void SendButton::paintRound(QPainter &p, bool over) {
@@ -167,11 +209,20 @@ void SendButton::paintRound(QPainter &p, bool over) {
(width() - _st.inner.rippleAreaSize) / 2,
_st.inner.rippleAreaPosition.y());
}
paintLottieIcon(p, kRoundToVoiceIndex, over);
}
const auto &icon = (isDisabled() || !over)
? _st.round
: _st.roundOver;
icon.paintInCenter(p, rect());
void SendButton::paintLottieIcon(QPainter &p, int index, bool over) {
auto &icon = _voiceRoundIcons[index];
if (!icon) {
initVoiceRoundIcon(index);
} else if (!_voiceRoundAnimating && icon->frameIndex() != 0) {
icon->jumpTo(0, [=] { update(); });
}
const auto color = (isDisabled() || !over)
? st::historyRecordVoiceFg->c
: st::historyRecordVoiceFgOver->c;
icon->paintInCenter(p, rect(), color);
}
void SendButton::paintSave(QPainter &p, bool over) {
@@ -330,6 +381,39 @@ QPoint SendButton::prepareRippleStartPosition() const {
return real - QPoint((width() - size) / 2, y);
}
void SendButton::initVoiceRoundIcon(int index) {
Expects(index >= 0 && index < 2);
_voiceRoundIcons[index] = Lottie::MakeIcon({
.path = ((index == kVoiceToRoundIndex)
? u":/animations/chat/voice_to_video.tgs"_q
: u":/animations/chat/video_to_voice.tgs"_q),
.sizeOverride = _st.recordSize,
.colorizeUsingAlpha = true,
});
}
void SendButton::paintVoiceRoundIcon(QPainter &p, bool over) {
if (!isDisabled()) {
paintRipple(
p,
(width() - _st.inner.rippleAreaSize) / 2,
_st.inner.rippleAreaPosition.y());
}
const auto color = (isDisabled() || !over)
? st::historyRecordVoiceFg->c
: st::historyRecordVoiceFgOver->c;
const auto toVideo = (_state.type == Type::Round);
const auto index = toVideo ? kVoiceToRoundIndex : kRoundToVoiceIndex;
_voiceRoundIcons[index]->paintInCenter(p, rect(), color);
}
bool SendButton::isVoiceRoundTransition(Type from, Type to) {
return (from == Type::Record && to == Type::Round)
|| (from == Type::Round && to == Type::Record);
}
SendStarButton::SendStarButton(
QWidget *parent,
const style::IconButton &st,
@@ -9,6 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/buttons.h"
#include <memory>
namespace Lottie {
class Icon;
} // namespace Lottie
namespace style {
struct SendButton;
struct IconButton;
@@ -20,6 +26,7 @@ namespace Ui {
class SendButton final : public RippleButton {
public:
SendButton(QWidget *parent, const style::SendButton &st);
~SendButton();
static constexpr auto kSlowmodeDelayLimit = 100 * 60;
@@ -76,6 +83,11 @@ private:
void paintSlowmode(QPainter &p);
void paintStarsToSend(QPainter &p, bool over);
void initVoiceRoundIcon(int index);
void paintVoiceRoundIcon(QPainter &p, bool over);
[[nodiscard]] static bool isVoiceRoundTransition(Type from, Type to);
void paintLottieIcon(QPainter &p, int index, bool over);
const style::SendButton &_st;
State _state;
@@ -87,6 +99,9 @@ private:
QString _slowmodeDelayText;
Ui::Text::String _starsToSendText;
std::array<std::unique_ptr<Lottie::Icon>, 2> _voiceRoundIcons;
bool _voiceRoundAnimating = false;
};
struct SendStarButtonState {
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "dialogs/dialogs_three_state_icon.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "platform/platform_window_title.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
@@ -565,7 +566,13 @@ void Generator::paintComposeArea() {
: st::historyAttach.iconPosition.y();
st::historyAttach.icon[_palette].paint(*_p, _composeArea.x() + attachIconLeft, controlsTop + attachIconTop, _rect.width());
auto right = st::historySendRight + st::historySendSize.width();
st::historyRecordVoice[_palette].paintInCenter(*_p, QRect(_composeArea.x() + _composeArea.width() - right, controlsTop, st::historySendSize.width(), st::historySendSize.height()));
const auto recordIcon = Lottie::MakeIcon({
.path = u":/animations/chat/voice_to_video.tgs"_q,
.sizeOverride = st::historySend.recordSize,
.colorizeUsingAlpha = true,
});
recordIcon->paintInCenter(*_p, QRect(_composeArea.x() + _composeArea.width() - right, controlsTop, st::historySendSize.width(), st::historySendSize.height()), st::historyRecordVoiceFg[_palette]->c);
const auto &emojiButton = st::historyAttachEmoji.inner;
const auto emojiIconLeft = (emojiButton.iconPosition.x() < 0)