From ba3862e70f2cbb7b5b84497749ddf0b6dd3a781e Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 10 Oct 2020 21:27:01 +0300 Subject: [PATCH] Added new send recorded voice button with recording animation. --- .../icons/send_control_record_active.png | Bin 0 -> 869 bytes .../icons/send_control_record_active@2x.png | Bin 0 -> 1396 bytes .../icons/send_control_record_active@3x.png | Bin 0 -> 1877 bytes .../history_view_voice_record_bar.cpp | 221 ++++++++++++++++-- .../controls/history_view_voice_record_bar.h | 3 + Telegram/SourceFiles/ui/chat/chat.style | 8 +- 6 files changed, 213 insertions(+), 19 deletions(-) create mode 100644 Telegram/Resources/icons/send_control_record_active.png create mode 100644 Telegram/Resources/icons/send_control_record_active@2x.png create mode 100644 Telegram/Resources/icons/send_control_record_active@3x.png diff --git a/Telegram/Resources/icons/send_control_record_active.png b/Telegram/Resources/icons/send_control_record_active.png new file mode 100644 index 0000000000000000000000000000000000000000..af937a2014591dd16c3b53a729f7c251298a8c6f GIT binary patch literal 869 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 parent, + rpl::producer<> leaveWindowEventProducer); + + void requestPaintColor(float64 progress); + void requestPaintProgress(float64 progress); + void requestPaintLevel(quint16 level); + void reset(); + + [[nodiscard]] rpl::producer actives() const; + + [[nodiscard]] bool inCircle(const QPoint &localPos) const; + +private: + void init(); + + void drawProgress(Painter &p); + + const int _height; + const int _center; + + rpl::variable _showProgress = 0.; + rpl::variable _colorProgress = 0.; + rpl::variable _inCircle = false; + + bool recordingAnimationCallback(crl::time now); + + // This can animate for a very long time (like in music playing), + // so it should be a Basic, not a Simple animation. + Ui::Animations::Basic _recordingAnimation; + anim::value _recordingLevel; + + rpl::lifetime _showingLifetime; +}; + class RecordLock final : public Ui::RpWidget { public: RecordLock(not_null parent); @@ -79,6 +116,149 @@ private: rpl::variable _progress = 0.; }; +RecordLevel::RecordLevel( + not_null parent, + rpl::producer<> leaveWindowEventProducer) +: AbstractButton(parent) +, _height(st::historyRecordLevelMaxRadius * 2) +, _center(_height / 2) +, _recordingAnimation([=](crl::time now) { + return recordingAnimationCallback(now); +}) { + resize(_height, _height); + std::move( + leaveWindowEventProducer + ) | rpl::start_with_next([=] { + _inCircle = false; + }, lifetime()); + init(); +} + +void RecordLevel::requestPaintLevel(quint16 level) { + _recordingLevel.start(level); + _recordingAnimation.start(); +} + +bool RecordLevel::recordingAnimationCallback(crl::time now) { + const auto dt = anim::Disabled() + ? 1. + : ((now - _recordingAnimation.started()) + / float64(kRecordingUpdateDelta)); + if (dt >= 1.) { + _recordingLevel.finish(); + } else { + _recordingLevel.update(dt, anim::sineInOut); + } + if (!anim::Disabled()) { + update(); + } + return (dt < 1.); +} + +void RecordLevel::init() { + shownValue( + ) | rpl::start_with_next([=](bool shown) { + }, lifetime()); + + paintRequest( + ) | rpl::start_with_next([=](const QRect &clip) { + Painter p(this); + + drawProgress(p); + + st::historyRecordVoiceActive.paintInCenter(p, rect()); + }, lifetime()); + + _showProgress.changes( + ) | rpl::map([](auto value) { + return value != 0.; + }) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](bool show) { + setVisible(show); + setMouseTracking(show); + if (!show) { + _recordingLevel = anim::value(); + _recordingAnimation.stop(); + _showingLifetime.destroy(); + } + }, lifetime()); + + actives( + ) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](bool active) { + setPointerCursor(active); + }, lifetime()); +} + +rpl::producer RecordLevel::actives() const { + return events( + ) | rpl::filter([=](not_null e) { + return (e->type() == QEvent::MouseMove + || e->type() == QEvent::Leave + || e->type() == QEvent::Enter); + }) | rpl::map([=](not_null e) { + switch(e->type()) { + case QEvent::MouseMove: + return inCircle((static_cast(e.get()))->pos()); + case QEvent::Leave: return false; + case QEvent::Enter: return inCircle(mapFromGlobal(QCursor::pos())); + default: return false; + } + }); +} + +bool RecordLevel::inCircle(const QPoint &localPos) const { + const auto &radii = st::historyRecordLevelMaxRadius; + const auto dx = std::abs(localPos.x() - _center); + if (dx > radii) { + return false; + } + const auto dy = std::abs(localPos.y() - _center); + if (dy > radii) { + return false; + } else if (dx + dy <= radii) { + return true; + } + return ((dx * dx + dy * dy) <= (radii * radii)); +} + +void RecordLevel::drawProgress(Painter &p) { + PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + const auto color = anim::color( + st::historyRecordSignalColor, + st::historyRecordVoiceFgActive, + _colorProgress.current()); + p.setBrush(color); + + const auto progress = _showProgress.current(); + + const auto center = QPoint(_center, _center); + const int mainRadii = progress * st::historyRecordLevelMainRadius; + + { + p.setOpacity(.5); + const auto min = progress * st::historyRecordLevelMinRadius; + const auto max = progress * st::historyRecordLevelMaxRadius; + const auto delta = std::min(_recordingLevel.current() / 0x4000, 1.); + const auto radii = qRound(min + (delta * (max - min))); + p.drawEllipse(center, radii, radii); + p.setOpacity(1.); + } + + p.drawEllipse(center, mainRadii, mainRadii); +} + +void RecordLevel::requestPaintProgress(float64 progress) { + _showProgress = progress; + update(); +} + +void RecordLevel::requestPaintColor(float64 progress) { + _colorProgress = progress; + update(); +} + RecordLock::RecordLock(not_null parent) : RpWidget(parent) { resize( st::historyRecordLockTopShadow.width(), @@ -222,6 +402,9 @@ VoiceRecordBar::VoiceRecordBar( , _controller(controller) , _send(send) , _lock(std::make_unique(parent)) +, _level(std::make_unique( + parent, + _controller->widget()->leaveEvents())) , _cancelFont(st::historyRecordFont) , _recordingAnimation([=](crl::time now) { return recordingAnimationCallback(now); @@ -264,6 +447,11 @@ void VoiceRecordBar::updateLockGeometry() { _lock->moveToRight(right, _lock->y()); } +void VoiceRecordBar::updateLevelGeometry() { + const auto center = (_send->width() - _level->width()) / 2; + _level->moveToRight(st::historySendRight + center, y() + center); +} + void VoiceRecordBar::init() { hide(); // Keep VoiceRecordBar behind SendButton. @@ -275,6 +463,7 @@ void VoiceRecordBar::init() { }) | rpl::to_empty ) | rpl::start_with_next([=] { stackUnder(_send.get()); + _level->raise(); }, lifetime()); sizeValue( @@ -298,6 +487,7 @@ void VoiceRecordBar::init() { } updateMessageGeometry(); updateLockGeometry(); + updateLevelGeometry(); }, lifetime()); paintRequest( @@ -347,27 +537,17 @@ void VoiceRecordBar::init() { ) | rpl::start_with_next([=] { installClickOutsideFilter(); - _send->clicks( - ) | rpl::filter([=] { - return _send->type() == Ui::SendButton::Type::Record; - }) | rpl::start_with_next([=] { + _level->clicks( + ) | rpl::start_with_next([=] { stop(true); }, _recordingLifetime); - auto hover = _send->events( - ) | rpl::filter([=](not_null e) { - return e->type() == QEvent::Enter - || e->type() == QEvent::Leave; - }) | rpl::map([=](not_null e) { - return (e->type() == QEvent::Enter); - }); - _send->setLockRecord(true); _send->setForceRippled(true); rpl::single( false ) | rpl::then( - std::move(hover) + _level->actives() ) | rpl::start_with_next([=](bool enter) { _inField = enter; }, _recordingLifetime); @@ -398,7 +578,7 @@ void VoiceRecordBar::activeAnimate(bool active) { } else { auto callback = [=] { update(_messageRect); - _send->requestPaintRecord(activeAnimationRatio()); + _level->requestPaintColor(activeAnimationRatio()); }; const auto from = active ? 0. : 1.; _activeAnimation.start(std::move(callback), from, to, duration); @@ -410,6 +590,7 @@ void VoiceRecordBar::visibilityAnimate(bool show, Fn &&callback) { const auto from = show ? 0. : 1.; const auto duration = st::historyRecordVoiceShowDuration; auto animationCallback = [=, callback = std::move(callback)](auto value) { + _level->requestPaintProgress(value); update(); if ((show && value == 1.) || (!show && value == 0.)) { if (callback) { @@ -429,6 +610,7 @@ void VoiceRecordBar::setLockBottom(rpl::producer &&bottom) { bottom ) | rpl::start_with_next([=](int value) { _lock->moveToLeft(_lock->x(), value - _lock->height()); + updateLevelGeometry(); }, lifetime()); } @@ -474,8 +656,12 @@ void VoiceRecordBar::startRecording() { const auto type = e->type(); if (type == QEvent::MouseMove) { const auto mouse = static_cast(e.get()); - const auto localPos = mapFromGlobal(mouse->globalPos()); - _inField = rect().contains(localPos); + const auto globalPos = mouse->globalPos(); + const auto localPos = mapFromGlobal(globalPos); + const auto inField = rect().contains(localPos); + _inField = inField + ? inField + : _level->inCircle(_level->mapFromGlobal(globalPos)); if (_showLockAnimation.animating()) { return; @@ -504,6 +690,7 @@ bool VoiceRecordBar::recordingAnimationCallback(crl::time now) { } void VoiceRecordBar::recordUpdated(quint16 level, int samples) { + _level->requestPaintLevel(level); _recordingLevel.start(level); _recordingAnimation.start(); _recordingSamples = samples; @@ -698,7 +885,7 @@ void VoiceRecordBar::installClickOutsideFilter() { } else if (type == QEvent::ContextMenu || type == QEvent::Shortcut) { return Type::ShowBox; } else if (type == QEvent::MouseButtonPress) { - return (noBox && !_send->underMouse()) + return (noBox && !_inField.current()) ? Type::ShowBox : Type::Continue; } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h index dfe60f1112..79f08ac60f 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.h @@ -22,6 +22,7 @@ class SessionController; namespace HistoryView::Controls { +class RecordLevel; class RecordLock; class VoiceRecordBar final : public Ui::RpWidget { @@ -56,6 +57,7 @@ private: void updateMessageGeometry(); void updateLockGeometry(); + void updateLevelGeometry(); void recordError(); void recordUpdated(quint16 level, int samples); @@ -86,6 +88,7 @@ private: const not_null _controller; const std::shared_ptr _send; const std::unique_ptr _lock; + const std::unique_ptr _level; rpl::event_stream _sendActionUpdates; rpl::event_stream _sendVoiceRequests; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 0ea46b9eb4..743831fb11 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -332,8 +332,8 @@ historyRecordVoiceShowDuration: 120; historyRecordVoiceDuration: 120; historyRecordVoice: icon {{ "send_control_record", historyRecordVoiceFg }}; historyRecordVoiceOver: icon {{ "send_control_record", historyRecordVoiceFgOver }}; -historyRecordVoiceActive: icon {{ "send_control_record", historyRecordVoiceFgActive }}; -historyRecordVoiceCancel: icon {{ "send_control_record", attentionButtonFg }}; +historyRecordVoiceActive: icon {{ "send_control_record_active", recordActiveIcon }}; +historyRecordVoiceCancel: icon {{ "send_control_record_active", attentionButtonFg }}; historyRecordVoiceRippleBgActive: lightButtonBgOver; historyRecordVoiceRippleBgCancel: attentionButtonBgRipple; historyRecordSignalColor: attentionButtonFg; @@ -345,6 +345,10 @@ historyRecordFont: font(13px); historyRecordDurationSkip: 12px; historyRecordDurationFg: historyComposeAreaFg; +historyRecordLevelMainRadius: 37px; +historyRecordLevelMinRadius: 38px; +historyRecordLevelMaxRadius: 60px; + historyRecordTextStyle: TextStyle(defaultTextStyle) { font: historyRecordFont; }