/**************************************************************************** ** ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). ** All rights reserved. ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** This file is part of the QtGui module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** No Commercial Usage ** This file contains pre-release code and may not be distributed. ** You may use this file in accordance with the terms and conditions ** contained in the Technology Preview License Agreement accompanying ** this package. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** If you have questions regarding the use of this file, please contact ** Nokia at qt-info@nokia.com. ** ** ** ** ** ** ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE #define KINETIC_SCROLLER_DEBUG #ifdef KINETIC_SCROLLER_DEBUG # define qKSDebug qDebug #else # 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. \ingroup qtmaemo5 \since 4.6 \preliminary With kinetic scrolling, the user can push the widget in a given direction and it will continue to scroll in this direction until it is stopped either by the user or by friction. Aspects of inertia, friction and other physical concepts can be changed in order to fine-tune an intuitive user experience. To enable kinetic scrolling for a widget or graphics item, you need to derive from this class and implement at least all the pure-virtual functions. Qt for Maemo 5 already comes with two implementations for QScrollArea and QWebView, and those kinetic scrollers are automatically instantiated and attached to these widgets on creation. In the QScrollArea case, the kinetic scroller is initially disabled. However, for QItemView and QScrollArea derived classes it is enabled by default. You can obtain these automatically created objects via a dynamic property: \code // disable the kinetic scroller on scrollArea QKineticScroller *scroller = scrollArea->property("kineticScroller") .value(); if (scroller) scroller->setEnabled(false); \endcode In addition there is also an example on how you would add kinetic scrolling to a QGraphicsView based application in \c maemobrowser examples in the \c maemo5 examples directory. The kinetic scroller installs an event filter on the widget to handle mouse presses and moves on the widget \mdash presses and moves on a device's touch screen are also handled by this mechanism. These events will be interpreted as scroll actions depending on the current state() of the scroller. Even though this kinetic scroller has a huge number of settings, we recommend that you leave them all at their default values. In case you really want to change them you can try out the \c kineticscroller example in the \c maemo5 examples directory. \sa QWidget */ /*! Constructs a new kinetic scroller. */ QKineticScroller::QKineticScroller() : d_ptr(new QKineticScrollerPrivate()) { Q_D(QKineticScroller); d->q_ptr = this; d->init(); } /*! \internal */ QKineticScroller::QKineticScroller(QKineticScrollerPrivate &dd) : d_ptr(&dd) { Q_D(QKineticScroller); d->q_ptr = this; d->init(); } /*! Destroys the scroller. */ QKineticScroller::~QKineticScroller() { } /*! \enum QKineticScroller::State This enum describes the possible states the kinetic scroller can be in. \value Inactive The scroller is inactive. It may also have been disabled. \value Pressed The user has pressed the mouse button (or pressed the the touch screen). \value Dragging The user is dragging the mouse cursor (or other input point) over the scroll area. \value Scrolling Scrolling is occurring without direct user input. */ QKineticScrollerPrivate::QKineticScrollerPrivate() : enabled(true) , state(QKineticScroller::StateInactive) , overshootPolicy(QKineticScroller::OvershootWhenScrollable) , pressTimestamp(0) , lastTimestamp(0) , scrollToX(false) , scrollToY(false) , overshootX(false) , overshootY(false) { } QKineticScrollerPrivate::~QKineticScrollerPrivate() { } void QKineticScrollerPrivate::init() { Q_Q(QKineticScroller); q->setDpiFromWidget(0); q->resetScrollMetrics(); } void QKineticScroller::resetScrollMetrics() { static QMap metrics; #ifdef Q_WS_MAEMO_5 metrics.insert(DragVelocitySmoothingFactor, qreal(0.15)); metrics.insert(ExponentialDecelerationBase, qreal(0.38)); // 0.85^20 metrics.insert(LinearDecelerationFactor, qreal(0)); metrics.insert(OvershootSpringConstant, qreal(0.1)); metrics.insert(OvershootDragResistanceFactor, qreal(1)); metrics.insert(OvershootMaximumDistance, QPointF(qreal(150), qreal(150))); metrics.insert(OvershootMaximumVelocity, qreal(2600)); metrics.insert(DragStartDistance, qreal(25)); metrics.insert(DragStartDirectionErrorMargin, qreal(10)); metrics.insert(MaximumVelocity, qreal(70000)); metrics.insert(MinimumVelocity, qreal(200)); metrics.insert(MaximumNonAcceleratedVelocity, qreal(5600)); metrics.insert(MaximumClickThroughVelocity, qreal(700)); metrics.insert(AxisLockThreshold, qreal(0)); metrics.insert(FastSwipeBaseVelocity, qreal(540)); metrics.insert(FastSwipeMinimumVelocity, qreal(800)); metrics.insert(FastSwipeMaximumTime, qreal(0.125)); metrics.insert(FramesPerSecond, qreal(20)); #else metrics.insert(DragVelocitySmoothingFactor, qreal(0.02)); metrics.insert(ExponentialDecelerationBase, qreal(1)); metrics.insert(LinearDecelerationFactor, qreal(0.38)); metrics.insert(OvershootSpringConstant, qreal(15.0)); metrics.insert(OvershootDragResistanceFactor, qreal(0.5)); metrics.insert(OvershootMaximumDistance, QPointF(0,0)); // QPointF(qreal(14.25 / 1000), qreal(14.25 / 1000))); metrics.insert(DragStartDistance, qreal(2.5 / 1000)); metrics.insert(DragStartDirectionErrorMargin, qreal(1.0 / 1000)); metrics.insert(MaximumVelocity, qreal(6650.0 / 1000)); metrics.insert(MaximumVelocity, qreal(6650.0 / 1000)); metrics.insert(MinimumVelocity, qreal(30.0 / 1000)); metrics.insert(MaximumNonAcceleratedVelocity, qreal(532.0 / 1000)); metrics.insert(MaximumClickThroughVelocity, qreal(66.5 / 1000)); metrics.insert(AxisLockThreshold, qreal(0)); metrics.insert(FastSwipeBaseVelocity, qreal(51.3 / 1000)); metrics.insert(FastSwipeMinimumVelocity, qreal(76.0 / 1000)); metrics.insert(FastSwipeMaximumTime, qreal(0.125)); metrics.insert(FramesPerSecond, qreal(60)); #endif if (!metrics.isEmpty()) { for (QMap::const_iterator it = metrics.constBegin(); it != metrics.constEnd(); ++it) setScrollMetric(it.key(), it.value()); if (metrics.count() != ScrollMetricCount) qWarning("QKineticScroller::resetAllMetrics(): scroll metrics parameter set did not contain all metrics."); } else { qWarning("QKineticScroller::resetAllMetrics(): no platform default parameter set available."); } } const char *QKineticScrollerPrivate::stateName(QKineticScroller::State state) { switch (state) { case QKineticScroller::StateInactive: return "inactive"; case QKineticScroller::StatePressed: return "pressed"; case QKineticScroller::StateDragging: return "dragging"; case QKineticScroller::StateScrolling: return "scrolling"; default: return "(invalid)"; } } const char *QKineticScrollerPrivate::inputName(QKineticScroller::Input input) { switch (input) { case QKineticScroller::InputPress: return "press"; case QKineticScroller::InputMove: return "move"; case QKineticScroller::InputRelease: return "release"; default: return "(invalid)"; } } void QKineticScrollerPrivate::timerEvent(QTimerEvent *e) { if (e->timerId() != timerId) { QObject::timerEvent(e); return; } struct timerevent { QKineticScroller::State state; typedef void (QKineticScrollerPrivate::*timerhandler_t)(); timerhandler_t handler; }; timerevent timerevents[] = { { 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) { (this->*te->handler)(); return; } } if (timerId) { qWarning() << "Unhandled timer event, while in state " << stateName(state); killTimer(timerId); timerId = 0; } // otherwise this is a timer event that was already queued when the // timer was killed, so just ignore it } bool QKineticScroller::handleInput(Input input, const QPointF &position, qint64 timestamp) { Q_D(QKineticScroller); qKSDebug() << __PRETTY_FUNCTION__ << input << "pos: " << position << "timestamp: " << timestamp; struct statechange { State state; Input input; typedef bool (QKineticScrollerPrivate::*inputhandler_t)(Input input, const QPointF &position, qint64 timestamp); inputhandler_t handler; }; statechange statechanges[] = { { StateInactive, InputPress, &QKineticScrollerPrivate::pressWhileInactive }, { StatePressed, InputMove, &QKineticScrollerPrivate::moveWhilePressed }, { StatePressed, InputRelease, &QKineticScrollerPrivate::releaseWhilePressed }, { StateDragging, InputMove, &QKineticScrollerPrivate::moveWhileDragging }, { StateDragging, InputRelease, &QKineticScrollerPrivate::releaseWhileDragging }, { StateScrolling, InputPress, &QKineticScrollerPrivate::pressWhileScrolling } }; for (int i = 0; i < int(sizeof(statechanges) / sizeof(*statechanges)); ++i) { statechange *sc = statechanges + i; if (d->state == sc->state && input == sc->input) return (d->*sc->handler)(input, position, timestamp); } qWarning() << "Unhandled input: got input " << d->inputName(input) << " while in state " << d->stateName(d->state); return false; } bool QKineticScroller::isEnabled() const { Q_D(const QKineticScroller); return d->enabled; } void QKineticScroller::setEnabled(bool b) { Q_D(QKineticScroller); d->enabled = b; } QKineticScroller::State QKineticScroller::state() const { Q_D(const QKineticScroller); return d->state; } /*! Resets the internal state of the kinetic scroller. This function is not needed for normal use. This function only needs to be called if the kinetic scroller is being re-attached to a different widget. */ void QKineticScroller::reset() { Q_D(QKineticScroller); d->setState(StateInactive); } QKineticScroller::OvershootPolicy QKineticScroller::overshootPolicy() const { Q_D(const QKineticScroller); return d->overshootPolicy; } void QKineticScroller::setOvershootPolicy(QKineticScroller::OvershootPolicy policy) { Q_D(QKineticScroller); d->overshootPolicy = policy; } QVariant QKineticScroller::scrollMetric(ScrollMetric metric) const { Q_D(const QKineticScroller); switch (metric) { case DragVelocitySmoothingFactor: return d->dragVelocitySmoothingFactor; case LinearDecelerationFactor: return d->linearDecelerationFactor; case ExponentialDecelerationBase: return d->exponentialDecelerationBase; case OvershootSpringConstant: return d->overshootSpringConstantRoot * d->overshootSpringConstantRoot; case OvershootDragResistanceFactor: return d->overshootDragResistanceFactor; case OvershootMaximumDistance: return d->overshootMaximumDistance; case DragStartDistance: return d->dragStartDistance; case DragStartDirectionErrorMargin: return d->dragStartDirectionErrorMargin; case MinimumVelocity: return d->minimumVelocity; case MaximumVelocity: return d->maximumVelocity; case MaximumNonAcceleratedVelocity: return d->maximumNonAcceleratedVelocity; case MaximumClickThroughVelocity: return d->maximumClickThroughVelocity; case AxisLockThreshold: return d->axisLockThreshold; case FramesPerSecond: return d->framesPerSecond; case FastSwipeMaximumTime: return d->fastSwipeMaximumTime; case FastSwipeMinimumVelocity: return d->fastSwipeMinimumVelocity; case FastSwipeBaseVelocity: return d->fastSwipeBaseVelocity; case ScrollMetricCount: break; } return QVariant(); } void QKineticScroller::setScrollMetric(ScrollMetric metric, const QVariant &value) { Q_D(QKineticScroller); switch (metric) { 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->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; case DragStartDirectionErrorMargin: d->dragStartDirectionErrorMargin = value.toReal(); break; case MinimumVelocity: d->minimumVelocity = value.toReal(); break; case MaximumVelocity: d->maximumVelocity = value.toReal(); break; case MaximumNonAcceleratedVelocity: d->maximumNonAcceleratedVelocity = value.toReal(); break; case MaximumClickThroughVelocity: d->maximumClickThroughVelocity = value.toReal(); break; case AxisLockThreshold: d->axisLockThreshold = qBound(qreal(0), value.toReal(), qreal(1)); break; case FramesPerSecond: d->framesPerSecond = qBound(1, value.toInt(), 100); break; case FastSwipeMaximumTime: d->fastSwipeMaximumTime = value.toReal(); break; case FastSwipeMinimumVelocity: d->fastSwipeMinimumVelocity = value.toReal(); break; case FastSwipeBaseVelocity: d->fastSwipeBaseVelocity = value.toReal(); break; case ScrollMetricCount: break; } } qreal QKineticScroller::dpi() const { Q_D(const QKineticScroller); return d->pixelPerMeter / qreal(39.3700787); } void QKineticScroller::setDpi(qreal dpi) { Q_D(QKineticScroller); d->pixelPerMeter = dpi * qreal(39.3700787); } void QKineticScroller::setDpiFromWidget(QWidget *widget) { #ifdef Q_WS_MAEMO_5 // The DPI value is hardcoded to 96 on Maemo5: // https://projects.maemo.org/bugzilla/show_bug.cgi?id=152525 // This value (260) is only correct for the N900 though, but // there's no way to get the real DPI at run time. setDpi(qreal(260)); #else if (!widget) widget = QApplication::desktop(); setDpi(qreal(widget->physicalDpiX() + widget->physicalDpiY()) / qreal(2)); #endif } void QKineticScrollerPrivate::updateVelocity(const QPointF &deltaPixelRaw, qint64 deltaTime) { qKSDebug() << "updateVelocity(dP = " << deltaPixelRaw << " [pix], dT = " << deltaTime << " [ms]) -- velocity: " << releaseVelocity << "[m/s]"; QPointF deltaPixel = deltaPixelRaw; // faster than 2.5mm/ms seems bogus (that would be a screen height in ~20 ms) if (((deltaPixelRaw / qreal(deltaTime)).manhattanLength() / pixelPerMeter * 1000) > qreal(2.5)) deltaPixel = deltaPixelRaw * qreal(2.5) * pixelPerMeter / 1000 / (deltaPixelRaw / qreal(deltaTime)).manhattanLength(); qreal inversSmoothingFactor = ((qreal(1) - dragVelocitySmoothingFactor) * qreal(deltaTime) / qreal(1000)); QPointF newv = -deltaPixel / qreal(deltaTime) * qreal(1000) / pixelPerMeter; newv = newv * (qreal(1) - inversSmoothingFactor) + releaseVelocity * inversSmoothingFactor; // newv = newv * dragVelocitySmoothingFactor + velocity * (qreal(1) - dragVelocitySmoothingFactor); if (deltaPixel.x()) releaseVelocity.setX(qBound(-maximumVelocity, newv.x(), maximumVelocity)); if (deltaPixel.y()) releaseVelocity.setY(qBound(-maximumVelocity, newv.y(), maximumVelocity)); qKSDebug() << "resulting new velocity:" << releaseVelocity; } 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) { Q_Q(QKineticScroller); QPointF deltaPixel = position - lastPosition; qint64 deltaTime = timestamp - lastTimestamp; if (axisLockThreshold) { int dx = qAbs(deltaPixel.x()); int dy = qAbs(deltaPixel.y()); if (dx || dy) { bool vertical = (dy > dx); qreal alpha = qreal(vertical ? dx : dy) / qreal(vertical ? dy : dx); //qKSDebug() << "axis lock: " << alpha << " / " << axisLockThreshold << " - isvertical: " << vertical << " - dx: " << dx << " - dy: " << dy; if (alpha <= axisLockThreshold) { if (vertical) deltaPixel.setX(0); else deltaPixel.setY(0); } } } // calculate velocity (if the user would release the mouse NOW) updateVelocity(deltaPixel, deltaTime); // restrict velocity, if content is not scrollable QPointF maxPos = q->maximumContentPosition(); bool canScrollX = maxPos.x() || (overshootPolicy == QKineticScroller::OvershootAlwaysOn); bool canScrollY = maxPos.y() || (overshootPolicy == QKineticScroller::OvershootAlwaysOn); if (!canScrollX) { deltaPixel.setX(0); releaseVelocity.setX(0); } if (!canScrollY) { deltaPixel.setY(0); releaseVelocity.setY(0); } // if (firstDrag) { // // Do not delay the first drag // setContentPositionHelper(q->contentPosition() - overshootDistance - deltaPixel); // dragDistance = QPointF(0, 0); // } else { dragDistance += deltaPixel; // } if (canScrollX) lastPosition.setX(position.x()); if (canScrollY) lastPosition.setY(position.y()); lastTimestamp = timestamp; } bool QKineticScrollerPrivate::pressWhileInactive(QKineticScroller::Input, const QPointF &position, qint64 timestamp) { Q_Q(QKineticScroller); if ((q->maximumContentPosition() > qreal(0)) || (overshootPolicy == QKineticScroller::OvershootAlwaysOn)) { if (q->canStartScrollingAt(position)) { lastPosition = pressPosition = position; lastTimestamp = pressTimestamp = timestamp; setState(QKineticScroller::StatePressed); } } return false; } bool QKineticScrollerPrivate::releaseWhilePressed(QKineticScroller::Input, const QPointF &, qint64) { if (overshootX || overshootY) setState(QKineticScroller::StateScrolling); else setState(QKineticScroller::StateInactive); return false; } bool QKineticScrollerPrivate::moveWhilePressed(QKineticScroller::Input, const QPointF &position, qint64 timestamp) { Q_Q(QKineticScroller); QPointF deltaPixel = position - pressPosition; bool moveStarted = ((deltaPixel.manhattanLength() / pixelPerMeter) > dragStartDistance); if (moveStarted) { qreal deltaXtoY = qAbs(pressPosition.x() - position.x()) - qAbs(pressPosition.y() - position.y()); deltaXtoY /= pixelPerMeter; QPointF maxPos = q->maximumContentPosition(); bool canScrollX = (maxPos.x() > 0); bool canScrollY = (maxPos.y() > 0); if (overshootPolicy == QKineticScroller::OvershootAlwaysOn) canScrollX = canScrollY = true; if (deltaXtoY < 0) { if (!canScrollY && (!canScrollX || (-deltaXtoY >= dragStartDirectionErrorMargin))) moveStarted = false; } else { if (!canScrollX && (!canScrollY || (deltaXtoY >= dragStartDirectionErrorMargin))) moveStarted = false; } } if (moveStarted) { q->cancelPress(pressPosition); setState(QKineticScroller::StateDragging); // subtract the dragStartDistance deltaPixel = deltaPixel - deltaPixel * (dragStartDistance / deltaPixel.manhattanLength()); if (!deltaPixel.isNull()) { // handleDrag updates lastPosition, lastTimestamp and velocity handleDrag(pressPosition + deltaPixel, timestamp); } } return moveStarted; } bool QKineticScrollerPrivate::moveWhileDragging(QKineticScroller::Input, const QPointF &position, qint64 timestamp) { // handleDrag updates lastPosition, lastTimestamp and velocity handleDrag(position, timestamp); return true; } void QKineticScrollerPrivate::timerEventWhileDragging() { if (!dragDistance.isNull()) { qDebug() << "timerEventWhileDragging" << dragDistance; setContentPositionHelper(-dragDistance); dragDistance = QPointF(0, 0); } } bool QKineticScrollerPrivate::releaseWhileDragging(QKineticScroller::Input, const QPointF &, qint64 timestamp) { Q_Q(QKineticScroller); // calculate the fastSwipe velocity QPointF maxPos = q->maximumContentPosition(); QPointF fastSwipeVelocity = QPoint(0, 0); QSizeF size = q->viewportSize(); if (size.width()) fastSwipeVelocity.setX(qMin(maximumVelocity, maxPos.x() / size.width() * fastSwipeBaseVelocity)); if (size.height()) fastSwipeVelocity.setY(qMin(maximumVelocity, maxPos.y() / size.height() * fastSwipeBaseVelocity)); if (fastSwipeMaximumTime && ((timestamp - pressTimestamp) < qint64(fastSwipeMaximumTime * 1000)) && (oldVelocity > fastSwipeMinimumVelocity)) { // more than one fast swipe in a row: add fastSwipeVelocity int signX = 0, signY = 0; if (releaseVelocity.x()) signX = (releaseVelocity.x() > 0) == (oldVelocity.x() > 0) ? 1 : -1; if (releaseVelocity.y()) signY = (releaseVelocity.y() > 0) == (oldVelocity.y() > 0) ? 1 : -1; releaseVelocity.setX(signX * (oldVelocity.x() + (oldVelocity.x() > 0 ? fastSwipeVelocity.x() : -fastSwipeVelocity.x()))); releaseVelocity.setY(signY * (oldVelocity.y() + (oldVelocity.y() > 0 ? fastSwipeVelocity.y() : -fastSwipeVelocity.y()))); } else if (releaseVelocity >= minimumVelocity) { // if we have a fast swipe, accelerate it to the fastSwipe velocity if ((qAbs(releaseVelocity.x()) > maximumNonAcceleratedVelocity) && (fastSwipeVelocity.x() > maximumNonAcceleratedVelocity)) { releaseVelocity.setX(releaseVelocity.x() > 0 ? fastSwipeVelocity.x() : -fastSwipeVelocity.x()); } if ((qAbs(releaseVelocity.y()) > maximumNonAcceleratedVelocity) && (fastSwipeVelocity.y() > maximumNonAcceleratedVelocity)) { releaseVelocity.setY(releaseVelocity.y() > 0 ? fastSwipeVelocity.y() : -fastSwipeVelocity.y()); } } qKSDebug() << "release While dragging, velocity: "<= minimumVelocity) setState(QKineticScroller::StateScrolling); else setState(QKineticScroller::StateInactive); return true; } void QKineticScrollerPrivate::timerEventWhileScrolling() { qreal deltaTime = qreal(scrollRelativeTimer.restart()) / 1000; qreal time = qreal(scrollAbsoluteTimer.elapsed()) / 1000; QPointF newVelocity = calculateVelocity(time); QPointF deltaPos = newVelocity * deltaTime * pixelPerMeter; // -- move (convert from [m/s] to [pix/frame] if (!deltaPos.isNull()) setContentPositionHelper(deltaPos); qWarning() << "DeltaPos: " << deltaPos << " - newVel: " << newVelocity << " - Time: " << time << " - PPM: " << pixelPerMeter; if (newVelocity.isNull() || (releaseVelocity.isNull() && !scrollToX && !scrollToY && !overshootX && !overshootY)) // if (newVelocity.isNull()) setState(QKineticScroller::StateInactive); } bool QKineticScrollerPrivate::pressWhileScrolling(QKineticScroller::Input, const QPointF &, qint64) { setState(QKineticScroller::StatePressed); return true; } void QKineticScrollerPrivate::setState(QKineticScroller::State newstate) { Q_Q(QKineticScroller); if (state == newstate) return; qWarning() << "Switching to state " << stateName(newstate); switch (newstate) { case QKineticScroller::StateInactive: if (state == QKineticScroller::StateScrolling) { if (timerId) { killTimer(timerId); timerId = 0; } else { qWarning() << "State change from " << stateName(state) << " to " << stateName(newstate) << ", but timer is not active."; } } releaseVelocity = QPointF(0, 0); break; case QKineticScroller::StatePressed: if (timerId) { killTimer(timerId); timerId = 0; } scrollToX = false; scrollToY = false; oldVelocity = releaseVelocity; // releaseVelocity = QPointF(0, 0); break; case QKineticScroller::StateDragging: dragDistance = QPointF(0, 0); if (state == QKineticScroller::StatePressed) { if (!timerId) { timerId = startTimer(1000 / framesPerSecond); } else { qWarning() << "State change from " << stateName(state) << " to " << stateName(newstate) << ", but timer is already active."; } } overshootPosition /= overshootDragResistanceFactor; break; case QKineticScroller::StateScrolling: if (!timerId) { timerId = startTimer(1000 / framesPerSecond); } scrollRelativeTimer.start(); scrollAbsoluteTimer.start(); if (state == QKineticScroller::StateDragging) { overshootPosition *= overshootDragResistanceFactor; overshootStartTimeX = overshootStartTimeY = qreal(scrollAbsoluteTimer.elapsed()) / 1000 - M_PI / (overshootSpringConstantRoot * 2); overshootVelocity = overshootPosition / pixelPerMeter * overshootSpringConstantRoot; } break; } qSwap(state, newstate); q->stateChanged(newstate); } /*! Starts scrolling the widget so that the point \a pos is visible inside the viewport. If the specified point cannot be reached, the contents are scrolled to the nearest valid position. The scrolling speed will be calculated so that the given position will be reached after a platform-defined time span (1 second for Maemo 5). The final speed at the end position is not guaranteed to be zero. \sa ensureVisible(), maximumContentPosition() */ void QKineticScroller::scrollTo(const QPointF &pos, int scrollTime) { Q_D(QKineticScroller); if (scrollTime <= 0) scrollTime = 1; qreal time = qreal(scrollTime) / 1000; if ((pos == contentPosition()) || (d->state == QKineticScroller::StatePressed) || (d->state == QKineticScroller::StateDragging)) { return; } // estimate the minimal start velocity // 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; qKSDebug() << "QAbstractKineticScroller::scrollTo v, dis(" << vMin << ", " << distMin; QPointF v = QPointF((pos.x()-contentPosition().x()) / distMin * vMin, (pos.y()-contentPosition().y()) / distMin * vMin); */ // 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) + C) - lDF / 2 * t ^ 2 // // pos(time) = pos - contentsPos() // vstart = ((lDF / 2) * time ^ 2 + (pos - contentPos())) / (eDB ^ time / ln(eDB) + C) // (for C = -1/ln(eDB) ) QPointF scrollDir(qSign(pos.x()-contentPosition().x()), qSign(pos.y()-contentPosition().y())); QPointF v = (scrollDir * (d->linearDecelerationFactor / qreal(2)) * qreal(time) * qreal(time) + (pos - contentPosition()) / d->pixelPerMeter); if (d->exponentialDecelerationBase != qreal(1)) v /= (qPow(d->exponentialDecelerationBase, time) - 1) / qLn(d->exponentialDecelerationBase); else v /= time; qKSDebug() << "QAbstractKineticScroller::scrollTo pos:" << pos << "v:" << v <<"minV:"<linearDecelerationFactor <<" 1: "<< (qreal(2) * qreal(time) * qreal(scrollTime)) << " 2: " << (pos - contentPosition()) << " 3: " << qLn(d->exponentialDecelerationBase) << "4: " << qPow(d->exponentialDecelerationBase, time) << " 5: "<<((qPow(d->exponentialDecelerationBase, time) - 1) / qLn(d->exponentialDecelerationBase)); // start the scrolling d->scrollToPosition = pos; d->scrollToX = true; d->scrollToY = true; d->releaseVelocity = v; d->setState(QKineticScroller::StateScrolling); } /*! Starts scrolling the widget so that the point \a pos is visible inside the viewport with margins specified in pixels by \a xmargin and \a ymargin. If the specified point cannot be reached, the contents are scrolled to the nearest valid position. The default value for both margins is 50 pixels. This function performs the actual scrolling by calling scrollTo(). \sa maximumContentPosition() */ void QKineticScroller::ensureVisible(const QPointF &pos, int xmargin, int ymargin, int scrollTime) { QSizeF visible = viewportSize(); QPointF currentPos = contentPosition(); qKSDebug() << "QAbstractKineticScroller::ensureVisible(" << pos << ", " << xmargin << ", " << ymargin << ") - position: " << contentPosition(); QRectF posRect(pos.x() - xmargin, pos.y() - ymargin, 2 * xmargin, 2 * ymargin); QRectF visibleRect(currentPos, visible); if (visibleRect.contains(posRect)) return; QPointF newPos = currentPos; if (posRect.top() < visibleRect.top()) newPos.setY(posRect.top()); else if (posRect.bottom() > visibleRect.bottom()) newPos.setY(posRect.bottom() - visible.height()); if (posRect.left() < visibleRect.left()) newPos.setX(posRect.left()); else if (posRect.right() > visibleRect.right()) newPos.setY(posRect.right() - visible.width()); scrollTo(newPos, scrollTime); } /*! \internal Helps when setting the content position. It will try to move the content by the requested delta but stop in case when we are coming back from an overshoot or a scrollTo. It will also indicate a new overshooting condition by the overshootX and oversthootY flags. In this cases it will reset the velocity variables and other flags. Also keeps track of the current over-shooting value in overshootPosition. \deltaPos is the amout of pixels the current content position should be moved */ void QKineticScrollerPrivate::setContentPositionHelper(const QPointF &deltaPos) { Q_Q(QKineticScroller); QPointF oldPos = q->contentPosition() + overshootPosition; QPointF newPos = oldPos + deltaPos; QPointF maxPos = q->maximumContentPosition(); qDebug() << "setContentPositionHelper overshoot:"<setContentPosition(newClampedPos, realOvershootDistance); qDebug() << "setContentPositionHelper" << newClampedPos << " overshoot:" << realOvershootDistance << "Overshoot: "<