Replaced static reaction icon in settings with animated lottie.

This commit is contained in:
23rd 2022-04-13 19:20:30 +03:00 committed by John Preston
parent 9380c4bbc3
commit 59fc9d3bfd
7 changed files with 214 additions and 175 deletions

View file

@ -41,138 +41,6 @@ namespace {
constexpr auto kVisibleButtonsCount = 7;
void AddIcon(
not_null<Ui::RpWidget*> parent,
rpl::producer<QPoint> iconPositionValue,
int iconSize,
not_null<Main::Session*> session,
const Data::Reaction &reaction,
rpl::producer<> &&selects,
rpl::producer<> &&destroys,
not_null<rpl::lifetime*> stateLifetime) {
struct State {
struct Entry {
std::shared_ptr<Data::DocumentMedia> media;
std::shared_ptr<Lottie::Icon> icon;
};
Entry appear;
Entry select;
bool appearAnimated = false;
rpl::lifetime loadingLifetime;
base::unique_qptr<Ui::RpWidget> widget;
Ui::Animations::Simple finalAnimation;
};
const auto state = stateLifetime->make_state<State>();
state->widget = base::make_unique_q<Ui::RpWidget>(parent);
state->appear.media = reaction.appearAnimation->createMediaView();
state->select.media = reaction.selectAnimation->createMediaView();
state->appear.media->checkStickerLarge();
state->select.media->checkStickerLarge();
rpl::single() | rpl::then(
session->downloaderTaskFinished()
) | rpl::start_with_next([=] {
const auto check = [&](State::Entry &entry) {
if (!entry.media) {
return true;
} else if (!entry.media->loaded()) {
return false;
}
entry.icon = HistoryView::Reactions::DefaultIconFactory(
entry.media.get(),
iconSize);
entry.media = nullptr;
return true;
};
if (check(state->select) && check(state->appear)) {
state->loadingLifetime.destroy();
}
}, state->loadingLifetime);
const auto widget = state->widget.get();
widget->resize(iconSize, iconSize);
widget->setAttribute(Qt::WA_TransparentForMouseEvents);
std::move(
iconPositionValue
) | rpl::start_with_next([=](const QPoint &point) {
widget->moveToLeft(point.x(), point.y());
}, widget->lifetime());
const auto update = crl::guard(widget, [=] { widget->update(); });
widget->paintRequest(
) | rpl::start_with_next([=,
frameSize = (iconSize / style::DevicePixelRatio())] {
Painter p(widget);
if (state->finalAnimation.animating()) {
const auto progress = 1. - state->finalAnimation.value(0.);
const auto size = widget->size();
const auto scaledSize = size * progress;
const auto scaledCenter = QPoint(
(size.width() - scaledSize.width()) / 2.,
(size.height() - scaledSize.height()) / 2.);
p.setOpacity(progress);
p.translate(scaledCenter);
p.scale(progress, progress);
}
const auto paintFrame = [&](not_null<Lottie::Icon*> animation) {
const auto frame = animation->frame();
p.drawImage(
QRect(
(widget->width() - frameSize) / 2,
(widget->height() - frameSize) / 2,
frameSize,
frameSize),
frame);
};
const auto appear = state->appear.icon.get();
if (appear && !state->appearAnimated) {
state->appearAnimated = true;
appear->animate(update, 0, appear->framesCount() - 1);
}
if (appear && appear->animating()) {
paintFrame(appear);
} else if (const auto select = state->select.icon.get()) {
paintFrame(select);
}
}, widget->lifetime());
std::move(
selects
) | rpl::start_with_next([=] {
const auto select = state->select.icon.get();
if (select && !select->animating()) {
select->animate(update, 0, select->framesCount() - 1);
}
}, widget->lifetime());
std::move(
destroys
) | rpl::take(1) | rpl::start_with_next([=, from = 0., to = 1.] {
state->finalAnimation.start(
[=](float64 value) {
update();
if (value == to) {
stateLifetime->destroy();
}
},
from,
to,
st::defaultPopupMenu.showDuration);
}, widget->lifetime());
widget->raise();
widget->show();
}
PeerId GenerateUser(not_null<History*> history, const QString &name) {
Expects(history->peer->isUser());
const auto peerId = Data::FakePeerIdForJustName(name);
@ -351,7 +219,7 @@ void AddMessage(
}
const auto index = state->icons.flag ? 1 : 0;
state->icons.lifetimes[index] = rpl::lifetime();
AddIcon(
AddReactionLottieIcon(
box->verticalLayout(),
widget->geometryValue(
) | rpl::map([=](const QRect &r) {
@ -375,6 +243,138 @@ void AddMessage(
} // namespace
void AddReactionLottieIcon(
not_null<Ui::RpWidget*> parent,
rpl::producer<QPoint> iconPositionValue,
int iconSize,
not_null<Main::Session*> session,
const Data::Reaction &reaction,
rpl::producer<> &&selects,
rpl::producer<> &&destroys,
not_null<rpl::lifetime*> stateLifetime) {
struct State {
struct Entry {
std::shared_ptr<Data::DocumentMedia> media;
std::shared_ptr<Lottie::Icon> icon;
};
Entry appear;
Entry select;
bool appearAnimated = false;
rpl::lifetime loadingLifetime;
base::unique_qptr<Ui::RpWidget> widget;
Ui::Animations::Simple finalAnimation;
};
const auto state = stateLifetime->make_state<State>();
state->widget = base::make_unique_q<Ui::RpWidget>(parent);
state->appear.media = reaction.appearAnimation->createMediaView();
state->select.media = reaction.selectAnimation->createMediaView();
state->appear.media->checkStickerLarge();
state->select.media->checkStickerLarge();
rpl::single() | rpl::then(
session->downloaderTaskFinished()
) | rpl::start_with_next([=] {
const auto check = [&](State::Entry &entry) {
if (!entry.media) {
return true;
} else if (!entry.media->loaded()) {
return false;
}
entry.icon = HistoryView::Reactions::DefaultIconFactory(
entry.media.get(),
iconSize);
entry.media = nullptr;
return true;
};
if (check(state->select) && check(state->appear)) {
state->loadingLifetime.destroy();
}
}, state->loadingLifetime);
const auto widget = state->widget.get();
widget->resize(iconSize, iconSize);
widget->setAttribute(Qt::WA_TransparentForMouseEvents);
std::move(
iconPositionValue
) | rpl::start_with_next([=](const QPoint &point) {
widget->moveToLeft(point.x(), point.y());
}, widget->lifetime());
const auto update = crl::guard(widget, [=] { widget->update(); });
widget->paintRequest(
) | rpl::start_with_next([=,
frameSize = (iconSize / style::DevicePixelRatio())] {
Painter p(widget);
if (state->finalAnimation.animating()) {
const auto progress = 1. - state->finalAnimation.value(0.);
const auto size = widget->size();
const auto scaledSize = size * progress;
const auto scaledCenter = QPoint(
(size.width() - scaledSize.width()) / 2.,
(size.height() - scaledSize.height()) / 2.);
p.setOpacity(progress);
p.translate(scaledCenter);
p.scale(progress, progress);
}
const auto paintFrame = [&](not_null<Lottie::Icon*> animation) {
const auto frame = animation->frame();
p.drawImage(
QRect(
(widget->width() - frameSize) / 2,
(widget->height() - frameSize) / 2,
frameSize,
frameSize),
frame);
};
const auto appear = state->appear.icon.get();
if (appear && !state->appearAnimated) {
state->appearAnimated = true;
appear->animate(update, 0, appear->framesCount() - 1);
}
if (appear && appear->animating()) {
paintFrame(appear);
} else if (const auto select = state->select.icon.get()) {
paintFrame(select);
}
}, widget->lifetime());
std::move(
selects
) | rpl::start_with_next([=] {
const auto select = state->select.icon.get();
if (select && !select->animating()) {
select->animate(update, 0, select->framesCount() - 1);
}
}, widget->lifetime());
std::move(
destroys
) | rpl::take(1) | rpl::start_with_next([=, from = 0., to = 1.] {
state->finalAnimation.start(
[=](float64 value) {
update();
if (value == to) {
stateLifetime->destroy();
}
},
from,
to,
st::defaultPopupMenu.showDuration);
}, widget->lifetime());
widget->raise();
widget->show();
}
void ReactionsSettingsBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller) {
@ -436,7 +436,7 @@ void ReactionsSettingsBox(
stButton);
const auto iconSize = st::settingsReactionSize;
AddIcon(
AddReactionLottieIcon(
button,
button->sizeValue(
) | rpl::map([=, left = button->st().iconLeft](const QSize &s) {

View file

@ -9,12 +9,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Ui {
class GenericBox;
class RpWidget;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
namespace Main {
class Session;
} // namespace Main
namespace Data {
struct Reaction;
} // namespace Data
void AddReactionLottieIcon(
not_null<Ui::RpWidget*> parent,
rpl::producer<QPoint> iconPositionValue,
int iconSize,
not_null<Main::Session*> session,
const Data::Reaction &reaction,
rpl::producer<> &&selects,
rpl::producer<> &&destroys,
not_null<rpl::lifetime*> stateLifetime);
void ReactionsSettingsBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller);

View file

@ -182,13 +182,11 @@ QImage Reactions::resolveImageFor(
if (set.bottomInfo.isNull() && set.icon) {
resolve(set.bottomInfo, st::reactionInfoImage);
resolve(set.inlineList, st::reactionInlineImage);
resolve(set.settings, st::reactionSettingsImage);
crl::async([icon = std::move(set.icon)]{});
}
switch (size) {
case ImageSize::BottomInfo: return set.bottomInfo;
case ImageSize::InlineList: return set.inlineList;
case ImageSize::Settings: return set.settings;
}
Unexpected("ImageSize in Reactions::resolveImageFor.");
}

View file

@ -54,7 +54,6 @@ public:
enum class ImageSize {
BottomInfo,
InlineList,
Settings,
};
void preloadImageFor(const QString &emoji);
void preloadAnimationsFor(const QString &emoji);
@ -78,7 +77,6 @@ private:
struct ImageSet {
QImage bottomInfo;
QImage inlineList;
QImage settings;
std::shared_ptr<DocumentMedia> media;
std::unique_ptr<Lottie::Icon> icon;
bool fromAppearAnimation = false;

View file

@ -376,6 +376,8 @@ settingsReactionCornerSize: size(28px, 22px);
settingsReactionCornerSkip: point(11px, -6px);
settingsReactionMessageSize: 36px;
settingsReactionRightIcon: 40px;
notifyPreviewMargins: margins(40px, 20px, 40px, 58px);
notifyPreviewUserpicSize: 36px;
notifyPreviewUserpicPosition: point(14px, 11px);

View file

@ -857,48 +857,72 @@ void SetupMessages(
const auto buttonRight = Ui::CreateChild<EmptyButton>(
inner,
st::stickersRemove);
const auto reactRight = Ui::CreateChild<Ui::RpWidget>(inner);
const auto toggleButtonRight = [=](bool value) {
buttonRight->setAttribute(Qt::WA_TransparentForMouseEvents, !value);
};
toggleButtonRight(false);
struct State {
QString lastFavorite;
QImage cache;
struct {
std::vector<rpl::lifetime> lifetimes;
bool flag = false;
} icons;
};
const auto state = reactRight->lifetime().make_state<State>();
const auto updateState = [=] {
auto &reactions = controller->session().data().reactions();
if (state->lastFavorite == reactions.favorite()) {
return;
const auto state = buttonRight->lifetime().make_state<State>();
state->icons.lifetimes = std::vector<rpl::lifetime>(2);
const auto &reactions = controller->session().data().reactions();
auto emojiValue = rpl::single(
reactions.favorite()
) | rpl::then(
reactions.updates() | rpl::map([=] {
return controller->session().data().reactions().favorite();
})
) | rpl::filter([](const QString &emoji) {
return !emoji.isEmpty();
});
rpl::duplicate(
emojiValue
) | rpl::start_with_next([=, emojiValue = std::move(emojiValue)](
const QString &emoji) {
const auto &reactions = controller->session().data().reactions();
for (const auto &r : reactions.list(Data::Reactions::Type::All)) {
if (emoji != r.emoji) {
continue;
}
const auto index = state->icons.flag ? 1 : 0;
state->icons.lifetimes[index] = rpl::lifetime();
const auto iconSize = st::settingsReactionRightIcon;
AddReactionLottieIcon(
inner,
buttonRight->geometryValue(
) | rpl::map([=](const QRect &r) {
return QPoint(
r.left() + (r.width() - iconSize) / 2,
r.top() + (r.height() - iconSize) / 2);
}),
iconSize,
&controller->session(),
r,
buttonRight->events(
) | rpl::filter([=](not_null<QEvent*> event) {
return event->type() == QEvent::Enter;
}) | rpl::to_empty,
rpl::duplicate(emojiValue) | rpl::skip(1) | rpl::to_empty,
&state->icons.lifetimes[index]);
state->icons.flag = !state->icons.flag;
toggleButtonRight(true);
break;
}
state->lastFavorite = reactions.favorite();
state->cache = reactions.resolveImageFor(
reactions.favorite(),
Data::Reactions::ImageSize::Settings);
reactRight->resize(state->cache.size() / style::DevicePixelRatio());
};
updateState();
controller->session().data().reactions().updates(
) | rpl::start_with_next(updateState, reactRight->lifetime());
}, buttonRight->lifetime());
reactRight->setAttribute(Qt::WA_TransparentForMouseEvents);
reactRight->paintRequest(
) | rpl::start_with_next([=] {
Painter p(reactRight);
p.drawImage(0, 0, state->cache);
}, reactRight->lifetime());
rpl::combine(
reactRight->sizeValue(),
react->geometryValue()
) | rpl::start_with_next([=](const QSize &rightSize, const QRect &r) {
reactRight->moveToRight(
react->geometryValue(
) | rpl::start_with_next([=](const QRect &r) {
const auto rightSize = buttonRight->size();
buttonRight->moveToRight(
st::settingsButtonRightSkip,
r.y() + (r.height() - rightSize.height()) / 2);
buttonRight->moveToLeft(
reactRight->x()
+ (rightSize.width() - buttonRight->width()) / 2,
reactRight->y()
+ (rightSize.height() - buttonRight->height()) / 2);
}, reactRight->lifetime());
}, buttonRight->lifetime());
groupQuick->setChangedCallback([=](Quick value) {
Core::App().settings().setChatQuickAction(value);

View file

@ -1007,8 +1007,6 @@ reactionInfoSkip: 3px;
reactionInfoDigitSkip: 6px;
reactionInfoBetween: 3px;
reactionSettingsImage: 40px;
reactionCornerSize: size(36px, 32px);
reactionCornerCenter: point(7px, -9px);
reactionCornerImage: 22px;