Xamarin.Forms Infinite Scrolling Image Background Effect - c#

I'm trying to make what I think will be a nice effect for an app - a series of images (think wallpaper) will be constantly scrolling in the background during a view. I started prototyping this in Xamarin.Forms, creating a custom control. Planned on a diagonal translation but started with the most basic approach and still ran into some issues fairly quickly, namely that it is not entirely smooth as it gets a bit choppy here and there (even when using caching and just a 10kb image) and 2) if user executes an action that's more involved it may cause a lag and the images get rendered more closely together than they should be. Is there a way to fix up this approach so that it's as smooth as possible and doesn't interfere (or get interfered with) the other UI elements, or is there a far superior approach for something like this - anyone ever tackle this? Please let me know, thanks.
FlyingImageBackground.cs
public class FlyingImageBackground : ContentView
{
public static readonly BindableProperty FlyingImageProperty =
BindableProperty.Create(nameof(FlyingImage), typeof(ImageSource), typeof(FlyingImageBackground), default(ImageSource), BindingMode.TwoWay, propertyChanged: OnFlyingImageChanged);
public ImageSource FlyingImage
{
get => (ImageSource)GetValue(FlyingImageProperty);
set => SetValue(FlyingImageProperty, value);
}
private AbsoluteLayout canvas;
public FlyingImageBackground()
{
this.canvas = new AbsoluteLayout()
{
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand
};
this.canvas.SizeChanged += Canvas_SizeChanged;
Content = this.canvas;
}
~FlyingImageBackground() => this.canvas.SizeChanged -= Canvas_SizeChanged;
private static void OnFlyingImageChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (FlyingImageBackground)bindable;
control.BringToLife();
}
private void BringToLife()
{
if (this.canvas.Width <= 0 || this.canvas.Height <= 0)
return;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
Device.BeginInvokeOnMainThread(async () =>
{
await SendImageWave();
});
return this.canvas.IsVisible;
});
}
private async Task SendImageWave()
{
var startingX = -100;
var endingX = this.canvas.Width;
if (endingX <= 0)
return;
endingX += 100;
var yPositions = Enumerable.Range(0, (int)this.canvas.Height).Where(x => x % 90 == 0).ToList();
var imgList = new List<CachedImage>();
foreach (var yPos in yPositions)
{
var img = new CachedImage
{
Source = FlyingImage,
HeightRequest = 50
};
imgList.Add(img);
this.canvas.Children.Add(img, new Point(startingX, yPos));
}
await Task.WhenAll(
imgList.Select(x => x.TranslateTo(endingX, 0, 10000)));
//.Concat(imgList.Select(x => x.TranslateTo(startingX, 0, uint.MinValue))));
imgList.ForEach(x =>
{
this.canvas.Children.Remove(x);
x = null;
});
imgList = null;
}
private void Canvas_SizeChanged(object sender, EventArgs e)
{
BringToLife();
}
}
Usage example:
Just put it into a Grid in a ContentPage along with the main content:
e.g.:
<ContentPage.Content>
<Grid>
<controls:FlyingImageBackground FlyingImage="fireTruck.png" />
<StackLayout HorizontalOptions="Center">
<Button
Text="I'm a button!" />
<Label
FontAttributes="Bold,Italic"
Text="You're a good man, old sport!!!"
TextDecorations="Underline" />
</StackLayout>
</Grid>
</ContentPage.Content>

