Shimmer.NET
1.4.1
dotnet add package Shimmer.NET --version 1.4.1
NuGet\Install-Package Shimmer.NET -Version 1.4.1
<PackageReference Include="Shimmer.NET" Version="1.4.1" />
<PackageVersion Include="Shimmer.NET" Version="1.4.1" />
<PackageReference Include="Shimmer.NET" />
paket add Shimmer.NET --version 1.4.1
#r "nuget: Shimmer.NET, 1.4.1"
#:package Shimmer.NET@1.4.1
#addin nuget:?package=Shimmer.NET&version=1.4.1
#tool nuget:?package=Shimmer.NET&version=1.4.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.4.0" />
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.AutoSkeleton()to turn your real content into a skeleton while loading: every descendantTextView/ImageViewgets a rounded placeholder bar with its text/image hidden, restored automatically when loading ends. It renders in mask mode with the base color kept, so only the bars are painted. Bars use each view's own bounds, so give text views a fixed size or placeholder text; nested Shimmer widgets are skipped.
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 re-scanned only when the view hierarchy or layout changes. The clock runs only while the group is attached, its window has focus, and at least one child is loading. Set UseSharedShader = true to build one gradient for children of identical size and appearance instead of one per child (IsClockRunning and ShimmerDrawable.IsUsingSharedShader expose the state).
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
bool MaskUsesBaseColor { get; set; } // with UseMask: repaint masked content in base/highlight colors (skeleton look)
bool IsUsingSharedShader { get; } // true while a ShimmerGroup shares its shader with this drawable
event EventHandler? LoadingStateChanged; // raised when IsLoading changes
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
void AutoSkeleton(); // skeleton from real TextViews/ImageViews while Loading
ShimmerGroup
long Duration { get; set; }
long RepeatDelay { get; set; }
bool UseSharedShader { get; set; } // share one gradient across identical children
bool IsClockRunning { get; } // false when nothing is loading or the window lacks focus
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.- All shimmer widgets also pause while the window loses focus (app backgrounded, dialog or notification shade on top) even though they stay attached, and
ShimmerGroupstops its clock entirely while no child is loading. - 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. Plain mask mode omits the base color, so content is only visible under the moving highlight; set MaskUsesBaseColor = true to keep the base color and repaint the masked content as a full skeleton (this is what AutoSkeleton() and ShimmerPresets use). 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.