From 7f583f86c0d4c2945e68e8413937df5c4d988712 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 27 Jun 2023 14:49:04 +0400 Subject: [PATCH] Nice expand / collapse animations. --- .../SourceFiles/dialogs/dialogs_widget.cpp | 12 +- .../dialogs/ui/dialogs_stories_list.cpp | 132 ++++++++++++++---- .../dialogs/ui/dialogs_stories_list.h | 11 +- 3 files changed, 125 insertions(+), 30 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index de20a5fac2..03679cfb8b 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -1137,7 +1137,16 @@ void Widget::scrollToDefault(bool verytop) { startScrollUpButtonAnimation(false); const auto scroll = [=] { - _scroll->scrollToY(qRound(_scrollToAnimation.value(scrollTo))); + const auto animated = qRound(_scrollToAnimation.value(scrollTo)); + const auto animatedDelta = animated - scrollTo; + const auto realDelta = _scroll->scrollTop() - scrollTo; + if (realDelta * animatedDelta < 0) { + // We scrolled manually to the other side of target 'scrollTo'. + _scrollToAnimation.stop(); + } else if (std::abs(realDelta) > std::abs(animatedDelta)) { + // We scroll by animation only if it gets us closer to target. + _scroll->scrollToY(animated); + } }; _scrollToAnimation.start( @@ -2019,7 +2028,6 @@ void Widget::dropEvent(QDropEvent *e) { void Widget::listScrollUpdated() { const auto scrollTop = _scroll->scrollTop(); - PROFILE_LOG(("SCROLLED: %1").arg(scrollTop)); _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); updateScrollUpVisibility(); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index b8d9c5a5b3..f1490669f3 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -18,8 +18,12 @@ namespace Dialogs::Stories { namespace { constexpr auto kSmallThumbsShown = 3; -constexpr auto kSummaryExpandLeft = 1.5; +constexpr auto kSummaryExpandLeft = 1; constexpr auto kPreloadPages = 2; +constexpr auto kExpandAfterRatio = 0.85; +constexpr auto kCollapseAfterRatio = 0.72; +constexpr auto kFrictionRatio = 0.15; +constexpr auto kSnapExpandedTimeout = crl::time(200); [[nodiscard]] int AvailableNameWidth(const style::DialogsStoriesList &st) { const auto &full = st.full; @@ -33,6 +37,7 @@ constexpr auto kPreloadPages = 2; struct List::Layout { int itemsCount = 0; int shownHeight = 0; + float64 expandedRatio = 0.; float64 ratio = 0.; float64 thumbnailLeft = 0.; float64 photoLeft = 0.; @@ -56,7 +61,8 @@ List::List( Fn shownHeight) : RpWidget(parent) , _st(st) -, _shownHeight(shownHeight) { +, _shownHeight(shownHeight) +, _snapExpandedTimer([=] { requestExpanded(_expanded); }) { setCursor(style::cur_default); std::move(content) | rpl::start_with_next([=](Content &&content) { @@ -251,6 +257,20 @@ rpl::producer<> List::loadMoreRequests() const { return _loadMoreRequests.events(); } +void List::requestExpanded(bool expanded) { + _snapExpandedTimer.cancel(); + if (_expanded != expanded) { + _expanded = expanded; + _expandedAnimation.start( + [=] { update(); }, + _expanded ? 0. : 1., + _expanded ? 1. : 0., + st::slideWrapDuration, + anim::sineInOut); + } + _toggleExpandedRequests.fire_copy(_expanded); +} + void List::enterEventHook(QEnterEvent *e) { _entered.fire({}); } @@ -259,12 +279,46 @@ void List::resizeEvent(QResizeEvent *e) { updateScrollMax(); } -List::Layout List::computeLayout() const { +void List::updateExpanding(int minHeight, int shownHeight, int fullHeight) { + Expects(shownHeight == minHeight || fullHeight > minHeight); + + const auto ratio = (shownHeight == minHeight) + ? 0. + : (float64(shownHeight - minHeight) / (fullHeight - minHeight)); + if (_lastRatio == ratio) { + return; + } + const auto expanding = (ratio > _lastRatio); + _lastRatio = ratio; + const auto change = _expanded + ? (!expanding && ratio < kCollapseAfterRatio) + : (expanding && ratio > kExpandAfterRatio); + if (change) { + requestExpanded(!_expanded); + } +} + +List::Layout List::computeLayout() { const auto &st = _st.small; const auto &full = _st.full; const auto shownHeight = std::max(_shownHeight(), st.height); - const auto ratio = float64(shownHeight - st.height) + if (_lastHeight != shownHeight) { + _lastHeight = shownHeight; + if (_lastHeight == st.height || _lastHeight == full.height) { + _snapExpandedTimer.cancel(); + } else { + _snapExpandedTimer.callOnce(kSnapExpandedTimeout); + } + } + updateExpanding(st.height, shownHeight, full.height); + + const auto expanded = _expandedAnimation.value(_expanded ? 1. : 0.); + const auto expandedRatio = float64(shownHeight - st.height) / (full.height - st.height); + const auto collapsedRatio = expandedRatio * kFrictionRatio; + const auto ratio = expandedRatio * expanded + + collapsedRatio * (1. - expanded); + const auto lerp = [&](float64 a, float64 b) { return a + (b - a) * ratio; }; @@ -304,6 +358,7 @@ List::Layout List::computeLayout() const { return Layout{ .itemsCount = itemsCount, .shownHeight = shownHeight, + .expandedRatio = expandedRatio, .ratio = ratio, .thumbnailLeft = thumbnailLeft, .photoLeft = photoLeft, @@ -326,14 +381,24 @@ void List::paintEvent(QPaintEvent *e) { const auto &full = _st.full; const auto layout = computeLayout(); const auto ratio = layout.ratio; + const auto expandRatio = (ratio >= kCollapseAfterRatio) + ? 1. + : (ratio <= kExpandAfterRatio * kFrictionRatio) + ? 0. + : ((ratio - kExpandAfterRatio * kFrictionRatio) + / (kCollapseAfterRatio - kExpandAfterRatio * kFrictionRatio)); const auto lerp = [&](float64 a, float64 b) { return a + (b - a) * ratio; }; + const auto elerp = [&](float64 a, float64 b) { + return a + (b - a) * expandRatio; + }; auto &rendering = _data.empty() ? _hidingData : _data; - const auto line = lerp(st.lineTwice, full.lineTwice) / 2.; - const auto lineRead = lerp(st.lineReadTwice, full.lineReadTwice) / 2.; + const auto line = elerp(st.lineTwice, full.lineTwice) / 2.; + const auto lineRead = elerp(st.lineReadTwice, full.lineReadTwice) / 2.; const auto photoTopSmall = (st.height - st.photo) / 2.; - const auto photoTop = lerp(photoTopSmall, full.photoTop); + const auto photoTop = photoTopSmall + + (full.photoTop - photoTopSmall) * layout.expandedRatio; const auto photo = lerp(st.photo, full.photo); const auto summaryTop = st.nameTop - (st.photoTop + (st.photo / 2.)) @@ -343,15 +408,15 @@ void List::paintEvent(QPaintEvent *e) { const auto nameWidth = nameScale * AvailableNameWidth(_st); const auto nameHeight = nameScale * full.nameStyle.font->height; const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.; - const auto readUserpicOpacity = lerp(_st.readOpacity, 1.); - const auto readUserpicAppearingOpacity = lerp(_st.readOpacity, 0.); + const auto readUserpicOpacity = elerp(_st.readOpacity, 1.); + const auto readUserpicAppearingOpacity = elerp(_st.readOpacity, 0.); auto p = QPainter(this); p.fillRect(e->rect(), _bgOverride.value_or(_st.bg)); p.translate(0, height() - layout.shownHeight); - const auto drawSmall = (ratio < 1.); - const auto drawFull = (ratio > 0.); + const auto drawSmall = (expandRatio < 1.); + const auto drawFull = (expandRatio > 0.); auto hq = PainterHighQualityEnabler(p); const auto count = std::max( @@ -364,6 +429,7 @@ void List::paintEvent(QPaintEvent *e) { Item *itemSmall = nullptr; int indexFull = 0; Item *itemFull = nullptr; + float64 photoTop = 0.; explicit operator bool() const { return itemSmall || itemFull; @@ -372,6 +438,11 @@ void List::paintEvent(QPaintEvent *e) { const auto lookup = [&](int index) { const auto indexSmall = layout.startIndexSmall + index; const auto indexFull = layout.startIndexFull + index; + const auto ySmall = photoTopSmall + + ((indexSmall - layout.smallSkip + 1) + * (photoTop - photoTopSmall) / 3.); + const auto y = elerp(ySmall, photoTop); + const auto small = (drawSmall && indexSmall < layout.endIndexSmall && indexSmall >= layout.smallSkip) @@ -381,7 +452,7 @@ void List::paintEvent(QPaintEvent *e) { ? &rendering.items[indexFull] : nullptr; const auto x = layout.left + layout.single * index; - return Single{ x, indexSmall, small, indexFull, full }; + return Single{ x, indexSmall, small, indexFull, full, y }; }; const auto hasUnread = [&](const Single &single) { return (single.itemSmall && single.itemSmall->element.unread) @@ -427,18 +498,23 @@ void List::paintEvent(QPaintEvent *e) { enumerate([&](Single single) { // Name. if (const auto full = single.itemFull) { - p.setOpacity(ratio); validateName(full); - p.drawImage( - QRectF(single.x + nameLeft, nameTop, nameWidth, nameHeight), - full->nameCache); + if (expandRatio > 0.) { + p.setOpacity(expandRatio); + p.drawImage(QRectF( + single.x + nameLeft, + nameTop, + nameWidth, + nameHeight + ), full->nameCache); + } } // Unread gradient. const auto x = single.x; const auto userpic = QRectF( x + layout.photoLeft, - photoTop, + single.photoTop, photo, photo); const auto small = single.itemSmall; @@ -448,9 +524,9 @@ void List::paintEvent(QPaintEvent *e) { const auto unreadOpacity = (smallUnread && fullUnread) ? 1. : smallUnread - ? (1. - ratio) + ? (1. - expandRatio) : fullUnread - ? ratio + ? expandRatio : 0.; if (unreadOpacity > 0.) { p.setOpacity(unreadOpacity); @@ -475,7 +551,7 @@ void List::paintEvent(QPaintEvent *e) { const auto x = single.x; const auto userpic = QRectF( x + layout.photoLeft, - photoTop, + single.photoTop, photo, photo); const auto small = single.itemSmall; @@ -487,7 +563,7 @@ void List::paintEvent(QPaintEvent *e) { const auto hasReadLine = (itemFull && !fullUnread); if (hasReadLine) { auto color = st::dialogsUnreadBgMuted->c; - color.setAlphaF(color.alphaF() * ratio); + color.setAlphaF(color.alphaF() * expandRatio); auto pen = QPen(color); pen.setWidthF(lineRead); p.setPen(pen); @@ -508,16 +584,18 @@ void List::paintEvent(QPaintEvent *e) { } else { if (small) { p.setOpacity(smallUnread - ? (itemFull ? 1. : (1. - ratio)) + ? (itemFull ? 1. : (1. - expandRatio)) : (itemFull ? _st.readOpacity : readUserpicAppearingOpacity)); validateThumbnail(small); - const auto size = (ratio > 0.) ? full.photo : st.photo; + const auto size = (expandRatio > 0.) + ? full.photo + : st.photo; p.drawImage(userpic, small->element.thumbnail->image(size)); } if (itemFull) { - p.setOpacity(ratio); + p.setOpacity(expandRatio); validateThumbnail(itemFull); const auto size = full.photo; p.drawImage( @@ -670,7 +748,7 @@ void List::wheelEvent(QWheelEvent *e) { const auto used = now - delta; const auto next = std::clamp(used, 0, _scrollLeftMax); if (next != now) { - _toggleExpandedRequests.fire(true); + requestExpanded(true); _scrollLeft = next; updateSelected(); checkLoadMore(); @@ -698,7 +776,7 @@ void List::mouseMoveEvent(QMouseEvent *e) { if ((_lastMousePosition - *_mouseDownPosition).manhattanLength() >= QApplication::startDragDistance()) { if (_shownHeight() < _st.full.height) { - _toggleExpandedRequests.fire(true); + requestExpanded(true); } _dragging = true; _startDraggingLeft = _scrollLeft; @@ -742,7 +820,7 @@ void List::mouseReleaseEvent(QMouseEvent *e) { updateSelected(); if (_selected == pressed) { if (_selected < 0) { - _toggleExpandedRequests.fire(true); + requestExpanded(true); } else if (_selected < _data.items.size()) { _clicks.fire_copy(_data.items[_selected].element.id); } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index 1f1cabb45e..1259a0beb4 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/qt/qt_compare.h" +#include "base/timer.h" #include "base/weak_ptr.h" #include "ui/rp_widget.h" @@ -148,7 +149,9 @@ private: void checkDragging(); bool finishDragging(); void checkLoadMore(); + void requestExpanded(bool expanded); + void updateExpanding(int minHeight, int shownHeight, int fullHeight); void updateHeight(); void toggleAnimated(bool shown); void paintSummary( @@ -157,7 +160,7 @@ private: float64 summaryTop, float64 hidden); - [[nodiscard]] Layout computeLayout() const; + [[nodiscard]] Layout computeLayout(); const style::DialogsStoriesList &_st; Content _content; @@ -181,6 +184,12 @@ private: int _scrollLeftMax = 0; bool _dragging = false; + Ui::Animations::Simple _expandedAnimation; + base::Timer _snapExpandedTimer; + float64 _lastRatio = 0.; + int _lastHeight = 0; + bool _expanded = false; + int _selected = -1; int _pressed = -1;