Nice expand / collapse animations.

This commit is contained in:
John Preston 2023-06-27 14:49:04 +04:00
parent aff094f278
commit 7f583f86c0
3 changed files with 125 additions and 30 deletions

View file

@ -1137,7 +1137,16 @@ void Widget::scrollToDefault(bool verytop) {
startScrollUpButtonAnimation(false); startScrollUpButtonAnimation(false);
const auto scroll = [=] { 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( _scrollToAnimation.start(
@ -2019,7 +2028,6 @@ void Widget::dropEvent(QDropEvent *e) {
void Widget::listScrollUpdated() { void Widget::listScrollUpdated() {
const auto scrollTop = _scroll->scrollTop(); const auto scrollTop = _scroll->scrollTop();
PROFILE_LOG(("SCROLLED: %1").arg(scrollTop));
_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
updateScrollUpVisibility(); updateScrollUpVisibility();

View file

@ -18,8 +18,12 @@ namespace Dialogs::Stories {
namespace { namespace {
constexpr auto kSmallThumbsShown = 3; constexpr auto kSmallThumbsShown = 3;
constexpr auto kSummaryExpandLeft = 1.5; constexpr auto kSummaryExpandLeft = 1;
constexpr auto kPreloadPages = 2; 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) { [[nodiscard]] int AvailableNameWidth(const style::DialogsStoriesList &st) {
const auto &full = st.full; const auto &full = st.full;
@ -33,6 +37,7 @@ constexpr auto kPreloadPages = 2;
struct List::Layout { struct List::Layout {
int itemsCount = 0; int itemsCount = 0;
int shownHeight = 0; int shownHeight = 0;
float64 expandedRatio = 0.;
float64 ratio = 0.; float64 ratio = 0.;
float64 thumbnailLeft = 0.; float64 thumbnailLeft = 0.;
float64 photoLeft = 0.; float64 photoLeft = 0.;
@ -56,7 +61,8 @@ List::List(
Fn<int()> shownHeight) Fn<int()> shownHeight)
: RpWidget(parent) : RpWidget(parent)
, _st(st) , _st(st)
, _shownHeight(shownHeight) { , _shownHeight(shownHeight)
, _snapExpandedTimer([=] { requestExpanded(_expanded); }) {
setCursor(style::cur_default); setCursor(style::cur_default);
std::move(content) | rpl::start_with_next([=](Content &&content) { std::move(content) | rpl::start_with_next([=](Content &&content) {
@ -251,6 +257,20 @@ rpl::producer<> List::loadMoreRequests() const {
return _loadMoreRequests.events(); 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) { void List::enterEventHook(QEnterEvent *e) {
_entered.fire({}); _entered.fire({});
} }
@ -259,12 +279,46 @@ void List::resizeEvent(QResizeEvent *e) {
updateScrollMax(); 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 &st = _st.small;
const auto &full = _st.full; const auto &full = _st.full;
const auto shownHeight = std::max(_shownHeight(), st.height); 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); / (full.height - st.height);
const auto collapsedRatio = expandedRatio * kFrictionRatio;
const auto ratio = expandedRatio * expanded
+ collapsedRatio * (1. - expanded);
const auto lerp = [&](float64 a, float64 b) { const auto lerp = [&](float64 a, float64 b) {
return a + (b - a) * ratio; return a + (b - a) * ratio;
}; };
@ -304,6 +358,7 @@ List::Layout List::computeLayout() const {
return Layout{ return Layout{
.itemsCount = itemsCount, .itemsCount = itemsCount,
.shownHeight = shownHeight, .shownHeight = shownHeight,
.expandedRatio = expandedRatio,
.ratio = ratio, .ratio = ratio,
.thumbnailLeft = thumbnailLeft, .thumbnailLeft = thumbnailLeft,
.photoLeft = photoLeft, .photoLeft = photoLeft,
@ -326,14 +381,24 @@ void List::paintEvent(QPaintEvent *e) {
const auto &full = _st.full; const auto &full = _st.full;
const auto layout = computeLayout(); const auto layout = computeLayout();
const auto ratio = layout.ratio; 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) { const auto lerp = [&](float64 a, float64 b) {
return a + (b - a) * ratio; return a + (b - a) * ratio;
}; };
const auto elerp = [&](float64 a, float64 b) {
return a + (b - a) * expandRatio;
};
auto &rendering = _data.empty() ? _hidingData : _data; auto &rendering = _data.empty() ? _hidingData : _data;
const auto line = lerp(st.lineTwice, full.lineTwice) / 2.; const auto line = elerp(st.lineTwice, full.lineTwice) / 2.;
const auto lineRead = lerp(st.lineReadTwice, full.lineReadTwice) / 2.; const auto lineRead = elerp(st.lineReadTwice, full.lineReadTwice) / 2.;
const auto photoTopSmall = (st.height - st.photo) / 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 photo = lerp(st.photo, full.photo);
const auto summaryTop = st.nameTop const auto summaryTop = st.nameTop
- (st.photoTop + (st.photo / 2.)) - (st.photoTop + (st.photo / 2.))
@ -343,15 +408,15 @@ void List::paintEvent(QPaintEvent *e) {
const auto nameWidth = nameScale * AvailableNameWidth(_st); const auto nameWidth = nameScale * AvailableNameWidth(_st);
const auto nameHeight = nameScale * full.nameStyle.font->height; const auto nameHeight = nameScale * full.nameStyle.font->height;
const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.; const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.;
const auto readUserpicOpacity = lerp(_st.readOpacity, 1.); const auto readUserpicOpacity = elerp(_st.readOpacity, 1.);
const auto readUserpicAppearingOpacity = lerp(_st.readOpacity, 0.); const auto readUserpicAppearingOpacity = elerp(_st.readOpacity, 0.);
auto p = QPainter(this); auto p = QPainter(this);
p.fillRect(e->rect(), _bgOverride.value_or(_st.bg)); p.fillRect(e->rect(), _bgOverride.value_or(_st.bg));
p.translate(0, height() - layout.shownHeight); p.translate(0, height() - layout.shownHeight);
const auto drawSmall = (ratio < 1.); const auto drawSmall = (expandRatio < 1.);
const auto drawFull = (ratio > 0.); const auto drawFull = (expandRatio > 0.);
auto hq = PainterHighQualityEnabler(p); auto hq = PainterHighQualityEnabler(p);
const auto count = std::max( const auto count = std::max(
@ -364,6 +429,7 @@ void List::paintEvent(QPaintEvent *e) {
Item *itemSmall = nullptr; Item *itemSmall = nullptr;
int indexFull = 0; int indexFull = 0;
Item *itemFull = nullptr; Item *itemFull = nullptr;
float64 photoTop = 0.;
explicit operator bool() const { explicit operator bool() const {
return itemSmall || itemFull; return itemSmall || itemFull;
@ -372,6 +438,11 @@ void List::paintEvent(QPaintEvent *e) {
const auto lookup = [&](int index) { const auto lookup = [&](int index) {
const auto indexSmall = layout.startIndexSmall + index; const auto indexSmall = layout.startIndexSmall + index;
const auto indexFull = layout.startIndexFull + 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 const auto small = (drawSmall
&& indexSmall < layout.endIndexSmall && indexSmall < layout.endIndexSmall
&& indexSmall >= layout.smallSkip) && indexSmall >= layout.smallSkip)
@ -381,7 +452,7 @@ void List::paintEvent(QPaintEvent *e) {
? &rendering.items[indexFull] ? &rendering.items[indexFull]
: nullptr; : nullptr;
const auto x = layout.left + layout.single * index; 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) { const auto hasUnread = [&](const Single &single) {
return (single.itemSmall && single.itemSmall->element.unread) return (single.itemSmall && single.itemSmall->element.unread)
@ -427,18 +498,23 @@ void List::paintEvent(QPaintEvent *e) {
enumerate([&](Single single) { enumerate([&](Single single) {
// Name. // Name.
if (const auto full = single.itemFull) { if (const auto full = single.itemFull) {
p.setOpacity(ratio);
validateName(full); validateName(full);
p.drawImage( if (expandRatio > 0.) {
QRectF(single.x + nameLeft, nameTop, nameWidth, nameHeight), p.setOpacity(expandRatio);
full->nameCache); p.drawImage(QRectF(
single.x + nameLeft,
nameTop,
nameWidth,
nameHeight
), full->nameCache);
}
} }
// Unread gradient. // Unread gradient.
const auto x = single.x; const auto x = single.x;
const auto userpic = QRectF( const auto userpic = QRectF(
x + layout.photoLeft, x + layout.photoLeft,
photoTop, single.photoTop,
photo, photo,
photo); photo);
const auto small = single.itemSmall; const auto small = single.itemSmall;
@ -448,9 +524,9 @@ void List::paintEvent(QPaintEvent *e) {
const auto unreadOpacity = (smallUnread && fullUnread) const auto unreadOpacity = (smallUnread && fullUnread)
? 1. ? 1.
: smallUnread : smallUnread
? (1. - ratio) ? (1. - expandRatio)
: fullUnread : fullUnread
? ratio ? expandRatio
: 0.; : 0.;
if (unreadOpacity > 0.) { if (unreadOpacity > 0.) {
p.setOpacity(unreadOpacity); p.setOpacity(unreadOpacity);
@ -475,7 +551,7 @@ void List::paintEvent(QPaintEvent *e) {
const auto x = single.x; const auto x = single.x;
const auto userpic = QRectF( const auto userpic = QRectF(
x + layout.photoLeft, x + layout.photoLeft,
photoTop, single.photoTop,
photo, photo,
photo); photo);
const auto small = single.itemSmall; const auto small = single.itemSmall;
@ -487,7 +563,7 @@ void List::paintEvent(QPaintEvent *e) {
const auto hasReadLine = (itemFull && !fullUnread); const auto hasReadLine = (itemFull && !fullUnread);
if (hasReadLine) { if (hasReadLine) {
auto color = st::dialogsUnreadBgMuted->c; auto color = st::dialogsUnreadBgMuted->c;
color.setAlphaF(color.alphaF() * ratio); color.setAlphaF(color.alphaF() * expandRatio);
auto pen = QPen(color); auto pen = QPen(color);
pen.setWidthF(lineRead); pen.setWidthF(lineRead);
p.setPen(pen); p.setPen(pen);
@ -508,16 +584,18 @@ void List::paintEvent(QPaintEvent *e) {
} else { } else {
if (small) { if (small) {
p.setOpacity(smallUnread p.setOpacity(smallUnread
? (itemFull ? 1. : (1. - ratio)) ? (itemFull ? 1. : (1. - expandRatio))
: (itemFull : (itemFull
? _st.readOpacity ? _st.readOpacity
: readUserpicAppearingOpacity)); : readUserpicAppearingOpacity));
validateThumbnail(small); 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)); p.drawImage(userpic, small->element.thumbnail->image(size));
} }
if (itemFull) { if (itemFull) {
p.setOpacity(ratio); p.setOpacity(expandRatio);
validateThumbnail(itemFull); validateThumbnail(itemFull);
const auto size = full.photo; const auto size = full.photo;
p.drawImage( p.drawImage(
@ -670,7 +748,7 @@ void List::wheelEvent(QWheelEvent *e) {
const auto used = now - delta; const auto used = now - delta;
const auto next = std::clamp(used, 0, _scrollLeftMax); const auto next = std::clamp(used, 0, _scrollLeftMax);
if (next != now) { if (next != now) {
_toggleExpandedRequests.fire(true); requestExpanded(true);
_scrollLeft = next; _scrollLeft = next;
updateSelected(); updateSelected();
checkLoadMore(); checkLoadMore();
@ -698,7 +776,7 @@ void List::mouseMoveEvent(QMouseEvent *e) {
if ((_lastMousePosition - *_mouseDownPosition).manhattanLength() if ((_lastMousePosition - *_mouseDownPosition).manhattanLength()
>= QApplication::startDragDistance()) { >= QApplication::startDragDistance()) {
if (_shownHeight() < _st.full.height) { if (_shownHeight() < _st.full.height) {
_toggleExpandedRequests.fire(true); requestExpanded(true);
} }
_dragging = true; _dragging = true;
_startDraggingLeft = _scrollLeft; _startDraggingLeft = _scrollLeft;
@ -742,7 +820,7 @@ void List::mouseReleaseEvent(QMouseEvent *e) {
updateSelected(); updateSelected();
if (_selected == pressed) { if (_selected == pressed) {
if (_selected < 0) { if (_selected < 0) {
_toggleExpandedRequests.fire(true); requestExpanded(true);
} else if (_selected < _data.items.size()) { } else if (_selected < _data.items.size()) {
_clicks.fire_copy(_data.items[_selected].element.id); _clicks.fire_copy(_data.items[_selected].element.id);
} }

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once #pragma once
#include "base/qt/qt_compare.h" #include "base/qt/qt_compare.h"
#include "base/timer.h"
#include "base/weak_ptr.h" #include "base/weak_ptr.h"
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
@ -148,7 +149,9 @@ private:
void checkDragging(); void checkDragging();
bool finishDragging(); bool finishDragging();
void checkLoadMore(); void checkLoadMore();
void requestExpanded(bool expanded);
void updateExpanding(int minHeight, int shownHeight, int fullHeight);
void updateHeight(); void updateHeight();
void toggleAnimated(bool shown); void toggleAnimated(bool shown);
void paintSummary( void paintSummary(
@ -157,7 +160,7 @@ private:
float64 summaryTop, float64 summaryTop,
float64 hidden); float64 hidden);
[[nodiscard]] Layout computeLayout() const; [[nodiscard]] Layout computeLayout();
const style::DialogsStoriesList &_st; const style::DialogsStoriesList &_st;
Content _content; Content _content;
@ -181,6 +184,12 @@ private:
int _scrollLeftMax = 0; int _scrollLeftMax = 0;
bool _dragging = false; bool _dragging = false;
Ui::Animations::Simple _expandedAnimation;
base::Timer _snapExpandedTimer;
float64 _lastRatio = 0.;
int _lastHeight = 0;
bool _expanded = false;
int _selected = -1; int _selected = -1;
int _pressed = -1; int _pressed = -1;