Shimmer.NET
1.3.1
dotnet add package Shimmer.NET --version 1.3.1
NuGet\Install-Package Shimmer.NET -Version 1.3.1
<PackageReference Include="Shimmer.NET" Version="1.3.1" />
<PackageVersion Include="Shimmer.NET" Version="1.3.1" />
<PackageReference Include="Shimmer.NET" />
paket add Shimmer.NET --version 1.3.1
#r "nuget: Shimmer.NET, 1.3.1"
#:package Shimmer.NET@1.3.1
#addin nuget:?package=Shimmer.NET&version=1.3.1
#tool nuget:?package=Shimmer.NET&version=1.3.1
Shimmer.NET
A high-performance, pure C# shimmer/skeleton-loading library for .NET Android. No Java/Kotlin bindings — the gradient engine, view widgets, and state machine are all native C#.
- Content-aware masking —
PorterDuff.Mode.SrcInshimmer that hugs text and view bounds instead of painting over transparent background. - Synchronized sweeping —
ShimmerGroupdrives many independent shimmers off one clock for a perfectly unified sweep. - Built-in state machine —
Loading/Content/Errortransitions with cross-fade, plus a generated retry view. - Theme-aware by default — colors adapt to light/dark mode out of the box, no configuration required.
- Single-view widgets —
ShimmerTextView/ShimmerImageViewshimmer on their own, no wrapping container required. - Reduced-motion aware — automatically falls back to a static alpha pulse when the user has disabled system animations.
Contents
- Installation
- Core components
- XML attributes
- Runtime API reference
- Lifecycle & performance
- Masking mode
- Examples
- Troubleshooting
- Contributing
Installation
<PackageReference Include="Shimmer.NET" Version="1.3.1" />
Targets net9.0-android and net10.0-android. Requires the .NET Android workload.
Core components
ShimmerDrawable
The core engine: a Drawable that builds a LinearGradient/RadialGradient shader (base → highlight → base) and repositions it every animation tick. Exposes every visual/timing knob (Direction, Shape, Intensity, Dropoff, Angle, Duration, RepeatDelay, RepeatCount, RepeatMode, Interpolator, AutoStart, ReduceMotion) so you can drive it manually if you're not using ShimmerFrameLayout.
ShimmerFrameLayout
A FrameLayout (XML tag shimmer.net.widgets.ShimmerFrameLayout) that owns a ShimmerDrawable and applies it over its children. Supports:
- Overlay mode (default): draws the shimmer on top of your placeholder views.
- Mask mode (
app:shimmer_use_mask="true"): the shimmer only paints where child content already has non-transparent pixels — see Masking mode. - A
ShimmerState(Loading/Content/Error) state machine viaState/SetState(...), with automatic alpha cross-fading. ShowError(message, retryText, onRetry)to generate and show a retry view without building your own layout.
ShimmerGroup
A LinearLayout that runs a single shared ValueAnimator and pushes its progress into every descendant ShimmerFrameLayout, so a profile header, a card, and a list can all sweep in lockstep. Children are tracked lazily and re-scanned only when the view hierarchy actually changes.
ShimmerTextView / ShimmerImageView
TextView (XML tag shimmer.net.widgets.ShimmerTextView) and ImageView (shimmer.net.widgets.ShimmerImageView) subclasses that each own their own ShimmerDrawable directly — no wrapping ShimmerFrameLayout needed for a single label or thumbnail. Same app:shimmer_* attributes, same overlay/mask draw modes, same IsLoading toggle. Mask mode only shows anything over pixels the view already renders, so an empty TextView or an ImageView with no image/background has nothing to mask onto — give it real content/background first, or leave shimmer_use_mask off (the default) to use the overlay sweep instead.
ShimmerRecyclerView
A thin RecyclerView wrapper. ShowLoading(layoutId, count) swaps in a throwaway adapter that inflates layoutId as pure, unbound placeholders; HideLoading() restores whatever adapter was set beforehand.
ShimmerPresets
Static factories that build a ready-to-add ShimmerFrameLayout (or ShimmerGroup) entirely in code — no XML needed:
ShimmerPresets.CreateProfile(context, cornerRadius = 8f)— circular avatar + two rounded text lines.ShimmerPresets.CreatePost(context, cornerRadius = 8f)— rounded image block + rounded text lines.ShimmerPresets.CreateList(context, itemCount = 5, cornerRadius = 8f)—itemCountsynchronized post rows inside aShimmerGroup.
All three use density-independent dimensions (cornerRadius is in dp) and pick their placeholder color from ShimmerTheme (via GradientDrawable, not a flat rectangle), so they scale across screens, adapt to dark mode automatically, and read as rounded skeletons rather than hard-edged blocks.
XML attributes
All attributes are declared once and shared by every widget that owns a ShimmerDrawable — ShimmerFrameLayout, ShimmerTextView, and ShimmerImageView — under the app: namespace.
| Attribute | Type | Notes |
|---|---|---|
shimmer_base_color |
color | Defaults to a theme-aware light gray / deep graphite if omitted. |
shimmer_highlight_color |
color | Defaults to a theme-aware highlight if omitted. |
shimmer_base_alpha |
integer (0–255) | Overrides the base color's alpha channel. |
shimmer_highlight_alpha |
integer (0–255) | Overrides the highlight color's alpha channel. |
shimmer_angle |
float | Tilt of a Linear gradient, in degrees. |
shimmer_duration |
integer (ms) | Sweep duration. Default 1000. |
shimmer_repeat_count |
integer | -1 for infinite (default). |
shimmer_repeat_mode |
enum: restart, reverse |
|
shimmer_repeat_delay |
integer (ms) | Pause between sweeps. |
shimmer_intensity |
float (0–1) | Width of the highlight band. |
shimmer_dropoff |
float (0–1) | Edge softness of the highlight. |
shimmer_direction |
enum: left_to_right, right_to_left, top_to_bottom, bottom_to_top |
|
shimmer_shape |
enum: linear, radial |
|
shimmer_use_mask |
boolean | Enables content-aware PorterDuff masking. |
shimmer_is_loading |
boolean | Initial IsLoading/State value. |
shimmer_auto_start |
boolean | Whether entering/loading or attachment starts the animator automatically. Set false for manual Start() control. Default true. |
shimmer_reduce_motion |
boolean | Forces (or forces off) the static alpha-pulse fallback. If omitted, auto-detected from the system animator setting. |
Any omitted attribute uses the documented theme-aware/default value.
Runtime API reference
ShimmerDrawable
bool IsLoading { get; set; } // setting true starts when AutoStart is enabled
void Start(); // ensures the animator exists and (re)starts it
void Stop(); // cancels the animator without resetting state
bool IsStarted { get; }
float Progress { get; set; } // used by ShimmerGroup when IsExternallyDriven
bool IsExternallyDriven { get; set; } // true while a ShimmerGroup owns the clock
bool AutoStart { get; set; } // false stops automatic/running animation
Color BaseColor { get; set; }
Color HighlightColor { get; set; }
float Angle { get; set; }
long Duration { get; set; }
long RepeatDelay { get; set; }
int RepeatCount { get; set; }
ValueAnimatorRepeatMode RepeatMode { get; set; }
float Intensity { get; set; } // clamped 0..1
float Dropoff { get; set; } // clamped 0..1
ShimmerDirection Direction { get; set; }
ShimmerShape Shape { get; set; }
bool UseMask { get; set; }
bool ReduceMotion { get; set; } // static alpha pulse instead of a moving sweep
IInterpolator Interpolator { get; set; }
ShimmerFrameLayout
ShimmerDrawable ShimmerDrawable { get; }
ShimmerState State { get; set; } // Loading | Content | Error
bool IsLoading { get; set; } // convenience wrapper over State
void SetState(ShimmerState state, bool animate = true);
void SetErrorView(View errorView);
void ShowError(string message, string retryText = "Retry", Action? onRetry = null);
void Pause(); // stop the sweep, keep State as-is
void Resume(); // resume if still Loading
ShimmerTextView / ShimmerImageView
ShimmerDrawable ShimmerDrawable { get; }
bool IsLoading { get; set; } // passthrough to ShimmerDrawable.IsLoading
ShimmerRecyclerView
void ShowLoading(int loadingLayoutId, int itemCount = 10);
void HideLoading();
Lifecycle & performance
ShimmerFrameLayout(andShimmerTextView/ShimmerImageView) automatically stop their sweep inOnDetachedFromWindowand whenever visibility changes to anything other thanVisible(window backgrounded, an ancestor hidden, an off-screen ViewPager page), then resume automatically when visible again — no wiring required.- For explicit control (e.g. you want to pause slightly before
OnPauseactually fires), callPause()/Resume()directly; both leaveStateuntouched, so the placeholder layout doesn't flash. ShimmerDrawablereuses its gradient color/position arrays and repositions the shader viaMatrix.SetLocalMatrix— no per-frame allocation. Rebuilding the shader (on color/intensity/dropoff/shape/bounds changes) disposes the outgoingShaderrather than leaking it, and mask mode reuses a single cachedPorterDuffXfermodeinstead of allocating one per frame.- Every widget disposes its owned
ShimmerDrawable/ValueAnimators in its ownDispose(bool)override — you don't need to do anything extra, but if you're building a custom widget on top ofShimmerDrawabledirectly, remember thatCancel()on aValueAnimatordoesn't release it; replace-and-dispose, don't just drop the reference. - Prefer
ShimmerGroupover N independentShimmerFrameLayouts when they should look synchronized: it's oneValueAnimator, not N. - In a
RecyclerView, set allapp:shimmer_*attributes via XML/style rather than one-by-one in code —ShimmerFrameLayout.Init()batches XML attributes into a single shader/animator rebuild, but setting individual properties on an already-inflated drawable rebuilds on each call. - If the user has disabled system animations, every shimmer widget auto-detects it and falls back to a static alpha pulse (
ReduceMotion) instead of a moving sweep, so the loading indicator stays legible without violating the user's motion preference. Override withshimmer_reduce_motionif you need to force one behavior regardless of the system setting.
Masking mode
Mask mode uses Canvas.SaveLayer() + PorterDuffXfermode(PorterDuff.Mode.SrcIn) so the shimmer only appears over pixels your placeholder views already drew — ideal for text-shaped or irregularly-shaped skeletons. Trade-offs:
- Requires an extra offscreen layer per frame, so it costs more than overlay mode.
- On some GPU/API-level combinations you may see artifacts; if so, set
shimmer_use_mask="false"(orUseMask = false) and fall back to overlay mode, optionally withSetLayerType(LayerType.Software, null). - On
ShimmerTextView/ShimmerImageViewspecifically, mask mode only has something to mask onto if the view already has visible content — realTexton aShimmerTextView, or an existingDrawable/background on aShimmerImageView. An empty one masks onto nothing (invisible shimmer); use overlay mode (the default) for those cases instead.
Examples
XML (linear shimmer, mask mode):
<shimmer.net.widgets.ShimmerFrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:shimmer_is_loading="true"
app:shimmer_use_mask="true"
app:shimmer_duration="1500"
app:shimmer_direction="left_to_right"
app:shimmer_shape="linear"
app:shimmer_intensity="0.3"
app:shimmer_dropoff="0.5">
</shimmer.net.widgets.ShimmerFrameLayout>
Runtime tweaks:
var shimmer = FindViewById<Shimmer.NET.Widgets.ShimmerFrameLayout>(Resource.Id.shimmer_main);
shimmer.ShimmerDrawable.Duration = 1200;
shimmer.ShimmerDrawable.Intensity = 0.4f;
shimmer.ShimmerDrawable.Direction = Shimmer.NET.Drawables.ShimmerDirection.TopToBottom;
shimmer.IsLoading = true;
State transitions with a retry view:
shimmer.State = ShimmerState.Loading;
// ...network call fails...
shimmer.ShowError("Couldn't load your feed.", "Retry", onRetry: () => ReloadFeed());
// ...network call succeeds...
shimmer.SetState(ShimmerState.Content, animate: true);
Code-only skeleton, no XML:
var list = Shimmer.NET.Helpers.ShimmerPresets.CreateList(this, itemCount: 6, cornerRadius: 12f);
container.AddView(list);
A single shimmering label or avatar, no wrapping ShimmerFrameLayout needed:
<shimmer.net.widgets.ShimmerTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Loading your feed..."
app:shimmer_is_loading="true"
app:shimmer_use_mask="true" />
<shimmer.net.widgets.ShimmerImageView
android:layout_width="64dp"
android:layout_height="64dp"
android:background="@drawable/avatar_placeholder"
app:shimmer_is_loading="true"
app:shimmer_use_mask="true" />
Troubleshooting
- No animation visible: confirm
IsLoading/State == Loading, that the container has non-zero bounds, and that the view is actuallyVisible(an invisible view now pauses its sweep by design — see Lifecycle & performance). - Visual artifacts in mask mode: switch to overlay mode (
shimmer_use_mask="false") or force a software layer, as described above. FindViewByIdreturnsnullfor a Material component: use the concrete type, e.g.Google.Android.Material.Button.MaterialButton, notAndroid.Widget.Button.- A shimmer removed from a
ShimmerGrouplooks frozen: fixed in 1.2.0 — removing a child from aShimmerGroupnow resets it back to driving its own animator. If you're on an older version, setshimmerView.ShimmerDrawable.IsExternallyDriven = falsemanually after removal. - The shimmer isn't sweeping, just pulsing in place: this is
ReduceMotionauto-detection, not a bug — it meansSettings.Global.AnimatorDurationScaleis0on the device/emulator (common on CI runners and emulators configured for faster UI tests). Setapp:shimmer_reduce_motion="false"to force the moving sweep regardless of the system setting.
Contributing
See library_roadmap.md for the current feature roadmap and ideas under consideration. Issues and PRs welcome at the repository.
License
Shimmer.NET is released under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0-android35.0 is compatible. net10.0-android was computed. net10.0-android36.0 is compatible. |
-
net10.0-android36.0
- Xamarin.AndroidX.RecyclerView (>= 1.4.0.6)
-
net9.0-android35.0
- Xamarin.AndroidX.RecyclerView (>= 1.4.0.6)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.