diff --git a/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/App.xaml b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/App.xaml
new file mode 100644
index 0000000..90013cf
--- /dev/null
+++ b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/App.xaml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/App.xaml.cs b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/App.xaml.cs
new file mode 100644
index 0000000..c2da10f
--- /dev/null
+++ b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/App.xaml.cs
@@ -0,0 +1,19 @@
+using Xamarin.Essentials;
+using Xamarin.Forms.Xaml;
+using XFShimmerLayoutPCLSample.Controls;
+
+[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
+namespace XFShimmerLayoutPCLSample
+{
+ public partial class App
+ {
+ public App()
+ {
+ InitializeComponent();
+
+ ShimmerLayout.Init(DeviceDisplay.ScreenMetrics.Density);
+
+ MainPage = new Views.ShimmerTestPage();
+ }
+ }
+}
diff --git a/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/Models/NotifyingObject.cs b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/Models/NotifyingObject.cs
new file mode 100644
index 0000000..b07450a
--- /dev/null
+++ b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/Models/NotifyingObject.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Xamarin.Forms;
+
+namespace XFShimmerLayoutPCLSample.Models
+{
+ public class NotifyingObject : BindableObject
+ {
+ private readonly Dictionary _properties;
+
+ public NotifyingObject()
+ {
+ _properties = new Dictionary();
+ }
+
+ #region [Notify Property Changed]
+
+ protected virtual bool Set(ref T storage, T value, [CallerMemberName] string propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(storage, value)) return false;
+
+ storage = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/Properties/AssemblyInfo.cs b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..9074093
--- /dev/null
+++ b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/Properties/AssemblyInfo.cs
@@ -0,0 +1,26 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+// Information about this assembly is defined by the following attributes.
+// Change them to the values specific to your project.
+
+[assembly: AssemblyTitle("XFShimmerLayoutPCLSample")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("")]
+[assembly: AssemblyCopyright("${AuthorCopyright}")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
+// The form "{Major}.{Minor}.*" will automatically update the build and revision,
+// and "{Major}.{Minor}.{Build}.*" will update just the revision.
+
+[assembly: AssemblyVersion("1.0.*")]
+
+// The following attributes are used to specify the signing key for the assembly,
+// if desired. See the Mono documentation for more information about signing.
+
+//[assembly: AssemblyDelaySign(false)]
+//[assembly: AssemblyKeyFile("")]
diff --git a/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/ViewModels/ShimmerTestPageViewModel.cs b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/ViewModels/ShimmerTestPageViewModel.cs
new file mode 100644
index 0000000..565a824
--- /dev/null
+++ b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/ViewModels/ShimmerTestPageViewModel.cs
@@ -0,0 +1,35 @@
+using System.Threading.Tasks;
+using Xamarin.Forms;
+using XFShimmerLayoutPCLSample.Models;
+
+namespace XFShimmerLayoutPCLSample.ViewModels
+{
+ public class ShimmerTestPageViewModel : NotifyingObject
+ {
+ private bool _isBusy;
+ public bool IsBusy
+ {
+ get => _isBusy;
+ set => Set(ref _isBusy, value);
+ }
+
+ private Command _startAnimationCommand;
+ public Command StartAnimationCommand
+ {
+ get => _startAnimationCommand;
+ set => Set(ref _startAnimationCommand, value);
+ }
+
+ public ShimmerTestPageViewModel()
+ {
+ StartAnimationCommand = new Command(async () =>
+ {
+ IsBusy = true;
+
+ await Task.Delay(5000);
+
+ IsBusy = false;
+ });
+ }
+ }
+}
diff --git a/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/Views/ShimmerTestPage.xaml b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/Views/ShimmerTestPage.xaml
new file mode 100644
index 0000000..8033ed6
--- /dev/null
+++ b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/Views/ShimmerTestPage.xaml
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/Views/ShimmerTestPage.xaml.cs b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/Views/ShimmerTestPage.xaml.cs
new file mode 100644
index 0000000..73d63df
--- /dev/null
+++ b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/Views/ShimmerTestPage.xaml.cs
@@ -0,0 +1,13 @@
+using Xamarin.Forms.Xaml;
+
+namespace XFShimmerLayoutPCLSample.Views
+{
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+ public partial class ShimmerTestPage
+ {
+ public ShimmerTestPage()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/XFShimmerLayoutPCLSample.csproj b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/XFShimmerLayoutPCLSample.csproj
new file mode 100644
index 0000000..15d132a
--- /dev/null
+++ b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/XFShimmerLayoutPCLSample.csproj
@@ -0,0 +1,92 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {E3929578-BD44-4BD0-98E5-D6DBC86EE83C}
+ {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ Library
+ XFShimmerLayoutPCLSample
+ XFShimmerLayoutPCLSample
+ v4.5
+ Profile111
+
+
+ true
+ full
+ false
+ bin\Debug
+ DEBUG;
+ prompt
+ 4
+
+
+ true
+ bin\Release
+ prompt
+ 4
+
+
+
+
+
+
+ ..\..\XFShimmerLayoutSample\Views\Views\ShimmerTestPage.xaml
+ Code
+
+
+ App.xaml
+ Code
+
+
+
+
+ {A3943919-A81C-47F6-88C0-8551EB70E28F}
+ XFShimmerLayoutPCL
+
+
+
+
+
+
+
+ ..\..\..\packages\System.Numerics.Vectors.4.5.0\lib\portable-net45+win8+wp8+wpa81\System.Numerics.Vectors.dll
+
+
+ ..\..\..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\netstandard1.1\System.Runtime.InteropServices.RuntimeInformation.dll
+
+
+ ..\..\..\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll
+
+
+ ..\..\..\packages\Xamarin.Essentials.1.0.1\lib\netstandard1.0\Xamarin.Essentials.dll
+
+
+ ..\..\..\packages\Xamarin.Forms.3.4.0.1029999\lib\netstandard1.0\Xamarin.Forms.Core.dll
+
+
+ ..\..\..\packages\Xamarin.Forms.3.4.0.1029999\lib\netstandard1.0\Xamarin.Forms.Platform.dll
+
+
+ ..\..\..\packages\Xamarin.Forms.3.4.0.1029999\lib\netstandard1.0\Xamarin.Forms.Xaml.dll
+
+
+
+
+
+
+
+
+
+ Designer
+ MSBuild:UpdateDesignTimeXaml
+
+
+ Designer
+ MSBuild:UpdateDesignTimeXaml
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/packages.config b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/packages.config
new file mode 100644
index 0000000..05fb75a
--- /dev/null
+++ b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutPCLSample/packages.config
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutSample.Android/MainActivity.cs b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutSample.Android/MainActivity.cs
index 6bbba9a..5148993 100644
--- a/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutSample.Android/MainActivity.cs
+++ b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutSample.Android/MainActivity.cs
@@ -3,6 +3,7 @@
using Android.OS;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
+using XFShimmerLayoutPCLSample;
namespace XFShimmerLayoutSample.Droid
{
diff --git a/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutSample.Android/XFShimmerLayoutSample.Android.csproj b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutSample.Android/XFShimmerLayoutSample.Android.csproj
index 939b629..573c886 100644
--- a/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutSample.Android/XFShimmerLayoutSample.Android.csproj
+++ b/src/Sample/XFShimmerLayoutSample/XFShimmerLayoutSample.Android/XFShimmerLayoutSample.Android.csproj
@@ -15,7 +15,6 @@
Properties\AndroidManifest.xml
Resources
Assets
- false
v9.0
Xamarin.Android.Net.AndroidClientHandler
@@ -95,9 +94,9 @@
-
- {B5D70764-2D58-450C-8A26-3F7F01D61DE6}
- XFShimmerLayoutSample
+
+ {E3929578-BD44-4BD0-98E5-D6DBC86EE83C}
+ XFShimmerLayoutPCLSample
diff --git a/src/XFShimmerLayout.sln b/src/XFShimmerLayout.sln
index a020e77..ba91f6a 100644
--- a/src/XFShimmerLayout.sln
+++ b/src/XFShimmerLayout.sln
@@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XFShimmerLayoutSample.Andro
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XFShimmerLayoutSample", "Sample\XFShimmerLayoutSample\XFShimmerLayoutSample\XFShimmerLayoutSample.csproj", "{BB8529D3-C809-42C7-AEEA-70FDAFF5903D}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XFShimmerLayoutPCL", "XFShimmerLayoutPCL\XFShimmerLayoutPCL.csproj", "{A3943919-A81C-47F6-88C0-8551EB70E28F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XFShimmerLayoutPCLSample", "Sample\XFShimmerLayoutSample\XFShimmerLayoutPCLSample\XFShimmerLayoutPCLSample.csproj", "{E3929578-BD44-4BD0-98E5-D6DBC86EE83C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -31,6 +35,14 @@ Global
{BB8529D3-C809-42C7-AEEA-70FDAFF5903D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB8529D3-C809-42C7-AEEA-70FDAFF5903D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB8529D3-C809-42C7-AEEA-70FDAFF5903D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A3943919-A81C-47F6-88C0-8551EB70E28F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A3943919-A81C-47F6-88C0-8551EB70E28F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A3943919-A81C-47F6-88C0-8551EB70E28F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A3943919-A81C-47F6-88C0-8551EB70E28F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E3929578-BD44-4BD0-98E5-D6DBC86EE83C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E3929578-BD44-4BD0-98E5-D6DBC86EE83C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E3929578-BD44-4BD0-98E5-D6DBC86EE83C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E3929578-BD44-4BD0-98E5-D6DBC86EE83C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -38,6 +50,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{5E5E9F18-5EE0-4686-8DEB-E33716973742} = {BCEF8D27-B58E-4F5B-B370-96FFFAAFCB71}
{BB8529D3-C809-42C7-AEEA-70FDAFF5903D} = {BCEF8D27-B58E-4F5B-B370-96FFFAAFCB71}
+ {E3929578-BD44-4BD0-98E5-D6DBC86EE83C} = {BCEF8D27-B58E-4F5B-B370-96FFFAAFCB71}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3C3E6F0C-E192-4BFB-9DAD-D5BDD6FFD454}
diff --git a/src/XFShimmerLayoutPCL/Controls/ShimmerLayout.cs b/src/XFShimmerLayoutPCL/Controls/ShimmerLayout.cs
new file mode 100644
index 0000000..1eb62f3
--- /dev/null
+++ b/src/XFShimmerLayoutPCL/Controls/ShimmerLayout.cs
@@ -0,0 +1,450 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using SkiaSharp;
+using SkiaSharp.Views.Forms;
+using Xamarin.Forms;
+using Xamarin.Forms.Internals;
+using XFShimmerLayout.Extensions;
+using XFShimmerLayout.Models.SkiaHelpers;
+
+namespace XFShimmerLayoutPCL.Controls
+{
+ ///
+ ///
+ /// Adding Shimmering Effect to every child Element
+ ///
+ [ContentProperty("PackedView")]
+ public class ShimmerLayout : Grid
+ {
+ #region Bindable Properties
+
+ public static readonly BindableProperty IsLoadingProperty = BindableProperty.Create(
+ nameof(IsLoading), typeof(bool), typeof(ShimmerLayout), false,
+ propertyChanged: (b, o, n) => ((ShimmerLayout)b)?.Invalidate());
+
+ ///
+ /// The IsLoading Property to Enable/Disable The Shimmer
+ ///
+ public bool IsLoading
+ {
+ get => (bool)GetValue(IsLoadingProperty);
+ set => SetValue(IsLoadingProperty, value);
+ }
+
+ public static readonly BindableProperty DurationProperty = BindableProperty.Create(
+ nameof(Duration), typeof(uint), typeof(ShimmerLayout), 1000U);
+
+ ///
+ /// The Duration of the Shimmer
+ ///
+ public uint Duration
+ {
+ get => (uint)GetValue(DurationProperty);
+ set => SetValue(DurationProperty, value);
+ }
+
+ public static readonly BindableProperty PackedViewProperty = BindableProperty.Create(
+ nameof(PackedView), typeof(View), typeof(ShimmerLayout),
+ propertyChanged: (b, o, n) => ((ShimmerLayout)b)?.UpdatePackedView((View)o, (View)n));
+
+ ///
+ /// The View we want to apply the Shimmer
+ ///
+ public View PackedView
+ {
+ get => (View)GetValue(PackedViewProperty);
+ set => SetValue(PackedViewProperty, value);
+ }
+
+ public static readonly BindableProperty BackgroundGradientColorProperty = BindableProperty.Create(
+ nameof(BackgroundGradientColor), typeof(Color), typeof(ShimmerLayout), Color.FromHex("#B1AEB2"));
+
+ ///
+ /// The Background Color of the Shimmer
+ ///
+ public Color BackgroundGradientColor
+ {
+ get => (Color)GetValue(BackgroundGradientColorProperty);
+ set => SetValue(BackgroundGradientColorProperty, value);
+ }
+
+ public static readonly BindableProperty ForegroundGradientColorProperty = BindableProperty.Create(
+ nameof(ForegroundGradientColor), typeof(Color), typeof(ShimmerLayout), Color.FromHex("#9B969C"));
+
+ ///
+ /// The Foreground Color of the Shimmer
+ ///
+ public Color ForegroundGradientColor
+ {
+ get => (Color)GetValue(ForegroundGradientColorProperty);
+ set => SetValue(ForegroundGradientColorProperty, value);
+ }
+
+ public static readonly BindableProperty GradientSizeProperty = BindableProperty.Create(
+ nameof(GradientSize), typeof(float), typeof(ShimmerLayout), 0.4f);
+
+ ///
+ /// The size ratio of the Gradient
+ ///
+ public float GradientSize
+ {
+ get => (float)GetValue(GradientSizeProperty);
+ set => SetValue(GradientSizeProperty, value);
+ }
+
+ public static readonly BindableProperty AngleProperty = BindableProperty.Create(
+ nameof(Angle), typeof(int), typeof(ShimmerLayout), -45);
+
+ ///
+ /// The Angle for the Gradient
+ ///
+ public int Angle
+ {
+ get => (int)GetValue(AngleProperty);
+ set => SetValue(AngleProperty, value);
+ }
+
+ #endregion
+
+ private static double _density;
+
+ private bool _isSizeAllocated;
+ private float[] _gradientPositions;
+ private SKColor[] _gradientColors;
+ private SKExtCanvasView _maskCanvasView;
+ private IList _childVisualElements;
+
+ private const string ShimmerAnimation = "ShimmerAnimation";
+ private CancellationTokenSource _animationCancellationTokenSource;
+ private TaskCompletionSource _animationCycleCompletionSource;
+
+ ///
+ /// Adds Shimmering Effect to its children
+ ///
+ public ShimmerLayout()
+ {
+ IsClippedToBounds = true;
+
+ SizeChanged += ElementSizeChanged;
+ }
+
+ ///
+ /// Initialized the ShimmerLayout by specifying the Device Density
+ ///
+ /// The Device Density
+ public static void Init(double deviceDensity)
+ {
+ _density = deviceDensity;
+ }
+
+ #region Events
+
+ ///
+ /// We want to make sure that we will got Width and Height before apply the Shimmer
+ ///
+ /// The Grid Container
+ /// SizeChanged Args
+ private void ElementSizeChanged(object sender, EventArgs args)
+ {
+ SizeChanged -= ElementSizeChanged;
+
+ _isSizeAllocated = true;
+
+ Invalidate();
+ }
+
+ ///
+ ///
+ /// On Parent Set is used to remove the event handler
+ ///
+ protected override void OnParentSet()
+ {
+ base.OnParentSet();
+
+ if (Parent != null) return;
+
+ _maskCanvasView.PaintSurface -= OnMaskCanvasPaintSurface;
+ }
+
+ #endregion
+
+ #region Property Changed
+
+ private void Invalidate()
+ {
+ if (!_isSizeAllocated) return;
+
+ if (IsLoading) ApplyShimmer();
+ else RemoveShimmer();
+ }
+
+ private void UpdatePackedView(View oldValue, View newValue)
+ {
+ if (oldValue != null && Children.Contains(oldValue))
+ {
+ Children.Remove(oldValue);
+ }
+
+ Children.Insert(0, newValue);
+ }
+
+ private void UpdateGradient()
+ {
+ _gradientPositions = new []
+ {
+ 0,
+ .5f - GradientSize / 2,
+ .5f + GradientSize / 2,
+ 1
+ };
+
+ _gradientColors = new []
+ {
+ BackgroundGradientColor.ToSKColor(),
+ ForegroundGradientColor.ToSKColor(),
+ ForegroundGradientColor.ToSKColor(),
+ BackgroundGradientColor.ToSKColor()
+ };
+ }
+
+ #endregion
+
+ #region Shimmer Animation
+
+ ///
+ /// Apply the Shimmer Effect to the child Elements
+ ///
+ private void ApplyShimmer()
+ {
+ if (_maskCanvasView == null)
+ {
+ _maskCanvasView = new SKExtCanvasView
+ {
+ Opacity = 0,
+ IsVisible = false,
+ VerticalOptions = LayoutOptions.FillAndExpand,
+ HorizontalOptions = LayoutOptions.FillAndExpand
+ };
+
+ _maskCanvasView.PaintSurface += OnMaskCanvasPaintSurface;
+
+ Children.Add(_maskCanvasView);
+ }
+
+ UpdateGradient();
+ ExtractVisualElements(PackedView);
+
+ Task.Run(async () => await StartAnimation());
+ }
+
+ ///
+ /// Starts the shimmering animation
+ ///
+ ///
+ private async Task StartAnimation()
+ {
+ CancelAnimation();
+
+ /* First Fade the CanvasView to 1 */
+ Device.BeginInvokeOnMainThread(() =>
+ {
+ _maskCanvasView.Opacity = 0;
+ _maskCanvasView.IsVisible = true;
+ });
+
+ var widthPixels = Width * _density;
+ var gradientSizePixels = GradientSize * Width * 2 * _density;
+
+ var startValue = -gradientSizePixels;
+ var endValue = gradientSizePixels + widthPixels;
+
+ var tasks = new []
+ {
+ Task.Run(async () => await _maskCanvasView.FadeTo(1, 250U, Easing.Linear)),
+ Task.Run(async () =>
+ {
+ _animationCancellationTokenSource = new CancellationTokenSource();
+
+ /* While no cancel requested, continue the loop */
+ while (!_animationCancellationTokenSource.Token.IsCancellationRequested)
+ {
+ _animationCycleCompletionSource = new TaskCompletionSource();
+
+ new Animation
+ {
+ {
+ 0, 1,
+ new Animation(t => _maskCanvasView.InvalidateSurface("Width", t), startValue, endValue)
+ }
+ }.Commit(_maskCanvasView, ShimmerAnimation, 16, Duration, Easing.Linear,
+ (v, c) => _animationCycleCompletionSource.SetResult(c));
+
+ /* Wait for the animation completion callback and start it again */
+ await _animationCycleCompletionSource.Task;
+ }
+ })
+ };
+
+ await Task.WhenAll(tasks);
+ }
+
+ ///
+ /// Removed the Shimmer Effect from the elements
+ ///
+ private void CancelAnimation()
+ {
+ /* Cancel The Animation */
+ _animationCancellationTokenSource?.Cancel();
+ _maskCanvasView?.AbortAnimation(ShimmerAnimation);
+ }
+
+ private void RemoveShimmer()
+ {
+ /* Cancel The Animation */
+ CancelAnimation();
+
+ Task.Run(async () =>
+ {
+ if (_maskCanvasView == null) return;
+
+ /* Fade the CanvasView to 0 */
+ await _maskCanvasView.FadeTo(0, 250U, Easing.Linear);
+ Device.BeginInvokeOnMainThread(() => _maskCanvasView.IsVisible = false);
+ });
+ }
+
+ #endregion
+
+ #region Canvas Draw
+
+ ///
+ /// Extracts all the VisualElements, excluding layouts from a View
+ ///
+ /// The parent Element to extract elements from
+ private void ExtractVisualElements(View baseElement)
+ {
+ _childVisualElements = new List();
+
+ if (!(baseElement is Layout baseLayout))
+ {
+ _childVisualElements.Add(baseElement.ToSKVisualElement());
+ return;
+ }
+
+ baseLayout
+ .ToSKLayout()
+ .GetChildrenSKVisualElements()
+ .ForEach(_childVisualElements.Add);
+ }
+
+ ///
+ /// PaintSurface Canvas Callback
+ ///
+ /// The SKCanvasView
+ /// Paint Parameters
+ private void OnMaskCanvasPaintSurface(object sender, SKPaintSurfaceEventArgs args)
+ {
+ args.Surface.Canvas.Clear();
+
+ using (var paint = new SKPaint { IsAntialias = true, IsDither = true })
+ {
+ /* Generate the Gradient Shader */
+ paint.Shader = GetGradientShader();
+
+ /* Draw every VisualElement in our layout tree to the SKCanvas */
+ foreach (var view in _childVisualElements)
+ {
+ DrawSKVisualElement(view, args.Surface.Canvas, paint);
+ }
+ }
+ }
+
+ ///
+ /// Generates a Gradient Shader for the Canvas with the actual Shimmer
+ ///
+ /// The SKShader
+ private SKShader GetGradientShader()
+ {
+ /* Try get the Width Argument from the maskCanvasView from the Animation */
+ if (!(_maskCanvasView.GetArgument("Width") is double currentWidth)) return null;
+
+ var widthPixels = Width * _density;
+ var heightPixels = Height * _density;
+
+ /*
+ * Calculate the size of gradient in pixels
+ */
+ var gradientSizePixels = GradientSize * Width * 2 * _density;
+
+ /*
+ * Hold on here:
+ * We've got 2 Points. The Start and the End point.
+ *
+ * First of all we must generate the two points from the Angle.
+ * This was so hard to achieve, that I was looking into the CSS Gradient Spec.
+ * We used an extension method to generate two points from the angle and the
+ * diagonal distance will be Math.Pow(2, -1/2). This extension method will return the
+ * ratio in [0,1]. We must transform it to the actual width/height.
+ *
+ * The Start Point will start from the exported ratio multiplied by the width pixels.
+ * Then we must add the X Offset that will be added in order to make the animation happen.
+ * Multiply also the ratio for the Y of the start point.
+ *
+ * The End Point must be the X Offset plus the calculated size of the gradient
+ * in pixels and then the result must be multiplied with the ratio that we've got
+ * from the extension method before. Y must be multiplied, also, with the ratio.
+ */
+ var points = ((double)Angle).ToSKPoints();
+ points[0].X = (float)(currentWidth + points[0].X * widthPixels);
+ points[0].Y = (float)(points[0].Y * heightPixels);
+ points[1].X = (float)((currentWidth + gradientSizePixels) * points[1].X);
+ points[1].Y = (float)(points[1].Y * heightPixels);
+
+ /*
+ * AFTER ALL THAT BRAIN F**K WE"VE GOT OUR GRADIENT
+ * LISTEN, NEVER. SRSLY, NEVER MESS UP WITH SHADERS
+ */
+ return SKShader.CreateLinearGradient(
+ points[0],
+ points[1],
+ _gradientColors,
+ _gradientPositions,
+ SKShaderTileMode.Clamp);
+ }
+
+ ///
+ /// Draws a specified SKVisualElement to the canvas
+ ///
+ /// The element to be drawn
+ /// The canvas to draw on
+ /// The paint that will be used to draw
+ private static void DrawSKVisualElement(SKVisualElement skVisualElement, SKCanvas canvas, SKPaint paint)
+ {
+ /*
+ Get the X and Y, Including Margins and Paddings
+ added to it exactly or to parent layouts
+ */
+ var startX = (float)(skVisualElement.GetX() * _density);
+ var startY = (float)(skVisualElement.GetY() * _density);
+ var widthPixels = (float)(skVisualElement.Width * _density);
+ var heightPixels = (float)(skVisualElement.Height * _density);
+
+ /* Generate Radii from CornerRadius */
+ var radii = skVisualElement.CornerRadius.ToRadiiSKPoints(_density);
+
+ /* Using the SKRect constructor, in width and height, we must add the offset X and Y */
+ var rectangle = new SKRect(startX, startY, widthPixels + startX, heightPixels + startY);
+
+ /* Create the Round Rectangle */
+ var roundRectangle = new SKRoundRect();
+ roundRectangle.SetRectRadii(rectangle, radii);
+
+ /* Draw it to the canvas */
+ canvas.DrawRoundRect(roundRectangle, paint);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/XFShimmerLayoutPCL/Extensions/CornerRadiusExtensions.cs b/src/XFShimmerLayoutPCL/Extensions/CornerRadiusExtensions.cs
new file mode 100644
index 0000000..608f200
--- /dev/null
+++ b/src/XFShimmerLayoutPCL/Extensions/CornerRadiusExtensions.cs
@@ -0,0 +1,41 @@
+using SkiaSharp;
+using Xamarin.Forms;
+
+namespace XFShimmerLayoutPCL.Extensions
+{
+ internal static class CornerRadiusExtensions
+ {
+ public static float[] ToRadii(this CornerRadius cornerRadius, double density)
+ {
+ return new[]
+ {
+ ToPixels(cornerRadius.TopLeft, density),
+ ToPixels(cornerRadius.TopLeft, density),
+ ToPixels(cornerRadius.TopRight, density),
+ ToPixels(cornerRadius.TopRight, density),
+ ToPixels(cornerRadius.BottomRight, density),
+ ToPixels(cornerRadius.BottomRight, density),
+ ToPixels(cornerRadius.BottomLeft, density),
+ ToPixels(cornerRadius.BottomLeft, density)
+ };
+ }
+
+ public static SKPoint[] ToRadiiSKPoints(this CornerRadius cornerRadius, double density)
+ {
+ var radii = cornerRadius.ToRadii(density);
+
+ return new[]
+ {
+ new SKPoint(radii[0], radii[1]),
+ new SKPoint(radii[2], radii[3]),
+ new SKPoint(radii[4], radii[5]),
+ new SKPoint(radii[6], radii[7])
+ };
+ }
+
+ public static float ToPixels(double units, double density)
+ {
+ return (float)(units * density);
+ }
+ }
+}
diff --git a/src/XFShimmerLayoutPCL/Extensions/SkiaExtensions.cs b/src/XFShimmerLayoutPCL/Extensions/SkiaExtensions.cs
new file mode 100644
index 0000000..21ae912
--- /dev/null
+++ b/src/XFShimmerLayoutPCL/Extensions/SkiaExtensions.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using SkiaSharp;
+using Xamarin.Forms;
+using XFShimmerLayout.Models.SkiaHelpers;
+
+namespace XFShimmerLayoutPCL.Extensions
+{
+ internal static class SkiaExtensions
+ {
+ public static SKVisualElement ToSKVisualElement(this View element)
+ {
+ var visualElement = new SKVisualElement((float)element.X, (float)element.Y, (float)element.Width, (float)element.Height, element.Margin);
+
+ switch (element)
+ {
+ case BoxView boxView:
+ visualElement.CornerRadius = boxView.CornerRadius;
+ break;
+ case Frame frame:
+ visualElement.CornerRadius = new CornerRadius(frame.CornerRadius);
+ break;
+ }
+
+ return visualElement;
+ }
+
+ public static SKLayout ToSKLayout(this Layout layout)
+ {
+ var layoutView = new SKLayout((float)layout.X, (float)layout.Y, (float)layout.Width, (float)layout.Height, layout.Margin);
+
+ var children = new List();
+
+ foreach (var view in layout.Children)
+ {
+ if (view is Layout childLayout) children.Add(childLayout.ToSKLayout());
+ else
+ {
+ var childView = view.ToSKVisualElement();
+ childView.Parent = layoutView;
+ children.Add(childView);
+ }
+ }
+
+ layoutView.Children = children;
+ layoutView.Padding = layout.Padding;
+
+ return layoutView;
+ }
+
+ public static IEnumerable GetChildrenSKVisualElements(this SKLayout layoutView)
+ {
+ var childViews = new List();
+
+ foreach (var childView in layoutView.Children)
+ {
+ if (childView is SKLayout childLayout) childViews.AddRange(childLayout.GetChildrenSKVisualElements());
+ else childViews.Add(childView);
+ }
+
+ return childViews;
+ }
+
+ public static float GetX(this SKVisualElement childView)
+ {
+ var x = childView.X;
+ var parent = childView.Parent;
+ while (parent != null)
+ {
+ x += parent.X;
+ parent = parent.Parent;
+ }
+
+ return x;
+ }
+
+ public static float GetY(this SKVisualElement childView)
+ {
+ var y = childView.Y;
+ var parent = childView.Parent;
+ while (parent != null)
+ {
+ y += parent.Y;
+ parent = parent.Parent;
+ }
+
+ return y;
+ }
+
+ public static SKPoint[] ToSKPoints(this double angle)
+ {
+ var points = angle.ToPoints().ToArray();
+
+ return new[]
+ {
+ new SKPoint((float) points[0].X, (float) points[0].Y),
+ new SKPoint((float) points[1].X, (float) points[1].Y)
+ };
+ }
+
+ public static IEnumerable ToPoints(this double angle)
+ {
+ var d = Math.Pow(2, .5);
+ var eps = Math.Pow(2, -52);
+
+ var finalAngle = angle % 360;
+
+ var startPointRadians = (180 - finalAngle).ToRadians();
+ var startX = d * Math.Cos(startPointRadians);
+ var startY = d * Math.Sin(startPointRadians);
+
+ var endPointRadians = (360 - finalAngle).ToRadians();
+ var endX = d * Math.Cos(endPointRadians);
+ var endY = d * Math.Sin(endPointRadians);
+
+ return new[]
+ {
+ new Point(startX.CheckForOverflow(eps), startY.CheckForOverflow(eps)),
+ new Point(endX.CheckForOverflow(eps), endY.CheckForOverflow(eps))
+ };
+ }
+
+ public static double ToRadians(this double angle)
+ {
+ return Math.PI * angle / 180;
+ }
+
+ private static double CheckForOverflow(this double value, double eps)
+ {
+ return value <= 0 || Math.Abs(value) <= eps ? 0f : value;
+ }
+ }
+}
diff --git a/src/XFShimmerLayoutPCL/Models/SkiaHelpers/SKExtCanvasView.cs b/src/XFShimmerLayoutPCL/Models/SkiaHelpers/SKExtCanvasView.cs
new file mode 100644
index 0000000..230b68f
--- /dev/null
+++ b/src/XFShimmerLayoutPCL/Models/SkiaHelpers/SKExtCanvasView.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using SkiaSharp.Views.Forms;
+
+namespace XFShimmerLayoutPCL.Models.SkiaHelpers
+{
+ internal class SKExtCanvasView : SKCanvasView
+ {
+ private readonly Dictionary _arguments;
+
+ public SKExtCanvasView()
+ {
+ _arguments = new Dictionary();
+ }
+
+ public object GetArgument(string key)
+ {
+ var result = _arguments.TryGetValue(key, out var value);
+
+ return result ? value : null;
+ }
+
+ public void InvalidateSurface(string key, object argument)
+ {
+ if (_arguments.ContainsKey(key))
+ {
+ _arguments[key] = argument;
+ }
+ else
+ {
+ _arguments.Add(key, argument);
+ }
+
+ InvalidateSurface();
+ }
+ }
+}
diff --git a/src/XFShimmerLayoutPCL/Models/SkiaHelpers/SKLayout.cs b/src/XFShimmerLayoutPCL/Models/SkiaHelpers/SKLayout.cs
new file mode 100644
index 0000000..164b0fd
--- /dev/null
+++ b/src/XFShimmerLayoutPCL/Models/SkiaHelpers/SKLayout.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using Xamarin.Forms;
+
+namespace XFShimmerLayoutPCL.Models.SkiaHelpers
+{
+ internal class SKLayout : SKVisualElement
+ {
+ public Thickness Padding { get; set; }
+ public IList Children { get; set; }
+
+ public SKLayout(float x, float y, float width, float height, Thickness margin)
+ : base(x, y, width, height, margin) { }
+ }
+}
diff --git a/src/XFShimmerLayoutPCL/Models/SkiaHelpers/SKVisualElement.cs b/src/XFShimmerLayoutPCL/Models/SkiaHelpers/SKVisualElement.cs
new file mode 100644
index 0000000..ed21cb9
--- /dev/null
+++ b/src/XFShimmerLayoutPCL/Models/SkiaHelpers/SKVisualElement.cs
@@ -0,0 +1,25 @@
+using Xamarin.Forms;
+
+namespace XFShimmerLayoutPCL.Models.SkiaHelpers
+{
+ internal class SKVisualElement
+ {
+ public float X { get; }
+ public float Y { get; }
+ public float Width { get; }
+ public float Height { get; }
+ public Thickness Margin { get; }
+ public SKLayout Parent { get; set; }
+ public CornerRadius CornerRadius { get; set; }
+
+ public SKVisualElement(float x, float y, float width, float height, Thickness margin)
+ {
+ X = x;
+ Y = y;
+ Width = width;
+ Height = height;
+ Margin = margin;
+ CornerRadius = new CornerRadius(0);
+ }
+ }
+}
diff --git a/src/XFShimmerLayoutPCL/Properties/AssemblyInfo.cs b/src/XFShimmerLayoutPCL/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..bb4f467
--- /dev/null
+++ b/src/XFShimmerLayoutPCL/Properties/AssemblyInfo.cs
@@ -0,0 +1,26 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+// Information about this assembly is defined by the following attributes.
+// Change them to the values specific to your project.
+
+[assembly: AssemblyTitle("XFShimmerLayoutPCL")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("")]
+[assembly: AssemblyCopyright("${AuthorCopyright}")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
+// The form "{Major}.{Minor}.*" will automatically update the build and revision,
+// and "{Major}.{Minor}.{Build}.*" will update just the revision.
+
+[assembly: AssemblyVersion("1.0.*")]
+
+// The following attributes are used to specify the signing key for the assembly,
+// if desired. See the Mono documentation for more information about signing.
+
+//[assembly: AssemblyDelaySign(false)]
+//[assembly: AssemblyKeyFile("")]
diff --git a/src/XFShimmerLayoutPCL/XFShimmerLayoutPCL.csproj b/src/XFShimmerLayoutPCL/XFShimmerLayoutPCL.csproj
new file mode 100644
index 0000000..08f7d65
--- /dev/null
+++ b/src/XFShimmerLayoutPCL/XFShimmerLayoutPCL.csproj
@@ -0,0 +1,66 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {A3943919-A81C-47F6-88C0-8551EB70E28F}
+ {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ Library
+ XFShimmerLayoutPCL
+ XFShimmerLayoutPCL
+ v4.5
+ Profile111
+
+
+ true
+ full
+ false
+ bin\Debug
+ DEBUG;
+ prompt
+ 4
+
+
+ true
+ bin\Release
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\packages\Xamarin.Forms.3.4.0.1029999\lib\netstandard1.0\Xamarin.Forms.Core.dll
+
+
+ ..\packages\Xamarin.Forms.3.4.0.1029999\lib\netstandard1.0\Xamarin.Forms.Platform.dll
+
+
+ ..\packages\Xamarin.Forms.3.4.0.1029999\lib\netstandard1.0\Xamarin.Forms.Xaml.dll
+
+
+ ..\packages\SkiaSharp.1.60.0\lib\portable-net45+win8+wpa81+wp8\SkiaSharp.dll
+
+
+ ..\packages\SkiaSharp.Views.Forms.1.60.0\lib\portable-net45+win8+wpa81+wp8\SkiaSharp.Views.Forms.dll
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/XFShimmerLayoutPCL/packages.config b/src/XFShimmerLayoutPCL/packages.config
new file mode 100644
index 0000000..fa23d5b
--- /dev/null
+++ b/src/XFShimmerLayoutPCL/packages.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file