/**************************************************************************** ** ** 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 /*! \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) { } 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(OvershootMaximumVelocity, qreal(247.0 / 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; QElapsedTimer *elapsedTimer; typedef void (QKineticScrollerPrivate::*timerhandler_t)(qint64 elapsed); timerhandler_t handler; }; timerevent timerevents[] = { { QKineticScroller::StateDragging, &dragTimer, &QKineticScrollerPrivate::timerEventWhileDragging }, { QKineticScroller::StateScrolling, &scrollTimer, &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); 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->overshootSpringConstant; 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 OvershootMaximumVelocity: return d->overshootMaximumVelocity; 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->overshootSpringConstant = 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 OvershootMaximumVelocity: d->overshootMaximumVelocity = value.toReal(); 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: " << velocity << "[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) + velocity * inversSmoothingFactor; // newv = newv * dragVelocitySmoothingFactor + velocity * (qreal(1) - dragVelocitySmoothingFactor); if (deltaPixel.x()) velocity.setX(qBound(-maximumVelocity, newv.x(), maximumVelocity)); if (deltaPixel.y()) velocity.setY(qBound(-maximumVelocity, newv.y(), maximumVelocity)); qKSDebug() << "resulting new velocity:" << 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); velocity.setX(0); } if (!canScrollY) { deltaPixel.setY(0); velocity.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; } 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) { 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 (!overshootDistance.isNull()) 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(qint64) { Q_Q(QKineticScroller); if (!dragDistance.isNull()) { setContentPositionHelper(q->contentPosition() + overshootDistance - 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 (velocity.x()) signX = (velocity.x() > 0) == (oldVelocity.x() > 0) ? 1 : -1; if (velocity.y()) signY = (velocity.y() > 0) == (oldVelocity.y() > 0) ? 1 : -1; velocity.setX(signX * (oldVelocity.x() + (oldVelocity.x() > 0 ? fastSwipeVelocity.x() : -fastSwipeVelocity.x()))); velocity.setY(signY * (oldVelocity.y() + (oldVelocity.y() > 0 ? fastSwipeVelocity.y() : -fastSwipeVelocity.y()))); } else if (velocity >= minimumVelocity) { // if we have a fast swipe, accelerate it to the fastSwipe velocity if ((qAbs(velocity.x()) > maximumNonAcceleratedVelocity) && (fastSwipeVelocity.x() > maximumNonAcceleratedVelocity)) { velocity.setX(velocity.x() > 0 ? fastSwipeVelocity.x() : -fastSwipeVelocity.x()); } if ((qAbs(velocity.y()) > maximumNonAcceleratedVelocity) && (fastSwipeVelocity.y() > maximumNonAcceleratedVelocity)) { velocity.setY(velocity.y() > 0 ? fastSwipeVelocity.y() : -fastSwipeVelocity.y()); } } qKSDebug() << "release While dragging, velocity: "<= minimumVelocity) setState(QKineticScroller::StateScrolling); else setState(QKineticScroller::StateInactive); return true; } qreal QKineticScrollerPrivate::decelerate(qreal v, qreal expDecel, qreal linDecel) { v *= expDecel; if (qAbs(v) >= linDecel) return v + (v < 0 ? linDecel : -linDecel); else return 0;; } 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; // -- 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; } } 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)); } if (overshootDistance.isNull() && (velocity < qreal(0.5) * qreal(framesPerSecond) / pixelPerMeter /* 1 [pix/frame] */) && !scrollToX && !scrollToY) setState(QKineticScroller::StateInactive); } bool QKineticScrollerPrivate::pressWhileScrolling(QKineticScroller::Input, const QPointF &, qint64) { oldVelocity = velocity; 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 (state) { case QKineticScroller::StateDragging: overshootDistance *= overshootDragResistanceFactor; break; default: break; } 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."; } } velocity = QPointF(0, 0); break; case QKineticScroller::StatePressed: if (timerId) { killTimer(timerId); timerId = 0; } scrollToX = false; scrollToY = false; velocity = 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."; } } dragTimer.start(); break; case QKineticScroller::StateScrolling: // if (state == QKineticScroller::StatePressed) { if (!timerId) { timerId = startTimer(1000 / framesPerSecond); } // } scrollTimer.start(); 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); 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) - lDF / (2 * t ^ 2) // // pos(scrollTime) = pos - contentsPos() // 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); 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; d->scrollToY = true; d->velocity = 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); } /* 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) { Q_Q(QKineticScroller); QPointF maxPos = q->maximumContentPosition(); QPointF clampedPos; clampedPos.setX(qBound(qreal(0), pos.x(), maxPos.x())); clampedPos.setY(qBound(qreal(0), pos.y(), maxPos.y())); // -- stop scroll to if hitting boundary if (clampedPos.x() != pos.x()) scrollToX = false; if (clampedPos.y() != pos.y()) scrollToY = false; 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 x = qBound(-overshootMaximumDistance.x() * pixelPerMeter, overshootX, overshootMaximumDistance.x() * pixelPerMeter); if (x != overshootX) velocity.setX(0); overshootDistance.setX(x); qKSDebug() << "Overshoot y: "<<(overshootMaximumDistance.y() * pixelPerMeter)<<"distance: "<setContentPosition(clampedPos, realOvershootDistance); } /*! \enum QKineticScroller::OvershootPolicy This enum describes the various modes of overshooting. \value OvershootWhenScrollable Overshooting is when the content is scrollable. This is the default. \value OvershootAlwaysOff Overshooting is never enabled (even when the content is scrollable). \value OvershootAlwaysOn Overshooting is always enabled (even when the content is not scrollable). */ /*! If kinetic scrolling can be started at the given content's \a position, this function needs to return true; otherwise it needs to return false. The default value is true, regardless of \a position. */ bool QKineticScroller::canStartScrollingAt(const QPointF &position) const { Q_UNUSED(position); return true; } /*! Since a mouse press is always delivered normally when the scroller is in the StateInactive state, we may need to cancel it as soon as the user has moved the mouse far enough to actually start a kinetic scroll operation. The \a pressPosition parameter can be used to find out which widget (or graphics item) received the mouse press in the first place. Subclasses may choose to simulate a fake mouse release event for that widget (or graphics item), preferably \bold not within its boundaries. The default implementation does nothing. */ void QKineticScroller::cancelPress(const QPointF &pressPosition) { Q_UNUSED(pressPosition); } /*! This function get called whenever the state of the kinetic scroller changes. The old state is supplied as \a oldState, while the new state is returned by calling state(). The default implementation does nothing. \sa state() */ void QKineticScroller::stateChanged(State oldState) { Q_UNUSED(oldState); } /*! \fn QPointF QKineticScroller::maximumContentPosition() const Returns the maximum valid content position. The minimum is always \c (0,0). \sa scrollTo() */ /*! \fn QSizeF QKineticScroller::viewportSize() const Returns the size of the currently visible content positions. In the case where an QAbstractScrollArea is used, this is equivalent to the viewport() size. \sa scrollTo() */ /*! \fn QPointF QKineticScroller::contentPosition() const \brief Returns the current position of the content. Note that overshooting is not considered to be "real" scrolling so the position might be (0,0) even if the user is currently dragging the widget outside the "normal" maximumContentPosition(). \sa maximumContentPosition() */ /*! \fn void QKineticScroller::setContentPosition(const QPointF &position, const QPointF &overshoot) Set the content's \a position. This parameter will always be in the valid range QPointF(0, 0) and maximumContentPosition(). In the case where overshooting is required, the \a overshoot parameter will give the direction and the absolute distance to overshoot. \sa maximumContentPosition() */ QT_END_NAMESPACE