From 959bc6e1177c987e7dff1d811dae26d18812eb37 Mon Sep 17 00:00:00 2001 From: Ralf Engels Date: Mon, 12 Apr 2010 19:59:22 +0200 Subject: Big rewrite --- main.cpp | 2 +- qkineticscroller.cpp | 390 ++++++++++++++++++++++++++++----------------------- qkineticscroller_p.h | 24 ++-- 3 files changed, 234 insertions(+), 182 deletions(-) diff --git a/main.cpp b/main.cpp index 128d69b..a1b1155 100644 --- a/main.cpp +++ b/main.cpp @@ -14,7 +14,7 @@ int main(int argc, char **argv) s->setWidget(lw); lw->show(); - s->scrollTo(QPointF(80,800)); + s->scrollTo(QPointF(80,300)); return a.exec(); } diff --git a/qkineticscroller.cpp b/qkineticscroller.cpp index 2197ec8..7a378d1 100644 --- a/qkineticscroller.cpp +++ b/qkineticscroller.cpp @@ -57,6 +57,36 @@ QT_BEGIN_NAMESPACE # define qKSDebug while (false) qDebug #endif + +inline bool operator<=(const QPointF &p, qreal f) +{ + return (qAbs(p.x()) <= f) && (qAbs(p.y()) <= f); +} +inline bool operator<(const QPointF &p, qreal f) +{ + return (qAbs(p.x()) < f) && (qAbs(p.y()) < f); +} + +inline bool operator>=(const QPointF &p, qreal f) +{ + return (qAbs(p.x()) >= f) || (qAbs(p.y()) >= f); +} +inline bool operator>(const QPointF &p, qreal f) +{ + return (qAbs(p.x()) > f) || (qAbs(p.y()) > f); +} + +inline QPointF qAbs(const QPointF &p) +{ + return QPointF(qAbs(p.x()), qAbs(p.y())); +} + +inline int qSign(qreal r) +{ + return (r < 0) ? -1 : ((r > 0) ? 1 : 0); +} + + /*! \class QKineticScroller \brief The QKineticScroller class enables kinetic scrolling for any scrolling widget or graphics item. @@ -150,9 +180,15 @@ QKineticScroller::~QKineticScroller() */ QKineticScrollerPrivate::QKineticScrollerPrivate() - : enabled(true), state(QKineticScroller::StateInactive), - overshootPolicy(QKineticScroller::OvershootWhenScrollable), - pressTimestamp(0), lastTimestamp(0), scrollToX(false), scrollToY(false) + : enabled(true) + , state(QKineticScroller::StateInactive) + , overshootPolicy(QKineticScroller::OvershootWhenScrollable) + , pressTimestamp(0) + , lastTimestamp(0) + , scrollToX(false) + , scrollToY(false) + , overshootX(false) + , overshootY(false) { } QKineticScrollerPrivate::~QKineticScrollerPrivate() @@ -254,22 +290,20 @@ void QKineticScrollerPrivate::timerEvent(QTimerEvent *e) struct timerevent { QKineticScroller::State state; - QElapsedTimer *elapsedTimer; - typedef void (QKineticScrollerPrivate::*timerhandler_t)(qint64 elapsed); + typedef void (QKineticScrollerPrivate::*timerhandler_t)(); timerhandler_t handler; }; timerevent timerevents[] = { - { QKineticScroller::StateDragging, &dragTimer, &QKineticScrollerPrivate::timerEventWhileDragging }, - { QKineticScroller::StateScrolling, &scrollTimer, &QKineticScrollerPrivate::timerEventWhileScrolling }, + { QKineticScroller::StateDragging, &QKineticScrollerPrivate::timerEventWhileDragging }, + { QKineticScroller::StateScrolling, &QKineticScrollerPrivate::timerEventWhileScrolling }, }; for (int i = 0; i < int(sizeof(timerevents) / sizeof(*timerevents)); ++i) { timerevent *te = timerevents + i; if (state == te->state) { - qint64 elapsed = te->elapsedTimer ? te->elapsedTimer->restart() : 0; - (this->*te->handler)(elapsed); + (this->*te->handler)(); return; } } @@ -366,7 +400,7 @@ QVariant QKineticScroller::scrollMetric(ScrollMetric metric) const case DragVelocitySmoothingFactor: return d->dragVelocitySmoothingFactor; case LinearDecelerationFactor: return d->linearDecelerationFactor; case ExponentialDecelerationBase: return d->exponentialDecelerationBase; - case OvershootSpringConstant: return d->overshootSpringConstant; + case OvershootSpringConstant: return d->overshootSpringConstantRoot * d->overshootSpringConstantRoot; case OvershootDragResistanceFactor: return d->overshootDragResistanceFactor; case OvershootMaximumDistance: return d->overshootMaximumDistance; case DragStartDistance: return d->dragStartDistance; @@ -395,7 +429,7 @@ void QKineticScroller::setScrollMetric(ScrollMetric metric, const QVariant &valu case DragVelocitySmoothingFactor: d->dragVelocitySmoothingFactor = qBound(qreal(0), value.toReal(), qreal(1)); break; case LinearDecelerationFactor: d->linearDecelerationFactor = qBound(qreal(0), value.toReal(), qreal(1)); break; case ExponentialDecelerationBase: d->exponentialDecelerationBase = qBound(qreal(0), value.toReal(), qreal(1)); break; - case OvershootSpringConstant: d->overshootSpringConstant = value.toReal(); break; + case OvershootSpringConstant: d->overshootSpringConstantRoot = qSqrt(value.toReal()); break; case OvershootDragResistanceFactor: d->overshootDragResistanceFactor = value.toReal(); break; case OvershootMaximumDistance: d->overshootMaximumDistance = value.toPointF(); break; case DragStartDistance: d->dragStartDistance = value.toReal(); break; @@ -467,6 +501,63 @@ void QKineticScrollerPrivate::updateVelocity(const QPointF &deltaPixelRaw, qint6 qKSDebug() << "resulting new velocity:" << velocity; } +qreal QKineticScrollerPrivate::decelerate(qreal v, qreal t) +{ + qreal result = v * qPow(exponentialDecelerationBase, t); + qreal linear = linearDecelerationFactor * t; + if (qAbs(result) > linear) + return result + (result < 0 ? linear : -linear); + else + return 0; +} + +/*! Calculates the current velocity during scrolling + */ +QPointF QKineticScrollerPrivate::calculateVelocity(qreal time) +{ + QPointF velocity; + + // -- x coordinate + if (overshootX) { + velocity.setX(overshootVelocity.x() * qCos(overshootSpringConstantRoot * (time - overshootStartTimeX))); + + } else { + qreal newVelocity = decelerate(releaseVelocity.x(), time); + + if (scrollToX) { + if (qAbs(newVelocity) < qreal(30.0 / 1000) /* 30mm/s */) + newVelocity = qreal(30.0 / 1000) * qSign(releaseVelocity.x()); + + } else { + if (qAbs(newVelocity) < qreal(0.5) * qreal(framesPerSecond) / pixelPerMeter /* 0.5 [pix/frame] */) + newVelocity = 0; + } + + velocity.setX(newVelocity); + } + + // -- y coordinate + if (overshootY) { + velocity.setY(overshootVelocity.y() * qCos(overshootSpringConstantRoot * (time - overshootStartTimeY))); + + } else { + qreal newVelocity = decelerate(releaseVelocity.y(), time); + + if (scrollToY) { + if (qAbs(newVelocity) < qreal(30.0 / 1000) /* 30mm/s */) + newVelocity = qreal(30.0 / 1000) * qSign(releaseVelocity.y()); + + } else { + if (qAbs(newVelocity) < qreal(0.5) * qreal(framesPerSecond) / pixelPerMeter /* 0.5 [pix/frame] */) + newVelocity = 0; + } + + velocity.setY(newVelocity); + } + + return velocity; +} + void QKineticScrollerPrivate::handleDrag(const QPointF &position, qint64 timestamp) { @@ -524,31 +615,6 @@ void QKineticScrollerPrivate::handleDrag(const QPointF &position, qint64 timesta } -inline bool operator<=(const QPointF &p, qreal f) -{ - return (qAbs(p.x()) <= f) && (qAbs(p.y()) <= f); -} -inline bool operator<(const QPointF &p, qreal f) -{ - return (qAbs(p.x()) < f) && (qAbs(p.y()) < f); -} - -inline bool operator>=(const QPointF &p, qreal f) -{ - return (qAbs(p.x()) >= f) || (qAbs(p.y()) >= f); -} -inline bool operator>(const QPointF &p, qreal f) -{ - return (qAbs(p.x()) > f) || (qAbs(p.y()) > f); -} - -inline QPointF qAbs(const QPointF &p) -{ - return QPointF(qAbs(p.x()), qAbs(p.y())); -} - - - bool QKineticScrollerPrivate::pressWhileInactive(QKineticScroller::Input, const QPointF &position, qint64 timestamp) { @@ -566,7 +632,7 @@ bool QKineticScrollerPrivate::pressWhileInactive(QKineticScroller::Input, const bool QKineticScrollerPrivate::releaseWhilePressed(QKineticScroller::Input, const QPointF &, qint64) { - if (!overshootDistance.isNull()) + if (overshootX || overshootY) setState(QKineticScroller::StateScrolling); else setState(QKineticScroller::StateInactive); @@ -623,12 +689,10 @@ bool QKineticScrollerPrivate::moveWhileDragging(QKineticScroller::Input, const Q return true; } -void QKineticScrollerPrivate::timerEventWhileDragging(qint64) +void QKineticScrollerPrivate::timerEventWhileDragging() { - Q_Q(QKineticScroller); - if (!dragDistance.isNull()) { - setContentPositionHelper(q->contentPosition() + overshootDistance - dragDistance); + setContentPositionHelper(-dragDistance); dragDistance = QPointF(0, 0); } } @@ -675,7 +739,7 @@ bool QKineticScrollerPrivate::releaseWhileDragging(QKineticScroller::Input, cons } qKSDebug() << "release While dragging, velocity: "<= minimumVelocity) @@ -686,98 +750,20 @@ bool QKineticScrollerPrivate::releaseWhileDragging(QKineticScroller::Input, cons return true; } -qreal QKineticScrollerPrivate::decelerate(qreal v, qreal expDecel, qreal linDecel) +void QKineticScrollerPrivate::timerEventWhileScrolling() { - v *= expDecel; - if (qAbs(v) >= linDecel) - return v + (v < 0 ? linDecel : -linDecel); - else - return 0;; -} + qreal deltaTime = qreal(scrollRelativeTimer.restart()) / 1000; + qreal time = qreal(scrollAbsoluteTimer.elapsed()) / 1000; -void QKineticScrollerPrivate::timerEventWhileScrolling(qint64 time_ms) -{ - Q_Q(QKineticScroller); - - qreal time = qreal(time_ms) / 1000; - - // --- handle overshooting and stop if the coordinate is going back inside the normal area - if (!overshootDistance.isNull()) { - // calculate the new overshoot distance (convert from [m/s] to [pixel/frame] - QPointF newOvershootDistance = overshootDistance + velocity * time * pixelPerMeter; - - qKSDebug() << "Overshooting, old: "< 0) != (newOvershootDistance.x() > 0))) { - overshootDistance.setX(0); - velocity.setX(0); - } - if (overshootDistance.y() && ((overshootDistance.y() > 0) != (newOvershootDistance.y() > 0))) { - overshootDistance.setY(0); - velocity.setY(0); - } - } - - qWarning() << "Pos: " << q->contentPosition() << " - Vel: " << velocity << " - Time: " << qreal(time) << " - PPM: " << pixelPerMeter; + QPointF newVelocity = calculateVelocity(time); + QPointF deltaPos = newVelocity * deltaTime * pixelPerMeter; // -- move (convert from [m/s] to [pix/frame] - setContentPositionHelper(q->contentPosition() + overshootDistance + velocity * time * pixelPerMeter); - - - // --- (de)accelerate overshooting axis using spring constant or friction - // actually we would need to calculate a root and not just divide - qreal forcePerFrame = overshootSpringConstant * time; - qreal exponentialDecelerationPerFrame = (qreal(1) - ((qreal(1) - exponentialDecelerationBase) * time)); - qreal linearDecelerationPerFrame = linearDecelerationFactor * time; - - // -- x coordinate - if (overshootDistance.x()) { - velocity.rx() -= forcePerFrame * overshootDistance.x() / pixelPerMeter; - velocity.setX(qBound(-overshootMaximumVelocity, velocity.x(), overshootMaximumVelocity)); - - } else if (scrollToX) { - if (qAbs(velocity.x()) * exponentialDecelerationPerFrame > linearDecelerationPerFrame + qreal(30.0 / 1000) )/* 30 px/s */ - velocity.setX(decelerate(velocity.x(), exponentialDecelerationPerFrame, linearDecelerationPerFrame)); - else - velocity.setX((velocity.x()<0 ? -1.0 : 1.0) * qreal(30.0 / 1000)); - - // - target reached? - qreal dist = scrollToPosition.x() - q->contentPosition().x(); -qDebug() << "scroll to x" << scrollToPosition.x() << "v:"<< velocity.x() << " dist " << dist; - if ((velocity.x() > 0.0 && dist <= 0.0) || (velocity.x() < 0.0 && dist >= 0.0)) { - velocity.setX(0.0); - scrollToX = false; - } + setContentPositionHelper(deltaPos); - } else { - velocity.setX(decelerate(velocity.x(), exponentialDecelerationPerFrame, linearDecelerationPerFrame)); - } - - // -- y coordinate - if (overshootDistance.y()) { - velocity.ry() -= forcePerFrame * overshootDistance.y() / pixelPerMeter; - velocity.setY(qBound(-overshootMaximumVelocity, velocity.y(), overshootMaximumVelocity)); - - } else if (scrollToY) { - if (qAbs(velocity.y()) * exponentialDecelerationPerFrame > linearDecelerationPerFrame + qreal(30.0 / 1000) )/* 30 px/s */ - velocity.setY(decelerate(velocity.y(), exponentialDecelerationPerFrame, linearDecelerationPerFrame)); - else - velocity.setY((velocity.y()<0 ? -1.0 : 1.0) * qreal(30.0 / 1000)); - - // - target reached? - qreal dist = scrollToPosition.y() - q->contentPosition().y(); -qDebug() << "scroll to y" << scrollToPosition.y() << "v:"<< velocity.y() << " dist " << dist; - if ((velocity.y() > 0.0 && dist <= 0.0) || (velocity.y() < 0.0 && dist >= 0.0)) { - velocity.setY(0.0); - scrollToY = false; - } - - } else { - velocity.setY(decelerate(velocity.y(), exponentialDecelerationPerFrame, linearDecelerationPerFrame)); - } + qWarning() << "DeltaPos: " << deltaPos << " - newVel: " << newVelocity << " - Time: " << time << " - PPM: " << pixelPerMeter; - if (overshootDistance.isNull() && (velocity < qreal(0.5) * qreal(framesPerSecond) / pixelPerMeter /* 1 [pix/frame] */) && !scrollToX && !scrollToY) + if (newVelocity.isNull()) setState(QKineticScroller::StateInactive); } @@ -800,7 +786,6 @@ void QKineticScrollerPrivate::setState(QKineticScroller::State newstate) switch (state) { case QKineticScroller::StateDragging: - overshootDistance *= overshootDragResistanceFactor; break; default: break; @@ -839,16 +824,29 @@ void QKineticScrollerPrivate::setState(QKineticScroller::State newstate) qWarning() << "State change from " << stateName(state) << " to " << stateName(newstate) << ", but timer is already active."; } } - dragTimer.start(); break; case QKineticScroller::StateScrolling: - // if (state == QKineticScroller::StatePressed) { - if (!timerId) { - timerId = startTimer(1000 / framesPerSecond); - } - // } - scrollTimer.start(); + if (!timerId) { + timerId = startTimer(1000 / framesPerSecond); + } + scrollRelativeTimer.start(); + scrollAbsoluteTimer.start(); + releaseVelocity = velocity; + + if (state == QKineticScroller::StateDragging) { + QPointF oldPos = q->contentPosition(); + QPointF maxPos = q->maximumContentPosition(); + QPointF oldClampedPos; + oldClampedPos.setX(qBound(qreal(0), oldPos.x(), maxPos.x())); + oldClampedPos.setY(qBound(qreal(0), oldPos.y(), maxPos.y())); + + QPointF overshootDistance = oldPos - oldClampedPos; + + overshootStartTimeX = overshootStartTimeY = scrollAbsoluteTimer.elapsed() - M_PI / (overshootSpringConstantRoot * 2); + overshootVelocity = overshootDistance * overshootSpringConstantRoot; + } + break; } @@ -885,6 +883,7 @@ void QKineticScroller::scrollTo(const QPointF &pos, int scrollTime) // if the start velocity is below that then the scrolling would stop before scrollTime. qreal vMin = d->linearDecelerationFactor * time / qPow(d->exponentialDecelerationBase, time); + /* // estimate of the distance passed within the vMin time during scrollTime qreal distMin = qreal(scrollTime) * vMin / 2.0; @@ -893,25 +892,26 @@ void QKineticScroller::scrollTo(const QPointF &pos, int scrollTime) QPointF v = QPointF((pos.x()-contentPosition().x()) / distMin * vMin, (pos.y()-contentPosition().y()) / distMin * vMin); - qKSDebug() << "QAbstractKineticScroller::scrollTo(" << pos << ", " << v; + */ + + // qKSDebug() << "QAbstractKineticScroller::scrollTo(" << pos << ", " << v; // v(t) = vstart * exponentialDecelerationBase ^ t - linearDecelerationFactor * t // pos(t) = integrate(v(t) * dt) - // pos(t) = vstart * eDB ^ t / ln(eDB) - lDF / (2 * t ^ 2) + // pos(t) = vstart * eDB ^ t / ln(eDB) - lDF / 2 * t ^ 2 // // pos(scrollTime) = pos - contentsPos() - // vstart = (lDF / 2 * scrollTime ^ 2 - (pos - contentPos())) * ln(eDB) / eDB ^ scrollTime -/* + // vstart = ((lDF / 2) * scrollTime ^ 2 + (pos - contentPos())) * ln(eDB) / eDB ^ scrollTime - QPointF v = QPointF(-1, -1) * d->linearDecelerationFactor / (qreal(2) * qreal(scrollTime) * qreal(scrollTime)) - (pos - contentPosition()) * - qLn(d->exponentialDecelerationBase) / qPow(d->exponentialDecelerationBase, scrollTime); + QPointF v = (QPointF(1, 1) * (d->linearDecelerationFactor / qreal(2)) * qreal(time) * qreal(time) + (pos - contentPosition()) / d->pixelPerMeter); + if (d->exponentialDecelerationBase != qreal(1)) + v *= qLn(d->exponentialDecelerationBase) / qPow(d->exponentialDecelerationBase, time); qKSDebug() << "QAbstractKineticScroller::scrollTo(" << pos << ", " << v; qKSDebug() << "QAbstr " << d->linearDecelerationFactor <<" 1: "<< (qreal(2) * qreal(scrollTime) * qreal(scrollTime)) << " 2: " << (pos - contentPosition()) << " 3: " << qLn(d->exponentialDecelerationBase) << "4: " << qPow(d->exponentialDecelerationBase, scrollTime); - v = QPointF(0.01, 0.01); -*/ + // start the scrolling d->scrollToPosition = pos; d->scrollToX = true; @@ -962,54 +962,98 @@ void QKineticScroller::ensureVisible(const QPointF &pos, int xmargin, int ymargi Decomposes the position into a scroll and an overshoot part. Also keeps track of the current over-shooting value in overshootDist. */ -void QKineticScrollerPrivate::setContentPositionHelper(const QPointF &pos) +void QKineticScrollerPrivate::setContentPositionHelper(const QPointF &deltaPos) { Q_Q(QKineticScroller); + QPointF oldPos = q->contentPosition(); + QPointF newPos = oldPos + deltaPos; QPointF maxPos = q->maximumContentPosition(); - QPointF clampedPos; - clampedPos.setX(qBound(qreal(0), pos.x(), maxPos.x())); - clampedPos.setY(qBound(qreal(0), pos.y(), maxPos.y())); + QPointF oldClampedPos; + oldClampedPos.setX(qBound(qreal(0), oldPos.x(), maxPos.x())); + oldClampedPos.setY(qBound(qreal(0), oldPos.y(), maxPos.y())); - // -- stop scroll to if hitting boundary - if (clampedPos.x() != pos.x()) - scrollToX = false; - if (clampedPos.y() != pos.y()) - scrollToY = false; + QPointF newClampedPos; + newClampedPos.setX(qBound(qreal(0), newPos.x(), maxPos.x())); + newClampedPos.setY(qBound(qreal(0), newPos.y(), maxPos.y())); + // --- handle overshooting and stop if the coordinate is going back inside the normal area bool alwaysOvershoot = (overshootPolicy == QKineticScroller::OvershootAlwaysOn); - qreal overshootX = (maxPos.x() || alwaysOvershoot) ? pos.x() - clampedPos.x() : 0; - qreal overshootY = (maxPos.y() || alwaysOvershoot) ? pos.y() - clampedPos.y() : 0; - // -- stop at the maximum overshoot distance (if set) - if (!overshootMaximumDistance.isNull()) { + qreal oldOvershootX = (maxPos.x() || alwaysOvershoot) ? oldPos.x() - oldClampedPos.x() : 0; + qreal oldOvershootY = (maxPos.y() || alwaysOvershoot) ? oldPos.y() - oldClampedPos.y() : 0; + if (state == QKineticScroller::StateDragging) { + oldOvershootX /= overshootDragResistanceFactor; + oldOvershootY /= overshootDragResistanceFactor; + } - qreal x = qBound(-overshootMaximumDistance.x() * pixelPerMeter, overshootX, overshootMaximumDistance.x() * pixelPerMeter); - if (x != overshootX) - velocity.setX(0); - overshootDistance.setX(x); + qreal newOvershootX = (maxPos.x() || alwaysOvershoot) ? newPos.x() - newClampedPos.x() : 0; + qreal newOvershootY = (maxPos.y() || alwaysOvershoot) ? newPos.y() - newClampedPos.y() : 0; - qKSDebug() << "Overshoot y: "<<(overshootMaximumDistance.y() * pixelPerMeter)<<"distance: "<setContentPosition(clampedPos, realOvershootDistance); + q->setContentPosition(newClampedPos, realOvershootDistance); + + qDebug() << "setContentPositionHelper" << newClampedPos << " overshoot:" << realOvershootDistance; } diff --git a/qkineticscroller_p.h b/qkineticscroller_p.h index cb06feb..1f5a564 100644 --- a/qkineticscroller_p.h +++ b/qkineticscroller_p.h @@ -79,14 +79,16 @@ public: bool pressWhileScrolling(QKineticScroller::Input input, const QPointF &position, qint64 timestamp); void timerEvent(QTimerEvent *); - void timerEventWhileDragging(qint64 deltaTime); - void timerEventWhileScrolling(qint64 deltaTime); + void timerEventWhileDragging(); + void timerEventWhileScrolling(); void handleDrag(const QPointF &position, qint64 timestamp); void updateVelocity(const QPointF &deltaPixel, qint64 deltaTime); - void setContentPositionHelper(const QPointF &position); - qreal decelerate(qreal v, qreal expDecel, qreal linDecel); + qreal decelerate(qreal v, qreal t); + QPointF calculateVelocity(qreal time); + void setContentPositionHelper(const QPointF &deltaPos); + static const char *stateName(QKineticScroller::State state); static const char *inputName(QKineticScroller::Input input); @@ -96,7 +98,7 @@ public: qreal dragVelocitySmoothingFactor; qreal linearDecelerationFactor; qreal exponentialDecelerationBase; - qreal overshootSpringConstant; + qreal overshootSpringConstantRoot; qreal overshootDragResistanceFactor; QPointF overshootMaximumDistance; qreal overshootMaximumVelocity; @@ -126,16 +128,22 @@ public: qint64 lastTimestamp; QPointF dragDistance; - QPointF overshootDistance; QPointF scrollToPosition; bool scrollToX; bool scrollToY; + bool overshootX; + bool overshootY; + qreal pixelPerMeter; - QElapsedTimer dragTimer; - QElapsedTimer scrollTimer; + QElapsedTimer scrollRelativeTimer; + QElapsedTimer scrollAbsoluteTimer; + QPointF releaseVelocity; + QPointF overshootVelocity; + qreal overshootStartTimeX; + qreal overshootStartTimeY; int timerId; QKineticScroller *q_ptr; -- cgit v1.2.3