Switched to SkiaSharp and much better results. The animation appears smooth and if the flow gets interrupted the images maintain their appropriate distance. Also realized in the first draft using the built-in Xamarin Animations that I screwed up the check for when to run it; .IsVisible prop will remain true even if the page isn't even on the screen anymore, so in this new version needed to bind to a property that tells me if the page is actually active or not (based on when it gets navigated to and navigated away from) and if not then stop the animation. This is still just handling the horizontal scrolling effect for now. Hope someone else finds it useful, and any other improvements would be welcome, please just comment/post an answer!
[DesignTimeVisible(true)]
public class FlyingImageBackgroundSkia : ContentView
{
public static readonly BindableProperty IsActiveProperty =
BindableProperty.Create(
nameof(IsActive),
typeof(bool),
typeof(FlyingImageBackground),
default(bool),
BindingMode.TwoWay,
propertyChanged: OnPageActivenessChanged);
private SKCanvasView canvasView;
private SKBitmap resourceBitmap;
private Stopwatch stopwatch = new Stopwatch();
// consider making these bindable props
private float percentComplete;
private float imageSize = 40;
private float columnSpacing = 100;
private float rowSpacing = 100;
private float framesPerSecond = 60;
private float cycleTime = 1; // in seconds, for a single column
public FlyingImageBackgroundSkia()
{
this.canvasView = new SKCanvasView();
this.canvasView.PaintSurface += OnCanvasViewPaintSurface;
this.Content = this.canvasView;
string resourceID = "XamarinTestProject.Resources.Images.fireTruck.png";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
this.resourceBitmap = SKBitmap.Decode(stream);
}
}
~FlyingImageBackgroundSkia() => this.resourceBitmap.Dispose();
public bool IsActive
{
get => (bool)GetValue(IsActiveProperty);
set => SetValue(IsActiveProperty, value);
}
private static async void OnPageActivenessChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (FlyingImageBackgroundSkia)bindable;
await control.AnimationLoop();
}
private async Task AnimationLoop()
{
this.stopwatch.Start();
while (IsActive)
{
this.percentComplete = (float)(this.stopwatch.Elapsed.TotalSeconds % this.cycleTime) / this.cycleTime; // always between 0 and 1
this.canvasView.InvalidateSurface(); // trigger redraw
await Task.Delay(TimeSpan.FromSeconds(1.0 / this.framesPerSecond)); // non-blocking
}
this.stopwatch.Stop();
}
private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
var xPositions = Enumerable.Range(0, info.Width + (int)this.columnSpacing).Where(x => x % (int)this.columnSpacing == 0).ToList();
xPositions.Insert(0, -(int)this.columnSpacing);
var yPositions = Enumerable.Range(0, info.Height + (int)this.rowSpacing).Where(x => x % (int)this.rowSpacing == 0).ToList();
yPositions.Insert(0, -(int)this.rowSpacing);
if (this.resourceBitmap != null)
{
foreach (var xPos in xPositions)
{
var xPosNow = xPos + (this.rowSpacing * this.percentComplete);
foreach (var yPos in yPositions)
{
canvas.DrawBitmap(
this.resourceBitmap,
new SKRect(xPosNow, yPos, xPosNow + this.imageSize, yPos + this.imageSize));
}
}
}
}
}

Related

C# Manually stopping an asynchronous for-statement (typewriter effect)

