summaryrefslogtreecommitdiffstats
path: root/xbmc/input/InertialScrollingHandler.cpp
blob: 1248722bb69dd2273705e349f6be7dc6f3c73c6f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
/*
 *  Copyright (C) 2011-2018 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#include "InertialScrollingHandler.h"

#include "ServiceBroker.h"
#include "application/Application.h"
#include "application/ApplicationComponents.h"
#include "application/ApplicationPowerHandling.h"
#include "guilib/GUIComponent.h"
#include "guilib/GUIWindowManager.h"
#include "input/actions/Action.h"
#include "input/actions/ActionIDs.h"
#include "input/touch/generic/GenericTouchInputHandler.h"
#include "utils/TimeUtils.h"
#include "windowing/WinSystem.h"

#include <cmath>
#include <numeric>

// time for reaching velocity 0 in secs
#define TIME_TO_ZERO_SPEED 1.0f
// minimum speed for doing inertial scroll is 100 pixels / s
#define MINIMUM_SPEED_FOR_INERTIA 200
// maximum speed for reducing time to zero
#define MAXIMUM_SPEED_FOR_REDUCTION 750
// maximum time between last movement and gesture end in ms to consider as moving
#define MAXIMUM_DELAY_FOR_INERTIA 200

CInertialScrollingHandler::CInertialScrollingHandler() : m_iLastGesturePoint(CPoint(0, 0))
{
}

unsigned int CInertialScrollingHandler::PanPoint::TimeElapsed() const
{
  return CTimeUtils::GetFrameTime() - time;
}

bool CInertialScrollingHandler::CheckForInertialScrolling(const CAction* action)
{
  bool ret = false; // return value - false no inertial scrolling - true - inertial scrolling

  if (CServiceBroker::GetWinSystem()->HasInertialGestures())
  {
    return ret; // no need for emulating inertial scrolling - windowing does support it natively.
  }

  // reset screensaver during pan
  if (action->GetID() == ACTION_GESTURE_PAN)
  {
    auto& components = CServiceBroker::GetAppComponents();
    const auto appPower = components.GetComponent<CApplicationPowerHandling>();
    if (appPower)
      appPower->ResetScreenSaver();
    if (!m_bScrolling)
    {
      m_panPoints.emplace_back(CTimeUtils::GetFrameTime(),
                               CVector{action->GetAmount(4), action->GetAmount(5)});
    }
    return false;
  }

  // mouse click aborts scrolling
  if (m_bScrolling && action->GetID() == ACTION_MOUSE_LEFT_CLICK)
  {
    ret = true;
    m_bAborting = true; // lets abort
  }

  // trim saved pan points to time range that qualifies for inertial scrolling
  while (!m_panPoints.empty() && m_panPoints.front().TimeElapsed() > MAXIMUM_DELAY_FOR_INERTIA)
    m_panPoints.pop_front();

  // on begin/tap stop all inertial scrolling
  if (action->GetID() == ACTION_GESTURE_BEGIN)
  {
    // release any former exclusive mouse mode
    // for making switching between multiple lists
    // possible
    CGUIMessage message(GUI_MSG_EXCLUSIVE_MOUSE, 0, 0);
    CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
    m_bScrolling = false;
    // wakeup screensaver on pan begin
    auto& components = CServiceBroker::GetAppComponents();
    const auto appPower = components.GetComponent<CApplicationPowerHandling>();
    appPower->ResetScreenSaver();
    appPower->WakeUpScreenSaverAndDPMS();
  }
  else if (action->GetID() == ACTION_GESTURE_END &&
           !m_panPoints.empty()) // do we need to animate inertial scrolling?
  {
    // Calculate velocity in the last MAXIMUM_DELAY_FOR_INERTIA milliseconds.
    // Do not use the velocity given by the ACTION_GESTURE_END data - it is calculated
    // for the whole duration of the touch and thus useless for inertia. The user
    // may scroll around for a few seconds and then only at the end flick in one
    // direction. Only the last flick should be relevant here.
    auto velocitySum =
        std::accumulate(m_panPoints.cbegin(), m_panPoints.cend(), CVector{},
                        [](CVector val, PanPoint const& p) { return val + p.velocity; });
    auto velocityX = velocitySum.x / m_panPoints.size();
    auto velocityY = velocitySum.y / m_panPoints.size();

    m_timeToZero = TIME_TO_ZERO_SPEED;
    auto velocityMax = std::max(std::abs(velocityX), std::abs(velocityY));
#ifdef TARGET_DARWIN_OSX
    float dpiScale = 1.0;
#else
    float dpiScale = CGenericTouchInputHandler::GetInstance().GetScreenDPI() / 160.0f;
#endif
    if (velocityMax > MINIMUM_SPEED_FOR_INERTIA * dpiScale)
    {
      if (velocityMax < MAXIMUM_SPEED_FOR_REDUCTION * dpiScale)
        m_timeToZero = (m_timeToZero * velocityMax) / (MAXIMUM_SPEED_FOR_REDUCTION * dpiScale);

      bool inertialRequested = false;
      CGUIMessage message(GUI_MSG_GESTURE_NOTIFY, 0, 0, static_cast<int>(velocityX),
                          static_cast<int>(velocityY));

      // ask if the control wants inertial scrolling
      if (CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message))
      {
        int result = 0;
        if (message.GetPointer())
        {
          int* p = static_cast<int*>(message.GetPointer());
          message.SetPointer(nullptr);
          result = *p;
          delete p;
        }
        if (result == EVENT_RESULT_PAN_HORIZONTAL || result == EVENT_RESULT_PAN_VERTICAL)
        {
          inertialRequested = true;
        }
      }

      if (inertialRequested)
      {
        m_iFlickVelocity.x = velocityX; // in pixels per sec
        m_iFlickVelocity.y = velocityY; // in pixels per sec
        m_iLastGesturePoint.x = action->GetAmount(2); // last gesture point x
        m_iLastGesturePoint.y = action->GetAmount(3); // last gesture point y

        // calc deacceleration for fullstop in TIME_TO_ZERO_SPEED secs
        // v = a*t + v0 -> set v = 0 because we want to stop scrolling
        // a = -v0 / t
        m_inertialDeacceleration.x = -1 * m_iFlickVelocity.x / m_timeToZero;
        m_inertialDeacceleration.y = -1 * m_iFlickVelocity.y / m_timeToZero;

        m_inertialStartTime = CTimeUtils::GetFrameTime(); // start time of inertial scrolling
        ret = true;
        m_bScrolling = true; // activate the inertial scrolling animation
      }
    }
  }

  if (action->GetID() == ACTION_GESTURE_BEGIN || action->GetID() == ACTION_GESTURE_END ||
      action->GetID() == ACTION_GESTURE_ABORT)
  {
    m_panPoints.clear();
  }

  return ret;
}

bool CInertialScrollingHandler::ProcessInertialScroll(float frameTime)
{
  // do inertial scroll animation by sending gesture_pan
  if (m_bScrolling)
  {
    float xMovement = 0.0;
    float yMovement = 0.0;

    // decrease based on negative acceleration
    // calc the overall inertial scrolling time in secs
    float absoluteInertialTime = (CTimeUtils::GetFrameTime() - m_inertialStartTime) / (float)1000;

    // as long as we aren't over the overall inertial scroll time - do the deacceleration
    if (absoluteInertialTime < m_timeToZero)
    {
      // v = s/t -> s = t * v
      xMovement = frameTime * m_iFlickVelocity.x;
      yMovement = frameTime * m_iFlickVelocity.y;

      // save new gesture point
      m_iLastGesturePoint.x += xMovement;
      m_iLastGesturePoint.y += yMovement;

      // fire the pan action
      if (!g_application.OnAction(CAction(ACTION_GESTURE_PAN, 0, m_iLastGesturePoint.x,
                                          m_iLastGesturePoint.y, xMovement, yMovement,
                                          m_iFlickVelocity.x, m_iFlickVelocity.y)))
      {
        m_bAborting = true; // we are done
      }

      // calc new velocity based on deacceleration
      // v = a*t + v0
      m_iFlickVelocity.x = m_inertialDeacceleration.x * frameTime + m_iFlickVelocity.x;
      m_iFlickVelocity.y = m_inertialDeacceleration.y * frameTime + m_iFlickVelocity.y;

      // check if the signs are equal - which would mean we deaccelerated to long and reversed the
      // direction
      if ((m_inertialDeacceleration.x < 0) == (m_iFlickVelocity.x < 0))
      {
        m_iFlickVelocity.x = 0;
      }
      if ((m_inertialDeacceleration.y < 0) == (m_iFlickVelocity.y < 0))
      {
        m_iFlickVelocity.y = 0;
      }
    }
    else // no movement -> done
    {
      m_bAborting = true; // we are done
    }
  }

  // if we are done - or we where aborted
  if (m_bAborting)
  {
    // fire gesture end action
    g_application.OnAction(CAction(ACTION_GESTURE_END, 0, 0.0f, 0.0f, 0.0f, 0.0f));
    m_bAborting = false;
    m_bScrolling = false; // stop scrolling
    m_iFlickVelocity.x = 0;
    m_iFlickVelocity.y = 0;
  }

  return true;
}