[NUI] Add VelocityTracker
authorjoogab.yun <joogab.yun@samsung.com>
Wed, 20 Mar 2024 07:09:17 +0000 (16:09 +0900)
committerbshsqa <32317749+bshsqa@users.noreply.github.com>
Mon, 13 May 2024 07:08:49 +0000 (16:08 +0900)
This is a utility that calculates velocity from consecutive touch coordinates.

src/Tizen.NUI/src/public/Utility/AccumulatingVelocityTrackerStrategy.cs [new file with mode: 0644]
src/Tizen.NUI/src/public/Utility/LeastSquaresVelocityTrackerStrategy.cs [new file with mode: 0644]
src/Tizen.NUI/src/public/Utility/VelocityTracker.cs [new file with mode: 0644]
src/Tizen.NUI/src/public/Utility/VelocityTrackerStrategy.cs [new file with mode: 0644]
test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/VelocityTrackerSample.cs [new file with mode: 0755]

diff --git a/src/Tizen.NUI/src/public/Utility/AccumulatingVelocityTrackerStrategy.cs b/src/Tizen.NUI/src/public/Utility/AccumulatingVelocityTrackerStrategy.cs
new file mode 100644 (file)
index 0000000..c79132c
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * 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();
+        }
+    }
+}
diff --git a/src/Tizen.NUI/src/public/Utility/LeastSquaresVelocityTrackerStrategy.cs b/src/Tizen.NUI/src/public/Utility/LeastSquaresVelocityTrackerStrategy.cs
new file mode 100644 (file)
index 0000000..3a5f31e
--- /dev/null
@@ -0,0 +1,314 @@
+/*
+ * 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;
+        }
+    }
+}
diff --git a/src/Tizen.NUI/src/public/Utility/VelocityTracker.cs b/src/Tizen.NUI/src/public/Utility/VelocityTracker.cs
new file mode 100644 (file)
index 0000000..962d143
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+ * 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;
+        }
+    }
+}
diff --git a/src/Tizen.NUI/src/public/Utility/VelocityTrackerStrategy.cs b/src/Tizen.NUI/src/public/Utility/VelocityTrackerStrategy.cs
new file mode 100644 (file)
index 0000000..800d1a0
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * 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; }
+    }
+}
diff --git a/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/VelocityTrackerSample.cs b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/VelocityTrackerSample.cs
new file mode 100755 (executable)
index 0000000..153d55f
--- /dev/null
@@ -0,0 +1,329 @@
+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();
+          }
+      }
+  }
+}