Home | History | Annotate | Download | only in platform
      1 /*
      2  * Copyright (c) 2010, Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 #include "config.h"
     32 
     33 #if ENABLE(SMOOTH_SCROLLING)
     34 
     35 #include "ScrollAnimatorWin.h"
     36 
     37 #include "FloatPoint.h"
     38 #include "ScrollableArea.h"
     39 #include "ScrollbarTheme.h"
     40 #include <algorithm>
     41 #include <wtf/CurrentTime.h>
     42 #include <wtf/PassOwnPtr.h>
     43 
     44 namespace WebCore {
     45 
     46 PassOwnPtr<ScrollAnimator> ScrollAnimator::create(ScrollableArea* scrollableArea)
     47 {
     48     return adoptPtr(new ScrollAnimatorWin(scrollableArea));
     49 }
     50 
     51 const double ScrollAnimatorWin::animationTimerDelay = 0.01;
     52 
     53 ScrollAnimatorWin::PerAxisData::PerAxisData(ScrollAnimatorWin* parent, float* currentPos)
     54     : m_currentPos(currentPos)
     55     , m_desiredPos(0)
     56     , m_currentVelocity(0)
     57     , m_desiredVelocity(0)
     58     , m_lastAnimationTime(0)
     59     , m_animationTimer(parent, &ScrollAnimatorWin::animationTimerFired)
     60 {
     61 }
     62 
     63 
     64 ScrollAnimatorWin::ScrollAnimatorWin(ScrollableArea* scrollableArea)
     65     : ScrollAnimator(scrollableArea)
     66     , m_horizontalData(this, &m_currentPosX)
     67     , m_verticalData(this, &m_currentPosY)
     68 {
     69 }
     70 
     71 ScrollAnimatorWin::~ScrollAnimatorWin()
     72 {
     73     stopAnimationTimerIfNeeded(&m_horizontalData);
     74     stopAnimationTimerIfNeeded(&m_verticalData);
     75 }
     76 
     77 bool ScrollAnimatorWin::scroll(ScrollbarOrientation orientation, ScrollGranularity granularity, float step, float multiplier)
     78 {
     79     // Don't animate jumping to the beginning or end of the document.
     80     if (granularity == ScrollByDocument)
     81         return ScrollAnimator::scroll(orientation, granularity, step, multiplier);
     82 
     83     // This is an animatable scroll.  Calculate the scroll delta.
     84     PerAxisData* data = (orientation == VerticalScrollbar) ? &m_verticalData : &m_horizontalData;
     85     float newPos = std::max(std::min(data->m_desiredPos + (step * multiplier), static_cast<float>(m_scrollableArea->scrollSize(orientation))), 0.0f);
     86     if (newPos == data->m_desiredPos)
     87         return false;
     88     data->m_desiredPos = newPos;
     89 
     90     // Calculate the animation velocity.
     91     if (*data->m_currentPos == data->m_desiredPos)
     92         return false;
     93     bool alreadyAnimating = data->m_animationTimer.isActive();
     94     // There are a number of different sources of scroll requests.  We want to
     95     // make both keyboard and wheel-generated scroll requests (which can come at
     96     // unpredictable rates) and autoscrolling from holding down the mouse button
     97     // on a scrollbar part (where the request rate can be obtained from the
     98     // scrollbar theme) feel smooth, responsive, and similar.
     99     //
    100     // When autoscrolling, the scrollbar's autoscroll timer will call us to
    101     // increment the desired position by |step| (with |multiplier| == 1) every
    102     // ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() seconds.  If we set
    103     // the desired velocity to exactly this rate, smooth scrolling will neither
    104     // race ahead (and then have to slow down) nor increasingly lag behind, but
    105     // will be smooth and synchronized.
    106     //
    107     // Note that because of the acceleration period, the current position in
    108     // this case would lag the desired one by a small, constant amount (see
    109     // comments on animateScroll()); the exact amount is given by
    110     //   lag = |step| - v(0.5tA + tD)
    111     // Where
    112     //   v = The steady-state velocity,
    113     //       |step| / ScrollbarTheme::nativeTheme()->autoscrollTimerDelay()
    114     //   tA = accelerationTime()
    115     //   tD = The time we pretend has already passed when starting to scroll,
    116     //        |animationTimerDelay|
    117     //
    118     // This lag provides some buffer against timer jitter so we're less likely
    119     // to hit the desired position and stop (and thus have to re-accelerate,
    120     // causing a visible hitch) while waiting for the next autoscroll increment.
    121     //
    122     // Thus, for autoscroll-timer-triggered requests, the ideal steady-state
    123     // distance to travel in each time interval is:
    124     //   float animationStep = step;
    125     // Note that when we're not already animating, this is exactly the same as
    126     // the distance to the target position.  We'll return to that in a moment.
    127     //
    128     // For keyboard and wheel scrolls, we don't know when the next increment
    129     // will be requested.  If we set the target velocity based on how far away
    130     // from the target position we are, then for keyboard/wheel events that come
    131     // faster than the autoscroll delay, we'll asymptotically approach the
    132     // velocity needed to stay smoothly in sync with the user's actions; for
    133     // events that come slower, we'll scroll one increment and then pause until
    134     // the next event fires.
    135     float animationStep = fabs(newPos - *data->m_currentPos);
    136     // If a key is held down (or the wheel continually spun), then once we have
    137     // reached a velocity close to the steady-state velocity, we're likely to
    138     // hit the desired position at around the same time we'd expect the next
    139     // increment to occur -- bad because it leads to hitching as described above
    140     // (if autoscroll-based requests didn't result in a small amount of constant
    141     // lag).  So if we're called again while already animating, we want to trim
    142     // the animationStep slightly to maintain lag like what's described above.
    143     // (I say "maintain" since we'll already be lagged due to the acceleration
    144     // during the first scroll period.)
    145     //
    146     // Remember that trimming won't cause us to fall steadily further behind
    147     // here, because the further behind we are, the larger the base step value
    148     // above.  Given the scrolling algorithm in animateScroll(), the practical
    149     // effect will actually be that, assuming a constant trim factor, we'll lag
    150     // by a constant amount depending on the rate at which increments occur
    151     // compared to the autoscroll timer delay.  The exact lag is given by
    152     //   lag = |step| * ((r / k) - 1)
    153     // Where
    154     //   r = The ratio of the autoscroll repeat delay,
    155     //       ScrollbarTheme::nativeTheme()->autoscrollTimerDelay(), to the
    156     //       key/wheel repeat delay (i.e. > 1 when keys repeat faster)
    157     //   k = The velocity trim constant given below
    158     //
    159     // We want to choose the trim factor such that for calls that come at the
    160     // autoscroll timer rate, we'll wind up with the same lag as in the
    161     // "perfect" case described above (or, to put it another way, we'll end up
    162     // with |animationStep| == |step| * |multiplier| despite the actual distance
    163     // calculated above being larger than that).  This will result in "perfect"
    164     // behavior for autoscrolling without having to special-case it.
    165     if (alreadyAnimating)
    166         animationStep /= (2.0 - ((1.0 / ScrollbarTheme::nativeTheme()->autoscrollTimerDelay()) * (0.5 * accelerationTime() + animationTimerDelay)));
    167     // The result of all this is that single keypresses or wheel flicks will
    168     // scroll in the same time period as single presses of scrollbar elements;
    169     // holding the mouse down on a scrollbar part will scroll as fast as
    170     // possible without hitching; and other repeated scroll events will also
    171     // scroll with the same time lag as holding down the mouse on a scrollbar
    172     // part.
    173     data->m_desiredVelocity = animationStep / ScrollbarTheme::nativeTheme()->autoscrollTimerDelay();
    174 
    175     // If we're not already scrolling, start.
    176     if (!alreadyAnimating)
    177         animateScroll(data);
    178     return true;
    179 }
    180 
    181 void ScrollAnimatorWin::scrollToOffsetWithoutAnimation(const FloatPoint& offset)
    182 {
    183     stopAnimationTimerIfNeeded(&m_horizontalData);
    184     stopAnimationTimerIfNeeded(&m_verticalData);
    185 
    186     *m_horizontalData.m_currentPos = offset.x();
    187     m_horizontalData.m_desiredPos = offset.x();
    188     m_horizontalData.m_currentVelocity = 0;
    189     m_horizontalData.m_desiredVelocity = 0;
    190 
    191     *m_verticalData.m_currentPos = offset.y();
    192     m_verticalData.m_desiredPos = offset.y();
    193     m_verticalData.m_currentVelocity = 0;
    194     m_verticalData.m_desiredVelocity = 0;
    195 
    196     notityPositionChanged();
    197 }
    198 
    199 double ScrollAnimatorWin::accelerationTime()
    200 {
    201     // We elect to use ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() as
    202     // the length of time we'll take to accelerate from 0 to our target
    203     // velocity.  Choosing a larger value would produce a more pronounced
    204     // acceleration effect.
    205     return ScrollbarTheme::nativeTheme()->autoscrollTimerDelay();
    206 }
    207 
    208 void ScrollAnimatorWin::animationTimerFired(Timer<ScrollAnimatorWin>* timer)
    209 {
    210     animateScroll((timer == &m_horizontalData.m_animationTimer) ? &m_horizontalData : &m_verticalData);
    211 }
    212 
    213 void ScrollAnimatorWin::stopAnimationTimerIfNeeded(PerAxisData* data)
    214 {
    215     if (data->m_animationTimer.isActive())
    216         data->m_animationTimer.stop();
    217 }
    218 
    219 void ScrollAnimatorWin::animateScroll(PerAxisData* data)
    220 {
    221     // Note on smooth scrolling perf versus non-smooth scrolling perf:
    222     // The total time to perform a complete scroll is given by
    223     //   t = t0 + 0.5tA - tD + tS
    224     // Where
    225     //   t0 = The time to perform the scroll without smooth scrolling
    226     //   tA = The acceleration time,
    227     //        ScrollbarTheme::nativeTheme()->autoscrollTimerDelay() (see below)
    228     //   tD = |animationTimerDelay|
    229     //   tS = A value less than or equal to the time required to perform a
    230     //        single scroll increment, i.e. the work done due to calling
    231     //        client()->valueChanged() (~0 for simple pages, larger for complex
    232     //        pages).
    233     //
    234     // Because tA and tD are fairly small, the total lag (as users perceive it)
    235     // is negligible for simple pages and roughly tS for complex pages.  Without
    236     // knowing in advance how large tS is it's hard to do better than this.
    237     // Perhaps we could try to remember previous values and forward-compensate.
    238 
    239 
    240     // We want to update the scroll position based on the time it's been since
    241     // our last update.  This may be longer than our ideal time, especially if
    242     // the page is complex or the system is slow.
    243     //
    244     // To avoid feeling laggy, if we've just started smooth scrolling we pretend
    245     // we've already accelerated for one ideal interval, so that we'll scroll at
    246     // least some distance immediately.
    247     double lastScrollInterval = data->m_currentVelocity ? (WTF::currentTime() - data->m_lastAnimationTime) : animationTimerDelay;
    248 
    249     // Figure out how far we've actually traveled and update our current
    250     // velocity.
    251     float distanceTraveled;
    252     if (data->m_currentVelocity < data->m_desiredVelocity) {
    253         // We accelerate at a constant rate until we reach the desired velocity.
    254         float accelerationRate = data->m_desiredVelocity / accelerationTime();
    255 
    256         // Figure out whether contant acceleration has caused us to reach our
    257         // target velocity.
    258         float potentialVelocityChange = accelerationRate * lastScrollInterval;
    259         float potentialNewVelocity = data->m_currentVelocity + potentialVelocityChange;
    260         if (potentialNewVelocity > data->m_desiredVelocity) {
    261             // We reached the target velocity at some point between our last
    262             // update and now.  The distance traveled can be calculated in two
    263             // pieces: the distance traveled while accelerating, and the
    264             // distance traveled after reaching the target velocity.
    265             float actualVelocityChange = data->m_desiredVelocity - data->m_currentVelocity;
    266             float accelerationInterval = actualVelocityChange / accelerationRate;
    267             // The distance traveled under constant acceleration is the area
    268             // under a line segment with a constant rising slope.  Break this
    269             // into a triangular portion atop a rectangular portion and sum.
    270             distanceTraveled = ((data->m_currentVelocity + (actualVelocityChange / 2)) * accelerationInterval);
    271             // The distance traveled at the target velocity is simply
    272             // (target velocity) * (remaining time after accelerating).
    273             distanceTraveled += (data->m_desiredVelocity * (lastScrollInterval - accelerationInterval));
    274             data->m_currentVelocity = data->m_desiredVelocity;
    275         } else {
    276             // Constant acceleration through the entire time interval.
    277             distanceTraveled = (data->m_currentVelocity + (potentialVelocityChange / 2)) * lastScrollInterval;
    278             data->m_currentVelocity = potentialNewVelocity;
    279         }
    280     } else {
    281         // We've already reached the target velocity, so the distance we've
    282         // traveled is simply (current velocity) * (elapsed time).
    283         distanceTraveled = data->m_currentVelocity * lastScrollInterval;
    284         // If our desired velocity has decreased, drop the current velocity too.
    285         data->m_currentVelocity = data->m_desiredVelocity;
    286     }
    287 
    288     // Now update the scroll position based on the distance traveled.
    289     if (distanceTraveled >= fabs(data->m_desiredPos - *data->m_currentPos)) {
    290         // We've traveled far enough to reach the desired position.  Stop smooth
    291         // scrolling.
    292         *data->m_currentPos = data->m_desiredPos;
    293         data->m_currentVelocity = 0;
    294         data->m_desiredVelocity = 0;
    295     } else {
    296         // Not yet at the target position.  Travel towards it and set up the
    297         // next update.
    298         if (*data->m_currentPos > data->m_desiredPos)
    299             distanceTraveled = -distanceTraveled;
    300         *data->m_currentPos += distanceTraveled;
    301         data->m_animationTimer.startOneShot(animationTimerDelay);
    302         data->m_lastAnimationTime = WTF::currentTime();
    303     }
    304 
    305     notityPositionChanged();
    306 }
    307 
    308 } // namespace WebCore
    309 
    310 #endif // ENABLE(SMOOTH_SCROLLING)
    311