I'm making a retro-style game with C# .NET-Framework, and for dialogue I'm using a for-statement, that prints my text letter by letter (like a typewriter-effect):
I'm working with different scenes, and I have a skip button (bottom right) that skips the current dialogue and passes to the next scene. My typewriter-effect automatically stops when all the text is displayed, but when I click on the skip button, it automatically skips to the next scene.
I would like it, when the typewriter is still active, and if I click on the skip button, that it first shows all the text, instead of skipping to the next scene.
So that it only skips to the next scene when all the text is displayed (automatically or manually).
This is the (working code) that I'm using for my typewriter method (+ variables):
public string FullTextBottom;
public string CurrentTextBottom = "";
public bool IsActive;
public async void TypeWriterEffectBottom()
{
if(this.BackgroundImage != null) // only runs on backgrounds that arent black
{
for(i=0; i < FullTextBottom.Length + 1; i++)
{
CurrentTextBottom = FullTextBottom.Substring(0, i); // updating current string with one extra letter
LblTextBottom.Text = CurrentTextBottom; // "temporarily place string in text box"
await Task.Delay(30); // wait for next update
#region checks for IsActive // for debugging only!
if(i < FullTextBottom.Length + 1)
{
IsActive = true;
Debug1.Text = "IsActive = " + IsActive.ToString();
}
if(CurrentTextBottom.Length == FullTextBottom.Length)
{
IsActive = false;
Debug1.Text = "IsActive = " + IsActive.ToString();
}
#endregion
}
}
}
And this is the code that I want to get for my skip button (named Pb_FastForward):
private void PbFastForward_Click(object sender, EventArgs e)
{
if( //typewriter is active)
{
//print all text into the textbox
}
else if( //all text is printed)
{
// skip to the next scene
}
}
But I don't know how to formulate the 2nd part of code. I've tried many different approaches, like using counters that increase on a buttonclick (and using that to check in an if-statement), and many different types of if-statements to see if the typewriter is still active or not, but I haven't got anything to work yet.
Edit
This is the sequence in which different components need to be loaded (on button click), which is related to the way different variables are updated:
Gamestate_Cycle() --> called for loading new scene.
FullTextBottom = LblTextBottom.Text --> called to refresh variables for typewriter.
TypeWriterEffectBottom() --> called to perform typewriter effect.
Avoid async void. Otherwise you can get an Exception that will break your game and you will not able to catch it.
Then use as less global variables in async methods as possible.
I suggest CancellationTokenSource as thread-safe way to stop the Type Writer.
public async Task TypeWriterEffectBottom(string text, CancellationToken token)
{
if (this.BackgroundImage != null)
{
Debug1.Text = "TypeWriter is active";
StringBuilder sb = new StringBuilder(text.Length);
try
{
foreach (char c in text)
{
LblTextBottom.Text = sb.Append(c).ToString();
await Task.Delay(30, token);
}
}
catch (OperationCanceledException)
{
LblTextBottom.Text = text;
}
Debug1.Text = "TypeWriter is finished";
}
}
Define CTS. It's thread-safe, so it's ok to have it in global scope.
private CancellationTokenSource cts = null;
Call TypeWriter from async method to be able to await it.
// set button layout as "Skip text" here
using (cts = new CancellationTokenSource())
{
await TypeWriterEffectBottom(yourString, cts.Token);
}
cts = null;
// set button layout as "Go to the next scene" here
And finally
private void PbFastForward_Click(object sender, EventArgs e)
{
if (cts != null)
{
cts?.Cancel();
}
else
{
// go to the next scene
}
}
I pondered on your task a bit more and it occurred to me that it is a good job for the Rx.Net library.
An advantage of this approach is that you have less mutable state to care about and you almost don't need to think about threads, synchronization, etc.; you manipulate higher-level building blocks instead: observables, subscriptions.
I extended the task a bit to better illustrate Rx capabilities:
there are two pieces of animated text, each one can be fast-forwarded separately;
the user can fast-forward to the final state;
the user can reset the animation state.
Here is the form code (C# 8, System.Reactive.Linq v4.4.1):
private enum DialogState
{
NpcSpeaking,
PlayerSpeaking,
EverythingShown
}
private enum EventKind
{
AnimationFinished,
Skip,
SkipToEnd
}
DialogState _state;
private readonly Subject<DialogState> _stateChanges = new Subject<DialogState>();
Dictionary<DialogState, (string, Label)> _lines;
IDisposable _eventsSubscription;
IDisposable _animationSubscription;
public Form1()
{
InitializeComponent();
_lines = new Dictionary<DialogState, (string, Label)>
{
{ DialogState.NpcSpeaking, ("NPC speaking...", lblNpc) },
{ DialogState.PlayerSpeaking, ("Player speaking...", lblCharacter) },
};
// tick = 1,2...
IObservable<long> tick = Observable
.Interval(TimeSpan.FromSeconds(0.15))
.ObserveOn(this)
.StartWith(-1)
.Select(x => x + 2);
IObservable<EventPattern<object>> fastForwardClicks = Observable.FromEventPattern(
h => btnFastForward.Click += h,
h => btnFastForward.Click -= h);
IObservable<EventPattern<object>> skipToEndClicks = Observable.FromEventPattern(
h => btnSkipToEnd.Click += h,
h => btnSkipToEnd.Click -= h);
// On each state change animationFarames starts from scratch: 1,2...
IObservable<long> animationFarames = _stateChanges
.Select(
s => Observable.If(() => _lines.ContainsKey(s), tick.TakeUntil(_stateChanges)))
.Switch();
var animationFinished = new Subject<int>();
_animationSubscription = animationFarames.Subscribe(frame =>
{
(string line, Label lbl) = _lines[_state];
if (frame > line.Length)
{
animationFinished.OnNext(default);
return;
}
lbl.Text = line.Substring(0, (int)frame);
});
IObservable<EventKind> events = Observable.Merge(
skipToEndClicks.Select(_ => EventKind.SkipToEnd),
fastForwardClicks.Select(_ => EventKind.Skip),
animationFinished.Select(_ => EventKind.AnimationFinished));
_eventsSubscription = events.Subscribe(e =>
{
DialogState prev = _state;
_state = prev switch
{
DialogState.NpcSpeaking => WhenSpeaking(e, DialogState.PlayerSpeaking),
DialogState.PlayerSpeaking => WhenSpeaking(e, DialogState.EverythingShown),
DialogState.EverythingShown => WhenEverythingShown(e)
};
_stateChanges.OnNext(_state);
});
Reset();
}
private DialogState WhenEverythingShown(EventKind _)
{
Close();
return _state;
}
private DialogState WhenSpeaking(EventKind e, DialogState next)
{
switch (e)
{
case EventKind.AnimationFinished:
case EventKind.Skip:
{
(string l, Label lbl) = _lines[_state];
lbl.Text = l;
return next;
}
case EventKind.SkipToEnd:
{
ShowFinalState();
return DialogState.EverythingShown;
}
default:
throw new NotSupportedException($"Unknown event '{e}'.");
}
}
private void ShowFinalState()
{
foreach ((string l, Label lbl) in _lines.Values)
{
lbl.Text = l;
}
}
private void Reset()
{
foreach ((_, Label lbl) in _lines.Values)
{
lbl.Text = "";
}
_state = DialogState.NpcSpeaking;
_stateChanges.OnNext(_state);
}
protected override void OnClosed(EventArgs e)
{
_eventsSubscription?.Dispose();
_animationSubscription?.Dispose();
base.OnClosed(e);
}
private void btnReset_Click(object sender, EventArgs e)
{
Reset();
}
I adjusted your code a little bit to achieve your goal. I'm not sure it's the best way to do it, but it should work.
public async void TypeWriterEffectBottom()
{
if(this.BackgroundImage == null)
{
return;
}
IsActive = true;
for(i=0; i < FullTextBottom.Length && IsActive; i++)
{
CurrentTextBottom = FullTextBottom.Substring(0, i+1);
LblTextBottom.Text = CurrentTextBottom;
await Task.Delay(30);
Debug1.Text = "IsActive = " + IsActive.ToString();
}
IsActive = false;
}
private void PbFastForward_Click(object sender, EventArgs e)
{
if(IsActive)
{
LblTextBottom.Text = FullTextBottom;
IsActive = false;
return;
}
// IsActive == false means all text is printed
// skip to the next scene
}
UPD: Just noticed that Hans Kesting has suggested pretty much exactly this in his comment.
You write what skip / forward button does, so you control it. Just have a check if the length of written text is equal to text that supposed to be written and if yes move as usual if not just display the text in full have delay to be read and move on

Why I don't see any more the TextField inside OnGUI in EditorWindow script?

using UnityEngine;
using UnityEditor;
public class SearchableWindow : EditorWindow
{
string searchString = "";
[MenuItem("Tools/Searching")]
private static void Searching()
{
const int width = 340;
const int height = 420;
var x = (Screen.currentResolution.width - width) / 2;
var y = (Screen.currentResolution.height - height) / 2;
GetWindow<SearchableWindow>().position = new Rect(x, y, width, height);
}
void OnGUI()
{
GUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.FlexibleSpace();
searchString = GUILayout.TextField(searchString, EditorStyles.toolbarTextField);
GUILayout.EndHorizontal();
var items = Selection.gameObjects;
// Do comparison here. For example
for (int i = 0; i < items.Length; i++)
{
if (items[i].name.Contains(searchString))
{
GUILayout.Label(items[i].name);
}
}
}
}
Before it was working fine but now everything is very slow and also I don't see the TextField and when selecting a GameObject in the Hierarchy it's taking almost 5 seconds to show it in the Editor Window.
And before it was all fast and showing the whole Hierarchy in the Editor Window.
Now it's empty:
I took the answer from this question:
Searchable
OnGUI is only called while the mouse is (moved/clicked) over the according Window -> it is not taking almost 5 seconds but until you move your mouse over the window again.
In order to solve that you could implement EditorWindow.OnSelectionChange and force a EditorWindow.Repaint so your window is refreshed everytime the Selection changes.
private void OnSelectionChange()
{
Repaint();
}
The second issue is produced as follows:
GUILayout.TextField together with GUILayout.FlexibleSpace if not defined different uses automatically the width of the inserted text -> Since you have an empty text at start the width is almost 0 ... you actually can see your TextField very thin there and it gets bigger as you fill it:
Using the drawers from the EditorGUILayout instead solves this:
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.FlexibleSpace();
searchString = EditorGUILayout.TextField(searchString, EditorStyles.toolbarTextField);
EditorGUILayout.EndHorizontal();
var items = Selection.gameObjects;
// Do comparison here. For example
foreach (var selectedObject in selected)
{
if (selectedObject .name.Contains(searchString))
{
EditorGUILayout.LabelField(selectedObject.name);
}
}
Tip If you replace
EditorGUILayout.LabelField(items[i].name);
with
if (GUILayout.Button(selectedObject.name, EditorStyles.label))
{
EditorGUIUtility.PingObject(selectedObject);
}
you can click on the name and "ping" the according GameObject in the hierachy!
Update since asked in the comments
If you want to include all children of the selection recursive you could do something like (there might be better ways but that's what I came up with within 10 minutes)
private static IEnumerable<GameObject> GetChildrenRecursive(GameObject root)
{
var output = new List<GameObject>();
//add the root object itself
output.Add(root);
// iterate over direct children
foreach (Transform child in root.transform)
{
// add the child itslef
output.Add(child.gameObject);
// Recursion here: Get all subchilds of this child
var childsOfchild = GetChildrenRecursive(child.gameObject);
output.AddRange(childsOfchild);
}
return output;
}
private static IEnumerable<GameObject> GetChildrenRecursive(IEnumerable<GameObject> rootObjects)
{
var output = new List<GameObject>();
foreach (var root in rootObjects)
{
output.AddRange(GetChildrenRecursive(root));
}
// remove duplicates
return output.Distinct().ToList();
}
and using
var selected = GetChildrenRecursive(Selection.gameObjects);
Another Update
Since this might not be very efficient you probably should move it to Searching and than only refresh the selected value also in OnSelectionChange like
public class SearchableWindow : EditorWindow
{
private string searchString = "";
private List<GameObject> selected;
[MenuItem("Tools/Searching")]
private static void Searching()
{
const int width = 340;
const int height = 420;
var x = (Screen.currentResolution.width - width) / 2;
var y = (Screen.currentResolution.height - height) / 2;
var window = GetWindow<SearchableWindow>();
window.position = new Rect(x, y, width, height);
window.selected = GetChildrenRecursive(Selection.gameObjects).ToList();
}
private void OnSelectionChange()
{
selected = GetChildrenRecursive(Selection.gameObjects).ToList();
Repaint();
}
private void OnGUI()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.FlexibleSpace();
searchString = EditorGUILayout.TextField(searchString, EditorStyles.toolbarTextField);
EditorGUILayout.EndHorizontal();
// only as fallback
if (selected == null)
{
selected = GetChildrenRecursive(Selection.gameObjects).ToList();
}
// Do comparison here. For example
foreach (var selectedObject in selected)
{
if (selectedObject.name.Contains(searchString))
{
if (GUILayout.Button(selectedObject.name, EditorStyles.label))
{
EditorGUIUtility.PingObject(selectedObject);
}
}
}
}
private static IEnumerable<GameObject> GetChildrenRecursive(GameObject root)
{
var output = new List<GameObject>();
//add the root object itself
output.Add(root);
// iterate over direct children
foreach (Transform child in root.transform)
{
// add the children themselves
output.Add(child.gameObject);
var childsOfchild = GetChildrenRecursive(child.gameObject);
output.AddRange(childsOfchild);
}
return output;
}
private static IEnumerable<GameObject> GetChildrenRecursive(IEnumerable<GameObject> rootObjects)
{
var output = new List<GameObject>();
foreach (var root in rootObjects)
{
output.AddRange(GetChildrenRecursive(root));
}
// remove any duplicates that would e.g. appear if you select a parent and its child
return output.Distinct();
}
}

Geometry with TranslateTransform sometimes loses fill

I have a framework for drawing made in C#. Recently I've been trying to do something and noticed the following problem:
When I draw a Geometry manually on DrawingVisual using its RenderOpen, and then move it using TranslateTransform, I sometime lose the fill.
To see what happens, you can insert the following framework element to a Window and run it:
class MyVisual : FrameworkElement {
private readonly DrawingVisual _visual = new DrawingVisual();
private readonly Geometry _geom;
private readonly Random _r = new Random();
public MyVisual()
{
AddVisualChild(_visual);
_geom = Geometry.Parse("M 95 100 L 130 130 95 160 Z").Clone();
_geom.Transform = new TranslateTransform();
UpdateVisual();
}
public void MoveGeom() {
var transform = _geom.Transform as TranslateTransform;
var x = _r.Next(-60, 200);
var y = _r.Next(-60, 200);
transform.X = x;
transform.Y = y;
}
void UpdateVisual()
{
using (var dc = _visual.RenderOpen())
{
UpdateVisual(dc);
}
}
private void UpdateVisual(DrawingContext dc)
{
var color = Brushes.Red;
var pen = new Pen(Brushes.Blue, 1);
dc.DrawGeometry(color, pen, _geom);
}
protected override int VisualChildrenCount => 1;
protected override Visual GetVisualChild(int index) => _visual;
}
public partial class MainWindow : Window
{
Timer _t = new Timer(500) { AutoReset = true };
public MainWindow()
{
InitializeComponent();
_t.Elapsed += (x, y) => Dispatcher.Invoke(() => _vis.MoveGeom());
_t.Start();
}
}
Is this a known issue, is there some simple workaround for it, or some other solution?
This solution seems to solve this issue:
public void MoveGeom()
{
var x = _r.Next(-60, 200);
var y = _r.Next(-60, 200);
_geom.Transform = new TranslateTransform(x, y);
}
Obviously there is a problem when the two coordinates are set separately because setting the single coordinates causes two updates and the rendering process is confused by that in any way.

Xamarin.Forms: Change RelativeLayout constraints afterwards

Is it possible to change the constraints of a RelativeLayout after they have been set one time?
In the documentation I see methods like GetBoundsConstraint(BindableObject), but I think you can only use them if you have a XAML file. For now I'm trying to do this in code. I get null if I try
RelativeLayout.GetBoundsConstraint(this.label);
The only way I found out is to remove the children from the layout and add it with the new constraints again.
Example:
public class TestPage : ContentPage
{
private RelativeLayout relativeLayout;
private BoxView view;
private bool moreWidth = false;
public TestPage()
{
this.view = new BoxView
{
BackgroundColor = Color.Yellow,
};
Button button = new Button
{
Text = "change",
TextColor = Color.Black,
};
button.Clicked += Button_Clicked;
this.relativeLayout = new RelativeLayout();
this.relativeLayout.Children.Add(this.view,
Constraint.Constant(0),
Constraint.Constant(0),
Constraint.Constant(80),
Constraint.RelativeToParent((parent) =>
{
return parent.Height;
}));
this.relativeLayout.Children.Add(button,
Constraint.RelativeToParent((parent) =>
{
return parent.Width / 2;
}),
Constraint.RelativeToParent((parent) =>
{
return parent.Height / 2;
}));
this.Content = this.relativeLayout;
}
private void Button_Clicked(object sender, EventArgs e)
{
double width = 0;
if(this.moreWidth)
{
width = 120;
}
else
{
width = 80;
}
var c= BoundsConstraint.FromExpression((Expression<Func<Rectangle>>)(() => new Rectangle(0, 0, width, this.Content.Height)), new View[0]);
RelativeLayout.SetBoundsConstraint(this.view, c);
this.relativeLayout.ForceLayout();
this.moreWidth = !this.moreWidth;
}
}
It does not officially possible with the current version of Xamarin Forms. The RelativeLayout container only recomputes constraints when adding/removing items from its children collection (it caches the solved constraints - presumable for performance). Even though the various constraints are implemented as Bindable Properties, they still do not get recomputed when changed.
I assume that the intention is to someday respect constraint updates, which would be useful with animations for example, but for now it doesn't appear to work that way.
HOWEVER, I took a look at the decompiled source for RelativeLayout and it is possible to hack together a way around it - but it might not suit your needs, depending on how much functionality you require and how complex your constraint definitions are.
See this example code (the key part is setting the constraint using SetBoundsConstraint, which overrides the internally computed bounds of the added view - and then calling ForceLayout()):
public partial class App : Application
{
public App ()
{
var label = new Label {
Text = "Test",
HorizontalTextAlignment = TextAlignment.Center,
VerticalTextAlignment = TextAlignment.Center,
BackgroundColor = Color.Silver
};
var layout = new RelativeLayout ();
layout.Children.Add (label,
Constraint.Constant (50),
Constraint.Constant (100),
Constraint.Constant (260),
Constraint.Constant (30));
MainPage = new ContentPage {
Content = layout
};
var fwd = true;
layout.Animate ("bounce",
(delta) => {
var d = fwd ? delta : 1.0 - delta;
var y = 100.0 + (50.0 * d);
var c = BoundsConstraint.FromExpression ((Expression<Func<Rectangle>>)(() => new Rectangle (50, y, 260, 30)), new View [0]);
RelativeLayout.SetBoundsConstraint(label, c);
layout.ForceLayout ();
}, 16, 800, Easing.SinInOut, (f, b) => {
// reset direction
fwd = !fwd;
}, () => {
// keep bouncing
return true;
});
}
}
Yes. This possible.
Layout code:
<StackLayout RelativeLayout.XConstraint="{Binding XConstaint}" ...>
VM code:
public Constraint XConstaint
{
get => _xConstaint;
set { SetFieldValue(ref _xConstaint, value, nameof(XConstaint)); }
}
public override void OnAppearing()
{
base.OnAppearing();
XConstaint = Constraint.RelativeToParent((parent) => { return parent.Width - 128; });
}
If i understood you correctly , I would try the following
Implement SizeChanged event for relativeLayout
like :-
var relativeLayout = new RelativeLayout ();
relativeLayout.SizeChanged += someEventForTesting;
and invoke the event whenever you want to resize the relative layout or some particular child.
public void someEventForTesting(Object sender , EventArgs)
{
var layout = (RelativeLayout)sender ;
//here you have access to layout and every child , you can add them again with new constraint .
}

Why is Xamarin.Forms so slow when displaying a few labels (especially on Android)?

We are trying to release some productive Apps with Xamarin.Forms but one of our main issues is the overall slowness between button pressing and displaying of content. After a few experiments, we discovered that even a simple ContentPage with 40 labels take more than 100 ms to show up:
public static class App
{
public static DateTime StartTime;
public static Page GetMainPage()
{
return new NavigationPage(new StartPage());
}
}
public class StartPage : ContentPage
{
public StartPage()
{
Content = new Button {
Text = "Start",
Command = new Command(o => {
App.StartTime = DateTime.Now;
Navigation.PushAsync(new StopPage());
}),
};
}
}
public class StopPage : ContentPage
{
public StopPage()
{
Content = new StackLayout();
for (var i = 0; i < 40; i++)
(Content as StackLayout).Children.Add(new Label{ Text = "Label " + i });
}
protected override void OnAppearing()
{
((Content as StackLayout).Children[0] as Label).Text = "Stop after " + (DateTime.Now - App.StartTime).TotalMilliseconds + " ms";
base.OnAppearing();
}
}
Especially on Android it get's worse the more labels you're trying to display. The first button press (which is crucial for the user) even takes ~300 ms. We need to show something on the screen in less than 30 ms to create a good user experience.
Why does it take so long with Xamarin.Forms to display some simple labels? And how to work around this issue to create a shippable App?
Experiments
The code can be forked on GitHub at https://github.com/perpetual-mobile/XFormsPerformance
I've also written a small example to demonstrate that similar code utilizing the native APIs from Xamarin.Android is significantly faster and does not get slower when adding more content: https://github.com/perpetual-mobile/XFormsPerformance/tree/android-native-api
Xamarin Support Team wrote me:
The team is aware of the issue, and they are working on optimising the
UI initialisation code. You may see some improvements in upcoming
releases.
Update: after seven month of idling, Xamarin changed the bug report status to 'CONFIRMED'.
Good to know. So we have to be patient. Fortunately Sean McKay over in Xamarin Forums suggested to override all layouting code to improve performance: https://forums.xamarin.com/discussion/comment/87393#Comment_87393
But his suggestion also means that we have to write the complete label code again. Here is an version of a FixedLabel which does not do the costly layout cycles and has a some features like bindable properties for text and color. Using this instead of Label improves performance by 80% and more depending on the number of labels and where they occur.
public class FixedLabel : View
{
public static readonly BindableProperty TextProperty = BindableProperty.Create<FixedLabel,string>(p => p.Text, "");
public static readonly BindableProperty TextColorProperty = BindableProperty.Create<FixedLabel,Color>(p => p.TextColor, Style.TextColor);
public readonly double FixedWidth;
public readonly double FixedHeight;
public Font Font;
public LineBreakMode LineBreakMode = LineBreakMode.WordWrap;
public TextAlignment XAlign;
public TextAlignment YAlign;
public FixedLabel(string text, double width, double height)
{
SetValue(TextProperty, text);
FixedWidth = width;
FixedHeight = height;
}
public Color TextColor {
get {
return (Color)GetValue(TextColorProperty);
}
set {
if (TextColor != value)
return;
SetValue(TextColorProperty, value);
OnPropertyChanged("TextColor");
}
}
public string Text {
get {
return (string)GetValue(TextProperty);
}
set {
if (Text != value)
return;
SetValue(TextProperty, value);
OnPropertyChanged("Text");
}
}
protected override SizeRequest OnSizeRequest(double widthConstraint, double heightConstraint)
{
return new SizeRequest(new Size(FixedWidth, FixedHeight));
}
}
The Android Renderer looks like this:
public class FixedLabelRenderer : ViewRenderer
{
public TextView TextView;
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.View> e)
{
base.OnElementChanged(e);
var label = Element as FixedLabel;
TextView = new TextView(Context);
TextView.Text = label.Text;
TextView.TextSize = (float)label.Font.FontSize;
TextView.Gravity = ConvertXAlignment(label.XAlign) | ConvertYAlignment(label.YAlign);
TextView.SetSingleLine(label.LineBreakMode != LineBreakMode.WordWrap);
if (label.LineBreakMode == LineBreakMode.TailTruncation)
TextView.Ellipsize = Android.Text.TextUtils.TruncateAt.End;
SetNativeControl(TextView);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Text")
TextView.Text = (Element as FixedLabel).Text;
base.OnElementPropertyChanged(sender, e);
}
static GravityFlags ConvertXAlignment(Xamarin.Forms.TextAlignment xAlign)
{
switch (xAlign) {
case Xamarin.Forms.TextAlignment.Center:
return GravityFlags.CenterHorizontal;
case Xamarin.Forms.TextAlignment.End:
return GravityFlags.End;
default:
return GravityFlags.Start;
}
}
static GravityFlags ConvertYAlignment(Xamarin.Forms.TextAlignment yAlign)
{
switch (yAlign) {
case Xamarin.Forms.TextAlignment.Center:
return GravityFlags.CenterVertical;
case Xamarin.Forms.TextAlignment.End:
return GravityFlags.Bottom;
default:
return GravityFlags.Top;
}
}
}
And here the iOS Render:
public class FixedLabelRenderer : ViewRenderer<FixedLabel, UILabel>
{
protected override void OnElementChanged(ElementChangedEventArgs<FixedLabel> e)
{
base.OnElementChanged(e);
SetNativeControl(new UILabel(RectangleF.Empty) {
BackgroundColor = Element.BackgroundColor.ToUIColor(),
AttributedText = ((FormattedString)Element.Text).ToAttributed(Element.Font, Element.TextColor),
LineBreakMode = ConvertLineBreakMode(Element.LineBreakMode),
TextAlignment = ConvertAlignment(Element.XAlign),
Lines = 0,
});
BackgroundColor = Element.BackgroundColor.ToUIColor();
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Text")
Control.AttributedText = ((FormattedString)Element.Text).ToAttributed(Element.Font, Element.TextColor);
base.OnElementPropertyChanged(sender, e);
}
// copied from iOS LabelRenderer
public override void LayoutSubviews()
{
base.LayoutSubviews();
if (Control == null)
return;
Control.SizeToFit();
var num = Math.Min(Bounds.Height, Control.Bounds.Height);
var y = 0f;
switch (Element.YAlign) {
case TextAlignment.Start:
y = 0;
break;
case TextAlignment.Center:
y = (float)(Element.FixedHeight / 2 - (double)(num / 2));
break;
case TextAlignment.End:
y = (float)(Element.FixedHeight - (double)num);
break;
}
Control.Frame = new RectangleF(0, y, (float)Element.FixedWidth, num);
}
static UILineBreakMode ConvertLineBreakMode(LineBreakMode lineBreakMode)
{
switch (lineBreakMode) {
case LineBreakMode.TailTruncation:
return UILineBreakMode.TailTruncation;
case LineBreakMode.WordWrap:
return UILineBreakMode.WordWrap;
default:
return UILineBreakMode.Clip;
}
}
static UITextAlignment ConvertAlignment(TextAlignment xAlign)
{
switch (xAlign) {
case TextAlignment.Start:
return UITextAlignment.Left;
case TextAlignment.End:
return UITextAlignment.Right;
default:
return UITextAlignment.Center;
}
}
}
What you are measuring here is the sum of:
the time it takes to create the page
to layout it
to animate it on screen
On a nexus5, I observe times in the magnitude of 300ms for the first call, and of 120ms for subsequent ones.
This is because the OnAppearing() will only be invoked when the view is fully animated in place.
You can easily measure the animation time by replacing your app with:
public class StopPage : ContentPage
{
public StopPage()
{
}
protected override void OnAppearing()
{
System.Diagnostics.Debug.WriteLine ((DateTime.Now - App.StartTime).TotalMilliseconds + " ms");
base.OnAppearing();
}
}
and I observe times like:
134.045 ms
2.796 ms
3.554 ms
This gives some insights:
- there's no animation on PushAsync on android (there's on iPhone, taking 500ms)
- the first time you push the page, you pay a 120ms tax, due to a new allocation.
- XF is doing a good job at reusing page renderers if possible
What we're interested into is the time for displaying the 40 labels, nothing else. Let's change the code again:
public class StopPage : ContentPage
{
public StopPage()
{
}
protected override void OnAppearing()
{
App.StartTime = DateTime.Now;
Content = new StackLayout();
for (var i = 0; i < 40; i++)
(Content as StackLayout).Children.Add(new Label{ Text = "Label " + i });
System.Diagnostics.Debug.WriteLine ((DateTime.Now - App.StartTime).TotalMilliseconds + " ms");
base.OnAppearing();
}
}
times observed (on 3 calls):
264.015 ms
186.772 ms
189.965 ms
188.696 ms
That's still a bit too much, but as the ContentView is set first, this is measuring 40 layout cycles, as each new Label is redrawing the screen. Let's change that:
public class StopPage : ContentPage
{
public StopPage()
{
}
protected override void OnAppearing()
{
App.StartTime = DateTime.Now;
var layout = new StackLayout();
for (var i = 0; i < 40; i++)
layout.Children.Add(new Label{ Text = "Label " + i });
Content = layout;
System.Diagnostics.Debug.WriteLine ((DateTime.Now - App.StartTime).TotalMilliseconds + " ms");
base.OnAppearing();
}
}
And here are my measurements:
178.685 ms
110.221 ms
117.832 ms
117.072 ms
This is becoming very reasonable, esp. given that you're drawing (instantiating, and measuring) 40 labels when your screen can only display 20.
There's indeed yet some room for improvement, but the situation is not as bad as it seems. The 30ms rule for mobile says that everything that takes more than 30ms should be async and not block the UI. Here it takes a bit more than 30ms to switch a page, but from a user point of view, I don't perceive this as slow.
In the setters of the Text and TextColor properties of the FixedLabel class, the code says:
If the new value is "different" from the current one, then do nothing!
It should be with the opposite condition, so that if the new value is the same as the current one, then there is nothing to do.

Categories