1 /*
2  *  Copyright (C) 2011-2018 Team Kodi
3  *  This file is part of Kodi - https://kodi.tv
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *  See LICENSES/README.md for more information.
7  */
8 
9 
10 #include "InertialScrollingHandler.h"
11 
12 #include "Application.h"
13 #include "ServiceBroker.h"
14 #include "guilib/GUIComponent.h"
15 #include "guilib/GUIWindowManager.h"
16 #include "input/Key.h"
17 #include "input/touch/generic/GenericTouchInputHandler.h"
18 #include "utils/TimeUtils.h"
19 #include "utils/log.h"
20 #include "windowing/WinSystem.h"
21 
22 #include <cmath>
23 #include <numeric>
24 
25 // time for reaching velocity 0 in secs
26 #define TIME_TO_ZERO_SPEED 1.0f
27 // minimum speed for doing inertial scroll is 100 pixels / s
28 #define MINIMUM_SPEED_FOR_INERTIA 200
29 // maximum speed for reducing time to zero
30 #define MAXIMUM_SPEED_FOR_REDUCTION 750
31 // maximum time between last movement and gesture end in ms to consider as moving
32 #define MAXIMUM_DELAY_FOR_INERTIA 200
33 
CInertialScrollingHandler()34 CInertialScrollingHandler::CInertialScrollingHandler() : m_iLastGesturePoint(CPoint(0, 0))
35 {
36 }
37 
TimeElapsed() const38 unsigned int CInertialScrollingHandler::PanPoint::TimeElapsed() const
39 {
40   return CTimeUtils::GetFrameTime() - time;
41 }
42 
CheckForInertialScrolling(const CAction * action)43 bool CInertialScrollingHandler::CheckForInertialScrolling(const CAction* action)
44 {
45   bool ret = false; // return value - false no inertial scrolling - true - inertial scrolling
46 
47   if (CServiceBroker::GetWinSystem()->HasInertialGestures())
48   {
49     return ret; // no need for emulating inertial scrolling - windowing does support it natively.
50   }
51 
52   // reset screensaver during pan
53   if (action->GetID() == ACTION_GESTURE_PAN)
54   {
55     g_application.ResetScreenSaver();
56     if (!m_bScrolling)
57     {
58       m_panPoints.emplace_back(CTimeUtils::GetFrameTime(),
59                                CVector{action->GetAmount(4), action->GetAmount(5)});
60     }
61     return false;
62   }
63 
64   // mouse click aborts scrolling
65   if (m_bScrolling && action->GetID() == ACTION_MOUSE_LEFT_CLICK)
66   {
67     ret = true;
68     m_bAborting = true; // lets abort
69   }
70 
71   // trim saved pan points to time range that qualifies for inertial scrolling
72   while (!m_panPoints.empty() && m_panPoints.front().TimeElapsed() > MAXIMUM_DELAY_FOR_INERTIA)
73     m_panPoints.pop_front();
74 
75   // on begin/tap stop all inertial scrolling
76   if (action->GetID() == ACTION_GESTURE_BEGIN)
77   {
78     // release any former exclusive mouse mode
79     // for making switching between multiple lists
80     // possible
81     CGUIMessage message(GUI_MSG_EXCLUSIVE_MOUSE, 0, 0);
82     CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message);
83     m_bScrolling = false;
84     // wakeup screensaver on pan begin
85     g_application.ResetScreenSaver();
86     g_application.WakeUpScreenSaverAndDPMS();
87   }
88   else if (action->GetID() == ACTION_GESTURE_END &&
89            !m_panPoints.empty()) // do we need to animate inertial scrolling?
90   {
91     // Calculate velocity in the last MAXIMUM_DELAY_FOR_INERTIA milliseconds.
92     // Do not use the velocity given by the ACTION_GESTURE_END data - it is calculated
93     // for the whole duration of the touch and thus useless for inertia. The user
94     // may scroll around for a few seconds and then only at the end flick in one
95     // direction. Only the last flick should be relevant here.
96     auto velocitySum =
97         std::accumulate(m_panPoints.cbegin(), m_panPoints.cend(), CVector{},
98                         [](CVector val, PanPoint const& p) { return val + p.velocity; });
99     auto velocityX = velocitySum.x / m_panPoints.size();
100     auto velocityY = velocitySum.y / m_panPoints.size();
101 
102     m_timeToZero = TIME_TO_ZERO_SPEED;
103     auto velocityMax = std::max(std::abs(velocityX), std::abs(velocityY));
104 #ifdef TARGET_DARWIN_OSX
105     float dpiScale = 1.0;
106 #else
107     float dpiScale = CGenericTouchInputHandler::GetInstance().GetScreenDPI() / 160.0f;
108 #endif
109     if (velocityMax > MINIMUM_SPEED_FOR_INERTIA * dpiScale)
110     {
111       if (velocityMax < MAXIMUM_SPEED_FOR_REDUCTION * dpiScale)
112         m_timeToZero = (m_timeToZero * velocityMax) / (MAXIMUM_SPEED_FOR_REDUCTION * dpiScale);
113 
114       bool inertialRequested = false;
115       CGUIMessage message(GUI_MSG_GESTURE_NOTIFY, 0, 0, static_cast<int>(velocityX),
116                           static_cast<int>(velocityY));
117 
118       // ask if the control wants inertial scrolling
119       if (CServiceBroker::GetGUI()->GetWindowManager().SendMessage(message))
120       {
121         int result = 0;
122         if (message.GetPointer())
123         {
124           int* p = static_cast<int*>(message.GetPointer());
125           message.SetPointer(nullptr);
126           result = *p;
127           delete p;
128         }
129         if (result == EVENT_RESULT_PAN_HORIZONTAL || result == EVENT_RESULT_PAN_VERTICAL)
130         {
131           inertialRequested = true;
132         }
133       }
134 
135       if (inertialRequested)
136       {
137         m_iFlickVelocity.x = velocityX; // in pixels per sec
138         m_iFlickVelocity.y = velocityY; // in pixels per sec
139         m_iLastGesturePoint.x = action->GetAmount(2); // last gesture point x
140         m_iLastGesturePoint.y = action->GetAmount(3); // last gesture point y
141 
142         // calc deacceleration for fullstop in TIME_TO_ZERO_SPEED secs
143         // v = a*t + v0 -> set v = 0 because we want to stop scrolling
144         // a = -v0 / t
145         m_inertialDeacceleration.x = -1 * m_iFlickVelocity.x / m_timeToZero;
146         m_inertialDeacceleration.y = -1 * m_iFlickVelocity.y / m_timeToZero;
147 
148         m_inertialStartTime = CTimeUtils::GetFrameTime(); // start time of inertial scrolling
149         ret = true;
150         m_bScrolling = true; // activate the inertial scrolling animation
151       }
152     }
153   }
154 
155   if (action->GetID() == ACTION_GESTURE_BEGIN || action->GetID() == ACTION_GESTURE_END ||
156       action->GetID() == ACTION_GESTURE_ABORT)
157   {
158     m_panPoints.clear();
159   }
160 
161   return ret;
162 }
163 
ProcessInertialScroll(float frameTime)164 bool CInertialScrollingHandler::ProcessInertialScroll(float frameTime)
165 {
166   // do inertial scroll animation by sending gesture_pan
167   if (m_bScrolling)
168   {
169     float xMovement = 0.0;
170     float yMovement = 0.0;
171 
172     // decrease based on negative acceleration
173     // calc the overall inertial scrolling time in secs
174     float absoluteInertialTime = (CTimeUtils::GetFrameTime() - m_inertialStartTime) / (float)1000;
175 
176     // as long as we aren't over the overall inertial scroll time - do the deacceleration
177     if (absoluteInertialTime < m_timeToZero)
178     {
179       // v = s/t -> s = t * v
180       xMovement = frameTime * m_iFlickVelocity.x;
181       yMovement = frameTime * m_iFlickVelocity.y;
182 
183       // save new gesture point
184       m_iLastGesturePoint.x += xMovement;
185       m_iLastGesturePoint.y += yMovement;
186 
187       // fire the pan action
188       if (!g_application.OnAction(CAction(ACTION_GESTURE_PAN, 0, m_iLastGesturePoint.x,
189                                           m_iLastGesturePoint.y, xMovement, yMovement,
190                                           m_iFlickVelocity.x, m_iFlickVelocity.y)))
191       {
192         m_bAborting = true; // we are done
193       }
194 
195       // calc new velocity based on deacceleration
196       // v = a*t + v0
197       m_iFlickVelocity.x = m_inertialDeacceleration.x * frameTime + m_iFlickVelocity.x;
198       m_iFlickVelocity.y = m_inertialDeacceleration.y * frameTime + m_iFlickVelocity.y;
199 
200       // check if the signs are equal - which would mean we deaccelerated to long and reversed the
201       // direction
202       if ((m_inertialDeacceleration.x < 0) == (m_iFlickVelocity.x < 0))
203       {
204         m_iFlickVelocity.x = 0;
205       }
206       if ((m_inertialDeacceleration.y < 0) == (m_iFlickVelocity.y < 0))
207       {
208         m_iFlickVelocity.y = 0;
209       }
210     }
211     else // no movement -> done
212     {
213       m_bAborting = true; // we are done
214     }
215   }
216 
217   // if we are done - or we where aborted
218   if (m_bAborting)
219   {
220     // fire gesture end action
221     g_application.OnAction(CAction(ACTION_GESTURE_END, 0, 0.0f, 0.0f, 0.0f, 0.0f));
222     m_bAborting = false;
223     m_bScrolling = false; // stop scrolling
224     m_iFlickVelocity.x = 0;
225     m_iFlickVelocity.y = 0;
226   }
227 
228   return true;
229 }
230