From 49f0028300c3627b0ac84dba9184b0f68334c34d Mon Sep 17 00:00:00 2001 From: "joogab.yun" Date: Wed, 20 Mar 2024 16:09:17 +0900 Subject: [PATCH] [NUI] Add VelocityTracker This is a utility that calculates velocity from consecutive touch coordinates. --- .../Utility/AccumulatingVelocityTrackerStrategy.cs | 125 ++++++++ .../Utility/LeastSquaresVelocityTrackerStrategy.cs | 314 ++++++++++++++++++++ .../src/public/Utility/VelocityTracker.cs | 184 ++++++++++++ .../src/public/Utility/VelocityTrackerStrategy.cs | 48 +++ .../Samples/VelocityTrackerSample.cs | 329 +++++++++++++++++++++ 5 files changed, 1000 insertions(+) create mode 100644 src/Tizen.NUI/src/public/Utility/AccumulatingVelocityTrackerStrategy.cs create mode 100644 src/Tizen.NUI/src/public/Utility/LeastSquaresVelocityTrackerStrategy.cs create mode 100644 src/Tizen.NUI/src/public/Utility/VelocityTracker.cs create mode 100644 src/Tizen.NUI/src/public/Utility/VelocityTrackerStrategy.cs create mode 100755 test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/VelocityTrackerSample.cs diff --git a/src/Tizen.NUI/src/public/Utility/AccumulatingVelocityTrackerStrategy.cs b/src/Tizen.NUI/src/public/Utility/AccumulatingVelocityTrackerStrategy.cs new file mode 100644 index 0000000..c79132c --- /dev/null +++ b/src/Tizen.NUI/src/public/Utility/AccumulatingVelocityTrackerStrategy.cs @@ -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 +{ + /// + /// Accumulating Velocity Tracker + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class AccumulatingVelocityTrackerStrategy : VelocityTrackerStrategy + { + /// + /// Positions and event time information + /// + 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> mMovements; + + + /// + /// Create an instance of AccumulatingVelocityTrackerStrategy. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public AccumulatingVelocityTrackerStrategy(uint maximumTime) : base() + { + mMaximumTime = maximumTime; + mMovements = new SortedDictionary>(); + } + + /// + /// Adds movement information + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override void AddMovement(uint eventTime, int pointerId, float position) + { + if (!mMovements.ContainsKey(pointerId)) + { + mMovements.Add(pointerId, new List()); + } + + List 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); + } + } + + /// + /// Resets the velocity tracker state. + /// + [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 index 0000000..3a5f31e --- /dev/null +++ b/src/Tizen.NUI/src/public/Utility/LeastSquaresVelocityTrackerStrategy.cs @@ -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; + + /// + /// Create an instance of LeastSquaresVelocityTrackerStrategy. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public LeastSquaresVelocityTrackerStrategy(int degree, Weighting weighting = Weighting.Delta) : base(mMaximumTime) + { + mDegree = degree; + mWeighting = weighting; + } + + private T[] GetSlice(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); + } + + /// + /// Retrieve the last computed velocity + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override float? GetVelocity(int pointerId) + { + if (!mMovements.ContainsKey(pointerId)) return null; + + List 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 positions = new List(); + List w = new List(); + List time = new List(); + 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 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 x, List y, List 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 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 index 0000000..962d143 --- /dev/null +++ b/src/Tizen.NUI/src/public/Utility/VelocityTracker.cs @@ -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 +{ + /// + /// Calculates the velocity of touch movements over time. + /// + /// 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(); + /// } + /// } + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class VelocityTracker + { + private struct Axis + { + public static int X = 0; + public static int Y = 1; + } + + private struct ComputedVelocity + { + private Dictionary> 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(); + mVelocities[axis][id] = velocity; + } + + public ComputedVelocity(Dictionary> velocities) + { + mVelocities = velocities; + } + } + + private int mPointerCount = 0; + private bool mIsComputed = false; + private ComputedVelocity mComputedVelocity; + private VelocityTrackerStrategy[] mConfiguredStrategies; + + /// + /// Creates a velocity tracker using the specified strategy. + /// + [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; + } + + /// + /// Adds movement information + /// + [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; + } + + /// + /// Compute the current velocity based on the points that have been collected. + /// + /// The units you would like the velocity in. A value of 1 provides units per millisecond, 1000 provides units per second. + /// The maximum velocity that can be computed by this method. + [EditorBrowsable(EditorBrowsableState.Never)] + public void ComputeVelocity(int units, float maxVelocity) + { + mComputedVelocity = new ComputedVelocity(new Dictionary>()); + 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; + } + + /// + /// Retrieve the last computed velocity. + /// + [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); + } + + /// + /// Resets the velocity tracker state. + /// + [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 index 0000000..800d1a0 --- /dev/null +++ b/src/Tizen.NUI/src/public/Utility/VelocityTrackerStrategy.cs @@ -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 +{ + + /// + /// VelocityTrackerStrategy + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class VelocityTrackerStrategy + { + /// + /// Resets the velocity tracker state. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void Clear() { } + + /// + /// Adds movement information + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual void AddMovement(uint eventTime, int pointerId, float position) { } + + /// + /// Retrieve the last computed velocity + /// + [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 index 0000000..153d55f --- /dev/null +++ b/test/Tizen.NUI.Samples/Tizen.NUI.Samples/Samples/VelocityTrackerSample.cs @@ -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 mMovements = new List(); + + 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(); + } + } + } +} -- 2.7.4