This is a utility that calculates velocity from consecutive touch coordinates.
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Modified by Joogab Yun(joogab.yun@samsung.com)
+ */
+
+using System.Collections.Generic;
+using System.ComponentModel;
+
+namespace Tizen.NUI.Utility
+{
+ /// <summary>
+ /// Accumulating Velocity Tracker
+ /// </sumary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public class AccumulatingVelocityTrackerStrategy : VelocityTrackerStrategy
+ {
+ /// <summary>
+ /// Positions and event time information
+ /// </summary>
+ protected struct Movement
+ {
+ public uint EventTime;
+ public float Position;
+ public Movement(uint pEventTime, float pPosition)
+ {
+ EventTime = pEventTime;
+ Position = pPosition;
+ }
+ };
+
+ private const int mHistorySize = 20;
+ private uint mMaximumTime;
+ private uint mLastEventTime = 0;
+ private float mLastPosition = 0;
+ private uint mAssumePointerStoppedTime = 40; // 40ms
+ protected SortedDictionary<int, List<Movement>> mMovements;
+
+
+ /// <summary>
+ /// Create an instance of AccumulatingVelocityTrackerStrategy.
+ /// </sumary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public AccumulatingVelocityTrackerStrategy(uint maximumTime) : base()
+ {
+ mMaximumTime = maximumTime;
+ mMovements = new SortedDictionary<int, List<Movement>>();
+ }
+
+ /// <summary>
+ /// Adds movement information
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override void AddMovement(uint eventTime, int pointerId, float position)
+ {
+ if (!mMovements.ContainsKey(pointerId))
+ {
+ mMovements.Add(pointerId, new List<Movement>());
+ }
+
+ List<Movement> movements = mMovements[pointerId];
+ int size = movements.Count;
+ if (size > 0)
+ {
+ if (eventTime - mLastEventTime > mAssumePointerStoppedTime)
+ {
+ mMovements.Clear();
+ return;
+ }
+
+ if (mLastPosition == position)
+ {
+ return;
+ }
+
+ if (movements[size - 1].EventTime == eventTime)
+ {
+ movements.RemoveAt(size - 1);
+ }
+
+ if (size > mHistorySize)
+ {
+ movements.RemoveAt(0);
+ }
+ }
+
+
+ mLastEventTime = eventTime;
+ mLastPosition = position;
+ movements.Add(new Movement(eventTime, position));
+
+ // Clear moves that are not among the latest moves.
+ while (movements.Count > 0 && eventTime - movements[0].EventTime > mMaximumTime)
+ {
+ movements.RemoveAt(0);
+ }
+ }
+
+ /// <summary>
+ /// Resets the velocity tracker state.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override void Clear()
+ {
+ mMovements.Clear();
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Modified by Joogab Yun(joogab.yun@samsung.com)
+ */
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+
+namespace Tizen.NUI.Utility
+{
+ /*
+ * Velocity tracker algorithm based on least-squares linear regression.
+ */
+ internal class LeastSquaresVelocityTrackerStrategy : AccumulatingVelocityTrackerStrategy
+ {
+ public enum Weighting
+ {
+ None, // No weights applied. All data points are equally reliable.
+ Delta, // Weight by time delta. Data points clustered together are weighted less.
+ Central, // Weight such that points within a certain horizon are weighed more than those outside of that horizon.
+ Recent // Weight such that points older than a certain amount are weighed less.
+ }
+
+ private const int mMaximumTime = 100; //100ms
+ private int mDegree;
+ private Weighting mWeighting;
+
+ /// <summary>
+ /// Create an instance of LeastSquaresVelocityTrackerStrategy.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public LeastSquaresVelocityTrackerStrategy(int degree, Weighting weighting = Weighting.Delta) : base(mMaximumTime)
+ {
+ mDegree = degree;
+ mWeighting = weighting;
+ }
+
+ private T[] GetSlice<T>(T[,] array, int row)
+ {
+ int cols = array.GetLength(1);
+ var slice = new T[cols];
+ for (int col = 0; col < cols; col++)
+ {
+ slice[col] = array[row, col];
+ }
+ return slice;
+ }
+
+ private float vectorDot(float[] a, float[] b, int m)
+ {
+ float r = 0;
+ for (int i = 0; i < m; i++)
+ {
+ r += a[i] * b[i];
+ }
+ return r;
+ }
+
+ private float vectorNorm(float[] a, int m)
+ {
+ float r = 0;
+ for (int i = 0; i < m; i++)
+ {
+ r += a[i] * a[i];
+ }
+ return (float)Math.Sqrt((double)r);
+ }
+
+ /// <summary>
+ /// Retrieve the last computed velocity
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override float? GetVelocity(int pointerId)
+ {
+ if (!mMovements.ContainsKey(pointerId)) return null;
+
+ List<Movement> movements = mMovements[pointerId];
+ int size = movements.Count;
+ if (size == 0) return null;
+
+ int degree = mDegree;
+ if (degree > size - 1)
+ {
+ degree = size - 1;
+ }
+
+ if (degree <= 0) return null;
+
+ if (degree == 2 && mWeighting == Weighting.None)
+ {
+ return solveUnweightedLeastSquaresDeg2(movements);
+ }
+
+ List<float> positions = new List<float>();
+ List<float> w = new List<float>();
+ List<float> time = new List<float>();
+ Movement newestMovement = movements[size - 1];
+ for (int i = size - 1; i >= 0; i--)
+ {
+ Movement movement = movements[i];
+ uint age = newestMovement.EventTime - movement.EventTime;
+ positions.Add(movement.Position);
+ w.Add(chooseWeight(pointerId, i));
+ time.Add(-age);
+ }
+ return solveLeastSquares(time, positions, w, degree + 1);
+ }
+
+ private float chooseWeight(int pointerId, int index)
+ {
+ List<Movement> movements = mMovements[pointerId];
+ int size = movements.Count;
+ switch (mWeighting)
+ {
+ case Weighting.Delta:
+ {
+
+ if (index == size - 1)
+ {
+ return 1.0f;
+ }
+ float deltaMillis =
+ (movements[index + 1].EventTime - movements[index].EventTime);
+
+ if (deltaMillis < 0)
+ {
+ return 0.5f;
+ }
+ if (deltaMillis < 10)
+ {
+ return 0.5f + deltaMillis * 0.05f;
+ }
+ return 1.0f;
+ }
+ case Weighting.Central:
+ {
+ // Weight points based on their age, weighing very recent and very old points less.
+ // age 0ms: 0.5
+ // age 10ms: 1.0
+ // age 50ms: 1.0
+ // age 60ms: 0.5
+ float ageMillis =
+ (movements[size - 1].EventTime - movements[index].EventTime);
+ if (ageMillis < 0)
+ {
+ return 0.5f;
+ }
+ if (ageMillis < 10)
+ {
+ return 0.5f + ageMillis * 0.05f;
+ }
+ if (ageMillis < 50)
+ {
+ return 1.0f;
+ }
+ if (ageMillis < 60)
+ {
+ return 0.5f + (60 - ageMillis) * 0.05f;
+ }
+ return 0.5f;
+ }
+ case Weighting.Recent:
+ {
+ // Weight points based on their age, weighing older points less.
+ // age 0ms: 1.0
+ // age 50ms: 1.0
+ // age 100ms: 0.5
+ float ageMillis =
+ (movements[size - 1].EventTime - movements[index].EventTime);
+ if (ageMillis < 50)
+ {
+ return 1.0f;
+ }
+ if (ageMillis < 100)
+ {
+ return 0.5f + (100 - ageMillis) * 0.01f;
+ }
+ return 0.5f;
+ }
+ case Weighting.None:
+ return 1.0f;
+ }
+ return 0.0f;
+ }
+
+ private float? solveLeastSquares(List<float> x, List<float> y, List<float> w, int n)
+ {
+ int m = x.Count;
+ float[,] a = new float[n, m];
+ for (int h = 0; h < m; h++)
+ {
+ a[0, h] = w[h];
+ for (int i = 1; i < n; i++)
+ {
+ a[i, h] = a[i - 1, h] * x[h];
+ }
+ }
+
+ float[,] q = new float[n, m];
+ float[,] r = new float[n, m];
+ for (int j = 0; j < n; j++)
+ {
+ for (int h = 0; h < m; h++)
+ {
+ q[j, h] = a[j, h];
+ }
+ for (int i = 0; i < j; i++)
+ {
+
+ float dot = vectorDot(GetSlice(q, j), GetSlice(q, i), m);
+ for (int h = 0; h < m; h++)
+ {
+ q[j, h] -= dot * q[i, h];
+ }
+ }
+ float norm = vectorNorm(GetSlice(q, j), m);
+ if (norm < 0.000001f)
+ {
+ return null;
+ }
+ float invNorm = 1.0f / norm;
+ for (int h = 0; h < m; h++)
+ {
+ q[j, h] *= invNorm;
+ }
+ for (int i = 0; i < n; i++)
+ {
+ r[j, i] = i < j ? 0 : vectorDot(GetSlice(q, j), GetSlice(a, i), m);
+ }
+ }
+
+ float[] wy = new float[m];
+ for (int h = 0; h < m; h++)
+ {
+ wy[h] = y[h] * w[h];
+ }
+ float[] outB = new float[5];
+ for (int i = n; i != 0;)
+ {
+ i--;
+ outB[i] = vectorDot(GetSlice(q, i), wy, m);
+ for (int j = n - 1; j > i; j--)
+ {
+ outB[i] -= r[i, j] * outB[j];
+ }
+ outB[i] /= r[i, i];
+ }
+ float ymean = 0.0f;
+ for (int h = 0; h < m; h++)
+ {
+ ymean += y[h];
+ }
+ ymean /= m;
+ return outB[1];
+ }
+
+ // Velocity tracker algorithm based on least-squares linear regression.
+ private float? solveUnweightedLeastSquaresDeg2(List<Movement> movements)
+ {
+ float sxi = 0, sxiyi = 0, syi = 0, sxi2 = 0, sxi3 = 0, sxi2yi = 0, sxi4 = 0;
+ int count = movements.Count;
+ Movement newestMovement = movements[count - 1];
+ for (int i = 0; i < count; i++)
+ {
+ Movement movement = movements[i];
+ uint age = newestMovement.EventTime - movement.EventTime;
+ float xi = -age;
+ float yi = movement.Position;
+ float xi2 = xi * xi;
+ float xi3 = xi2 * xi;
+ float xi4 = xi3 * xi;
+ float xiyi = xi * yi;
+ float xi2yi = xi2 * yi;
+ sxi += xi;
+ sxi2 += xi2;
+ sxiyi += xiyi;
+ sxi2yi += xi2yi;
+ syi += yi;
+ sxi3 += xi3;
+ sxi4 += xi4;
+ }
+ float Sxx = sxi2 - sxi * sxi / count;
+ float Sxy = sxiyi - sxi * syi / count;
+ float Sxx2 = sxi3 - sxi * sxi2 / count;
+ float Sx2y = sxi2yi - sxi2 * syi / count;
+ float Sx2x2 = sxi4 - sxi2 * sxi2 / count;
+ float denominator = Sxx * Sx2x2 - Sxx2 * Sxx2;
+ if (denominator == 0)
+ {
+ return null;
+ }
+ return (Sxy * Sx2x2 - Sx2y * Sxx2) / denominator;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Modified by Joogab Yun(joogab.yun@samsung.com)
+ */
+
+using System.Collections.Generic;
+using System.ComponentModel;
+
+namespace Tizen.NUI.Utility
+{
+ /// <summary>
+ /// Calculates the velocity of touch movements over time.
+ /// <example>
+ /// private void OnPanGestureDetected(object source, PanGestureDetector.DetectedEventArgs e)
+ /// {
+ /// tracker.AddMovement(e.PanGesture.ScreenPosition, e.PanGesture.Time);
+ /// if (e.PanGesture.State == Gesture.StateType.Started)
+ /// {
+ /// }
+ /// else if (e.PanGesture.State == Gesture.StateType.Continuing)
+ /// {
+ /// }
+ /// else if (e.PanGesture.State == Gesture.StateType.Finished || e.PanGesture.State == Gesture.StateType.Cancelled)
+ /// {
+ /// float panVelocity = (ScrollingDirection == Direction.Horizontal) ? tracker.GetVelocity().X : tracker.GetVelocity().Y;
+ /// tracker.Clear();
+ /// }
+ /// }
+ /// </example>
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public class VelocityTracker
+ {
+ private struct Axis
+ {
+ public static int X = 0;
+ public static int Y = 1;
+ }
+
+ private struct ComputedVelocity
+ {
+ private Dictionary<int, Dictionary<int, float>> mVelocities;
+
+ public float? GetVelocity(int axis, int id)
+ {
+ if (mVelocities.ContainsKey(axis) == false)
+ return null;
+ else if (mVelocities[axis].ContainsKey(id) == false)
+ return null;
+ return mVelocities[axis][id];
+ }
+
+ public void AddVelocity(int axis, int id, float velocity)
+ {
+ if (mVelocities.ContainsKey(axis) == false)
+ mVelocities[axis] = new Dictionary<int, float>();
+ mVelocities[axis][id] = velocity;
+ }
+
+ public ComputedVelocity(Dictionary<int, Dictionary<int, float>> velocities)
+ {
+ mVelocities = velocities;
+ }
+ }
+
+ private int mPointerCount = 0;
+ private bool mIsComputed = false;
+ private ComputedVelocity mComputedVelocity;
+ private VelocityTrackerStrategy[] mConfiguredStrategies;
+
+ /// <summary>
+ /// Creates a velocity tracker using the specified strategy.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public VelocityTracker(int degree = 2)
+ {
+ mConfiguredStrategies = new LeastSquaresVelocityTrackerStrategy[2];
+ for(int i = 0; i < 2; i++)
+ {
+ mConfiguredStrategies[i] = new LeastSquaresVelocityTrackerStrategy(degree);
+ }
+ }
+
+ public VelocityTracker(VelocityTrackerStrategy[] strategy)
+ {
+ mConfiguredStrategies = strategy;
+ }
+
+ /// <summary>
+ /// Adds movement information
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public void AddMovement(Vector2 position, uint eventTime)
+ {
+ if (mConfiguredStrategies != null && mConfiguredStrategies.Length > 1)
+ {
+ mConfiguredStrategies[Axis.X]?.AddMovement(eventTime, mPointerCount, position.X);
+ mConfiguredStrategies[Axis.Y]?.AddMovement(eventTime, mPointerCount, position.Y);
+ mIsComputed = false;
+ }
+ }
+
+ private static float Clamp(float value, float min, float max)
+ {
+ return value < min ? min : value > max ? max : value;
+ }
+
+ /// <summary>
+ /// Compute the current velocity based on the points that have been collected.
+ /// </summary>
+ /// <param name="units">The units you would like the velocity in. A value of 1 provides units per millisecond, 1000 provides units per second.</param>
+ /// <param name="maxVelocity">The maximum velocity that can be computed by this method.</param>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public void ComputeVelocity(int units, float maxVelocity)
+ {
+ mComputedVelocity = new ComputedVelocity(new Dictionary<int, Dictionary<int, float>>());
+ for (int axis = 0; axis < mConfiguredStrategies?.Length; axis++)
+ {
+ for (int j = 0; j <= mPointerCount; j++)
+ {
+ float? velocity = mConfiguredStrategies[axis]?.GetVelocity(j);
+ if (velocity != null)
+ {
+ float adjustedVelocity = Clamp((float)velocity * units / 1000, -maxVelocity, maxVelocity);
+ mComputedVelocity.AddVelocity(axis, j, adjustedVelocity);
+ }
+ }
+ }
+ mIsComputed = true;
+ }
+
+ private float GetVelocity(int axis, int id)
+ {
+ if (axis < 0 || axis > 1) return 0.0f;
+ float? value = mComputedVelocity.GetVelocity(axis, id);
+ return value.HasValue ? value.Value : 0.0f;
+ }
+
+ /// <summary>
+ /// Retrieve the last computed velocity.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Vector2 GetVelocity()
+ {
+ if (mIsComputed == false)
+ ComputeVelocity(1000, 100);
+ float velocityX = GetVelocity(Axis.X, mPointerCount);
+ float velocityY = GetVelocity(Axis.Y, mPointerCount);
+ return new Vector2(velocityX, velocityY);
+ }
+
+ /// <summary>
+ /// Resets the velocity tracker state.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public void Clear()
+ {
+ for(int i = 0; i < mConfiguredStrategies?.Length; i++)
+ {
+ mConfiguredStrategies[i]?.Clear();
+ }
+ mPointerCount = 0;
+ mIsComputed = false;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 Samsung Electronics Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+using System.Collections.Generic;
+using System.ComponentModel;
+
+namespace Tizen.NUI.Utility
+{
+
+ /// <summary>
+ /// VelocityTrackerStrategy
+ /// </sumary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public abstract class VelocityTrackerStrategy
+ {
+ /// <summary>
+ /// Resets the velocity tracker state.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public virtual void Clear() { }
+
+ /// <summary>
+ /// Adds movement information
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public virtual void AddMovement(uint eventTime, int pointerId, float position) { }
+
+ /// <summary>
+ /// Retrieve the last computed velocity
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public virtual float? GetVelocity(int pointerId) { return 0.0f; }
+ }
+}
--- /dev/null
+using Tizen.NUI;
+using Tizen.NUI.BaseComponents;
+using Tizen.NUI.Components;
+using Tizen.NUI.Events;
+using Tizen.NUI.Utility;
+using System.Collections.Generic;
+
+namespace Tizen.NUI.Samples
+{
+ public class VelocityTrackerSample : IExample
+ {
+ private View myView;
+ private PanGestureDetector panGestureDetector, panGestureDetector1;
+
+ Tizen.NUI.Utility.VelocityTracker tracker;
+ Tizen.NUI.Utility.VelocityTracker customTracker;
+
+ private ImageView panView, panView1;
+ private readonly int ImageSize = 24;
+ private static readonly string imagePath = Tizen.Applications.Application.Current.DirectoryInfo.Resource + "/images/Dali/CubeTransitionEffect/";
+
+ private readonly string[] ResourceUrl = {
+ imagePath + "gallery-large-9.jpg",
+ imagePath + "gallery-large-5.jpg",
+ };
+
+ private Animation flickAnimation = new Animation();
+ private Animation flickAnimation1 = new Animation();
+
+ public void Activate()
+ {
+ Init();
+ }
+
+ private void Init()
+ {
+
+ // You can use custom trackers created by yourself
+ Tizen.NUI.Utility.VelocityTrackerStrategy[] strategy = new CustomVelocityTracker[2];
+ for(int i=0 ; i<2; i++)
+ {
+ strategy[i] = new CustomVelocityTracker();
+ }
+ customTracker = new Tizen.NUI.Utility.VelocityTracker(strategy);
+
+ // you can use default tracker
+ tracker = new Tizen.NUI.Utility.VelocityTracker();
+
+
+ Window.Instance.BackgroundColor = Color.White;
+
+ panGestureDetector = new PanGestureDetector();
+ panGestureDetector1 = new PanGestureDetector();
+
+ myView = new View()
+ {
+ WidthResizePolicy = ResizePolicyType.FillToParent,
+ HeightResizePolicy = ResizePolicyType.FillToParent,
+ BackgroundColor = Color.White
+ };
+ Window.Instance.Add(myView);
+
+ panView = new ImageView()
+ {
+ Size2D = new Size2D(200, 200),
+ Position2D = new Position2D(100, 100),
+ ResourceUrl = ResourceUrl[0],
+ };
+ myView.Add(panView);
+ panGestureDetector.Attach(panView);
+
+ panView1 = new ImageView()
+ {
+ Size2D = new Size2D(200, 200),
+ Position2D = new Position2D(400, 100),
+ ResourceUrl = ResourceUrl[1],
+ };
+ myView.Add(panView1);
+ panGestureDetector1.Attach(panView1);
+
+
+ panGestureDetector.Attach(panView);
+ panGestureDetector.Detected += OnPanGestureDetector;
+
+ panGestureDetector1.Attach(panView1);
+ panGestureDetector1.Detected += OnPanGestureDetector1;
+
+ }
+
+ public void Deactivate()
+ {
+ }
+
+ private void OnPanGestureDetector(object source, PanGestureDetector.DetectedEventArgs e)
+ {
+ if (e.View is ImageView view)
+ {
+ flickAnimation.Stop();
+ flickAnimation.Clear();
+
+ if (e.PanGesture.State == Gesture.StateType.Started)
+ {
+ tracker.Clear();
+ tracker.AddMovement(e.PanGesture.ScreenPosition, e.PanGesture.Time);
+ Vector2 move = e.PanGesture.ScreenDisplacement;
+ view.Position += new Position(move.X, move.Y);
+ }
+ else if (e.PanGesture.State == Gesture.StateType.Continuing)
+ {
+ tracker.AddMovement(e.PanGesture.ScreenPosition, e.PanGesture.Time);
+ Vector2 move = e.PanGesture.ScreenDisplacement;
+ view.Position += new Position(move.X, move.Y);
+ }
+ else if (e.PanGesture.State == Gesture.StateType.Finished)
+ {
+ Vector2 move = e.PanGesture.ScreenDisplacement;
+ view.Position += new Position(move.X, move.Y);
+
+ int distanceX = (int)(tracker.GetVelocity().X * 100);
+ int distanceY = (int)(tracker.GetVelocity().Y * 100);
+
+ Tizen.Log.Error("NUI", $"tracker : {tracker.GetVelocity().X}, {tracker.GetVelocity().Y}, {e.PanGesture.ScreenVelocity.X}, {e.PanGesture.ScreenVelocity.Y}\n");
+
+ flickAnimation.AnimateBy(view, "PositionX", distanceX, 0, 300);
+ flickAnimation.AnimateBy(view, "PositionY", distanceY, 0, 300);
+ flickAnimation.Play();
+ }
+ }
+ }
+
+ private void OnPanGestureDetector1(object source, PanGestureDetector.DetectedEventArgs e)
+ {
+ if (e.View is ImageView view)
+ {
+ flickAnimation1.Stop();
+ flickAnimation1.Clear();
+
+ if (e.PanGesture.State == Gesture.StateType.Started)
+ {
+ customTracker.Clear();
+ customTracker.AddMovement(e.PanGesture.ScreenPosition, e.PanGesture.Time);
+
+ Vector2 move = e.PanGesture.ScreenDisplacement;
+ view.Position += new Position(move.X, move.Y);
+ }
+ else if (e.PanGesture.State == Gesture.StateType.Continuing)
+ {
+ customTracker.AddMovement(new Vector2(e.PanGesture.ScreenPosition), e.PanGesture.Time);
+
+ Vector2 move = e.PanGesture.ScreenDisplacement;
+ view.Position += new Position(move.X, move.Y);
+ }
+ else if (e.PanGesture.State == Gesture.StateType.Finished)
+ {
+ Vector2 move = e.PanGesture.ScreenDisplacement;
+ view.Position += new Position(move.X, move.Y);
+
+ Tizen.Log.Error("NUI", $"customTracker : {customTracker.GetVelocity().X}, {customTracker.GetVelocity().Y}, {e.PanGesture.ScreenVelocity.X}, {e.PanGesture.ScreenVelocity.Y}\n");
+
+ int distanceX = (int)(customTracker.GetVelocity().X * 100);
+ int distanceY = (int)(customTracker.GetVelocity().Y * 100);
+
+ flickAnimation1.AnimateBy(view, "PositionX", distanceX, 0, 300);
+ flickAnimation1.AnimateBy(view, "PositionY", distanceY, 0, 300);
+ flickAnimation1.Play();
+ }
+ }
+ }
+ }
+
+ public class CustomVelocityTracker : Tizen.NUI.Utility.VelocityTrackerStrategy
+ {
+ private uint mHistorySize = 20;
+ private uint mMaximumTime = 100;
+ private uint mLastEventTime = 0;
+ private uint mAssumePointerStoppedTime = 40; // 40ms
+ private Strategy mStrategy;
+ private uint mMinDuration = 10; //10ms
+ private List<Data> mMovements = new List<Data>();
+
+ public enum Strategy
+ {
+ Legacy,
+ LSQ2,
+ Default = Legacy
+ }
+
+ public struct Data
+ {
+ public float Position {get; set;}
+ public uint EventTime {get; set;}
+
+ public Data(float position, uint time)
+ {
+ Position = position;
+ EventTime = time;
+ }
+ }
+
+ public CustomVelocityTracker(Strategy strategy = Strategy.Default)
+ {
+ mStrategy = strategy;
+ }
+
+ public override void AddMovement(uint time, int pointerId, float position)
+ {
+ int size = mMovements.Count;
+ if(size > 0)
+ {
+ if(time - mLastEventTime > mAssumePointerStoppedTime)
+ {
+ mMovements.Clear();
+ return;
+ }
+
+ if(mLastEventTime == time)
+ {
+ mMovements.RemoveAt(size-1);
+ }
+
+ if(size > mHistorySize)
+ {
+ mMovements.RemoveAt(0);
+ }
+ }
+
+ mLastEventTime = time;
+ Data data = new Data(position, time);
+ mMovements.Add(data);
+
+ while(mMovements.Count > 0 && time - mMovements[0].EventTime > mMaximumTime)
+ {
+ mMovements.RemoveAt(0);
+ }
+ }
+
+ public override void Clear()
+ {
+ mMovements.Clear();
+ }
+
+ private float GetVelocityLSQ2()
+ {
+ int count = mMovements.Count;
+ if(count > 1)
+ {
+ // Solving y = a*x^2 + b*x + c, where
+ // - "x" is age (i.e. duration since latest movement) of the movemnets
+ // - "y" is positions of the movements.
+ float sxi = 0, sxiyi = 0, syi = 0, sxi2 = 0, sxi3 = 0, sxi2yi = 0, sxi4 = 0;
+
+ Data newestMovement = mMovements[count - 1];
+ for (int i = 0; i < count; i++) {
+ Data movement = mMovements[i];
+ float age = newestMovement.EventTime - movement.EventTime;
+ float xi = -age;
+ float yi = movement.Position;
+ float xi2 = xi*xi;
+ float xi3 = xi2*xi;
+ float xi4 = xi3*xi;
+ float xiyi = xi*yi;
+ float xi2yi = xi2*yi;
+ sxi += xi;
+ sxi2 += xi2;
+ sxiyi += xiyi;
+ sxi2yi += xi2yi;
+ syi += yi;
+ sxi3 += xi3;
+ sxi4 += xi4;
+ }
+ float Sxx = sxi2 - sxi*sxi / count;
+ float Sxy = sxiyi - sxi*syi / count;
+ float Sxx2 = sxi3 - sxi*sxi2 / count;
+ float Sx2y = sxi2yi - sxi2*syi / count;
+ float Sx2x2 = sxi4 - sxi2*sxi2 / count;
+ float denominator = Sxx*Sx2x2 - Sxx2*Sxx2;
+ if (denominator == 0) {
+ return 0.0f;
+ }
+ return (Sxy * Sx2x2 - Sx2y * Sxx2) / (denominator );
+ }
+ return 0.0f;
+ }
+
+ private float GetVelocityLegacy()
+ {
+ int size = mMovements.Count;
+ if(size > 0)
+ {
+ int oldestIndex = 0;
+ float accumV = 0;
+ bool samplesUsed = false;
+ Data oldestMovement = mMovements[oldestIndex];
+ float oldestPosition = oldestMovement.Position;
+ float lastDuration = 0;
+ for (int i = oldestIndex; i < size; i++) {
+ Data movement = mMovements[i];
+ float duration = movement.EventTime - oldestMovement.EventTime;
+ if (duration >= mMinDuration) {
+ float position = movement.Position;
+ float scale = 1.0f / duration; // one over time delta in seconds
+ float v = (position - oldestPosition) * scale;
+ accumV = (accumV * lastDuration + v * duration) / (duration + lastDuration);
+ lastDuration = duration;
+ samplesUsed = true;
+ }
+ }
+ if (samplesUsed) {
+ return accumV;
+ }
+ }
+ return 0.0f;
+ }
+
+
+
+ public override float? GetVelocity(int pointerId)
+ {
+ switch(mStrategy)
+ {
+ case Strategy.LSQ2:
+ return GetVelocityLSQ2();
+ case Strategy.Legacy:
+ default:
+ return GetVelocityLegacy();
+ }
+ }
+ }
+}