Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Android] Fix gestures in Label Spans #14410

Merged
merged 11 commits into from
May 2, 2023
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<views:BasePage
<views:BasePage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Pages.LabelPage"
Expand Down Expand Up @@ -91,8 +91,7 @@
VerticalTextAlignment="End"
Text="This should be at the bottom"
HeightRequest="100"/>
<Label
>
<Label x:Name="labelFormattedString">
<Label.FormattedText>
<FormattedString>
<Span Text="Plain old Text" />
Expand All @@ -103,7 +102,7 @@
<Span Text=" " />
<Span Text="Should be uppercase" TextTransform="Uppercase" />
<Span Text=" " />
<Span Text="Click Me" FontAttributes="Bold" TextDecorations="Underline" TextColor="Blue">
<Span x:Name="GestureSpan" Text="Click Me" FontAttributes="Bold" TextDecorations="Underline" TextColor="Blue">
<Span.GestureRecognizers>
<TapGestureRecognizer Tapped="ClickGestureRecognizer_Clicked" />
</Span.GestureRecognizers>
Expand All @@ -116,7 +115,58 @@
<Span Text="Plain old Text" />
</FormattedString>
</Label.FormattedText>

</Label>
<Button
Text="Change Formatted String"
Clicked="ChangeFormattedString_Clicked" />
<Label
Text="FormattedText with multiple gestures by Spans"
Style="{StaticResource Headline}"/>
<Label
Margin="10"
HorizontalOptions="Center"
TextColor="Black"
BackgroundColor="CadetBlue">
<Label.FormattedText>
<FormattedString>
<Span
Text="Two clickable spans in one line:&#10;" />
<Span
x:Name="Link1"
TextDecorations="Underline"
Text="Link1"
TextColor="Blue">
<Span.GestureRecognizers>
<TapGestureRecognizer
Tapped="OnLink1Tapped"/>
</Span.GestureRecognizers>
</Span>
<Span
Text=" " />
<Span
x:Name="Link2"
Text="Link2&#10;"
TextDecorations="Underline"
TextColor="Blue">
<Span.GestureRecognizers>
<TapGestureRecognizer
Tapped="OnLink2Tapped" />
</Span.GestureRecognizers>
</Span>
<Span
Text="Multiline tappable span:&#10;" />
<Span
x:Name="Link3"
TextDecorations="Underline"
Text="Link3_1&#10;Link3_2"
TextColor="Blue">
<Span.GestureRecognizers>
<TapGestureRecognizer
Tapped="OnLink3Tapped" />
</Span.GestureRecognizers>
</Span>
</FormattedString>
</Label.FormattedText>
</Label>
<Label
Text="Html text"
Expand Down Expand Up @@ -157,33 +207,6 @@
MaxLines="2"
LineBreakMode ="TailTruncation"
Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." />
<Label x:Name="labelFormattedString"
>
<Label.FormattedText>
<FormattedString>
<Span Text="Plain old Text" />
<Span Text=" " />
<Span Text="Colors" BackgroundColor="Cyan" TextColor="Navy" />
<Span Text=" " />
<Span Text="Strikethrough" TextDecorations="Strikethrough" />
<Span Text=" " />
<Span Text="Should be uppercase" TextTransform="Uppercase" />
<Span Text=" " />
<Span Text="Click Me" FontAttributes="Bold" TextDecorations="Underline" TextColor="Blue">
<Span.GestureRecognizers>
<TapGestureRecognizer Tapped="ClickGestureRecognizer_Clicked" />
</Span.GestureRecognizers>
</Span>
<Span Text=" " />
<Span Text="Big Font" FontSize="20" />
<Span Text=" " />
<Span Text="Different Font" FontFamily="Dokdo" FontSize="18" />
<Span Text=" " />
<Span Text="Plain old Text" />
</FormattedString>
</Label.FormattedText>

</Label>
<Button
Text="Change Formatted String"
Clicked="ChangeFormattedString_Clicked" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
using Maui.Controls.Sample.ViewModels;
using System;
using Maui.Controls.Sample.ViewModels;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;

namespace Maui.Controls.Sample.Pages
{
public partial class LabelPage
{
readonly Color[] _colors = new Color[] { Colors.Red, Colors.Blue, Colors.Green, Colors.Yellow, Colors.Brown, Colors.Purple, Colors.Orange, Colors.Gray };
readonly Random _rand = new Random();

public LabelPage()
{
InitializeComponent();

BindingContext = new LabelViewModel();
}

void ClickGestureRecognizer_Clicked(object sender, System.EventArgs e)
void ClickGestureRecognizer_Clicked(object sender, EventArgs e)
{
var rnd = new System.Random();

if (sender is Span span)
span.TextColor = Color.FromRgb((byte)rnd.Next(0, 254), (byte)rnd.Next(0, 254), (byte)rnd.Next(0, 254));
SetRandomBackgroundColor(GestureSpan);
}

void ChangeFormattedString_Clicked(object sender, System.EventArgs e)
void ChangeFormattedString_Clicked(object sender, EventArgs e)
{
labelFormattedString.FormattedText = new FormattedString
{
Expand All @@ -34,5 +35,34 @@ void ChangeFormattedString_Clicked(object sender, System.EventArgs e)
}
};
}

void OnLink1Tapped(object sender, EventArgs e)
{
SetRandomBackgroundColor(Link1);
}

void OnLink2Tapped(object sender, EventArgs e)
{
SetRandomBackgroundColor(Link2);
}

void OnLink3Tapped(object sender, EventArgs e)
{
SetRandomBackgroundColor(Link3);
}

void SetRandomBackgroundColor(Span span)
{
var oldColor = span.BackgroundColor;

Color newColor = _colors[_rand.Next(_colors.Length)];

while (oldColor == newColor)
{
newColor = _colors[_rand.Next(_colors.Length)];
}

span.BackgroundColor = newColor;
}
}
}
2 changes: 1 addition & 1 deletion src/Controls/src/Core/HandlerImpl/Label/Label.Android.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using System;
using Android.Text;
using Microsoft.Maui.Controls.Platform;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,25 +133,26 @@ internal static SpannableString ToSpannableStringNewWay(

public static void RecalculateSpanPositions(this TextView textView, Label element, SpannableString spannableString, SizeRequest finalSize)
{
if (element?.FormattedText?.Spans == null || element.FormattedText.Spans.Count == 0)
if (element?.FormattedText?.Spans is null || element.FormattedText.Spans.Count == 0)
return;

var labelWidth = finalSize.Request.Width;

if (labelWidth <= 0 || finalSize.Request.Height <= 0)
return;

if (spannableString == null || spannableString.IsDisposed())
return;

var layout = textView.Layout;

if (layout == null)
return;

int next = 0;
int count = 0;
IList<int> totalLineHeights = new List<int>();

var padding = element.Padding;
var padLeft = (int)textView.Context.ToPixels(padding.Left);
var padTop = (int)textView.Context.ToPixels(padding.Top);

#pragma warning disable CA1416
var strlen = spannableString.Length();
Expand All @@ -169,7 +170,7 @@ public static void RecalculateSpanPositions(this TextView textView, Label elemen
continue;

// Find the next span
next = spannableString.NextSpanTransition(i, strlen, type);
next = spannableString.NextSpanTransition(i, spannableString.Length(), type);

// Get all spans in the range - Android can have overlapping spans
var spans = spannableString.GetSpans(i, next, type);
Expand All @@ -180,42 +181,47 @@ public static void RecalculateSpanPositions(this TextView textView, Label elemen
var startSpan = spans[0];
var endSpan = spans[spans.Length - 1];

var startSpanOffset = spannableString.GetSpanStart(startSpan);
mattleibow marked this conversation as resolved.
Show resolved Hide resolved
var endSpanOffset = spannableString.GetSpanEnd(endSpan);

var thisLine = layout.GetLineForOffset(endSpanOffset);
var lineStart = layout.GetLineStart(thisLine);
var lineEnd = layout.GetLineEnd(thisLine);

// If this is true, endSpanOffset has the value for another line that belong to the next span and not it self.
// So it should be rearranged to value not pass the lineEnd.
if (endSpanOffset > (lineEnd - lineStart))
endSpanOffset = lineEnd;
var spanStartOffset = spannableString.GetSpanStart(startSpan);
var spanEndOffset = spannableString.GetSpanEnd(endSpan);

var startX = layout.GetPrimaryHorizontal(startSpanOffset);
var endX = layout.GetPrimaryHorizontal(endSpanOffset);
var spanStartLine = layout.GetLineForOffset(spanStartOffset);
var spanEndLine = layout.GetLineForOffset(spanEndOffset);

var startLine = layout.GetLineForOffset(startSpanOffset);
var endLine = layout.GetLineForOffset(endSpanOffset);

double[] lineHeights = new double[endLine - startLine + 1];

// Calculate all the different line heights
for (var lineCount = startLine; lineCount <= endLine; lineCount++)
// Go through all lines that are affected by the span and calculate a rectangle for each
List<Graphics.Rect> spanRectangles = new List<Graphics.Rect>();
for (var curLine = spanStartLine; curLine <= spanEndLine; curLine++)
{
var lineHeight = layout.GetLineBottom(lineCount) - layout.GetLineTop(lineCount);
lineHeights[lineCount - startLine] = lineHeight;

if (totalLineHeights.Count <= lineCount)
totalLineHeights.Add(lineHeight);
global::Android.Graphics.Rect bounds = new global::Android.Graphics.Rect();
layout.GetLineBounds(curLine, bounds);

var lineHeight = bounds.Height();
var lineStartOffset = layout.GetLineStart(curLine);
var lineVisibleEndOffset = layout.GetLineVisibleEnd(curLine);

var startOffset = (curLine == spanStartLine) ? spanStartOffset : lineStartOffset;
var spanStartX = (int)layout.GetPrimaryHorizontal(startOffset);

var endOffset = (curLine == spanEndLine) ? spanEndOffset : lineVisibleEndOffset;
var spanEndX = (int)layout.GetSecondaryHorizontal(endOffset);

var spanWidth = spanEndX - spanStartX;
var spanLeftX = spanStartX;

// If rtl is used, startX would be bigger than endX
if (spanStartX > spanEndX)
{
spanWidth = spanStartX - spanEndX;
spanLeftX = spanEndX;
}

if (spanWidth > 1)
{
var rectangle = new Graphics.Rect(spanLeftX + padLeft, bounds.Top + padTop, spanWidth, lineHeight);
spanRectangles.Add(rectangle);
}
}

var yaxis = 0.0;

for (var line = startLine; line > 0; line--)
yaxis += totalLineHeights[line];

((ISpatialElement)span).Region = Region.FromLines(lineHeights, labelWidth, startX, endX, yaxis).Inflate(10);
((ISpatialElement)span).Region = Region.FromRectangles(spanRectangles).Inflate(10);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,11 @@ void SetupGestures()
return;

var platformView = Control;

if (platformView == null)
return;

if (View.GestureRecognizers.Count == 0)
if (View.GestureController.CompositeGestureRecognizers.Count == 0)
Comment on lines -182 to +183
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PureWeen is this correct?

{
platformView.Touch -= OnPlatformViewTouched;
}
Expand Down
7 changes: 7 additions & 0 deletions src/Controls/src/Core/Region.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.Maui.Graphics;

namespace Microsoft.Maui.Controls
Expand All @@ -26,6 +27,12 @@ public struct Region : IEquatable<Region>
_inflation = inflation;
}

internal static Region FromRectangles(IEnumerable<Rect> rectangles)
mattleibow marked this conversation as resolved.
Show resolved Hide resolved
{
var list = rectangles.ToList();
return new Region(list);
}

/// <include file="../../docs/Microsoft.Maui.Controls/Region.xml" path="//Member[@MemberName='FromLines']/Docs/*" />
public static Region FromLines(double[] lineHeights, double maxWidth, double startX, double endX, double startY)
{
Expand Down
4 changes: 4 additions & 0 deletions src/Core/tests/DeviceTests.Shared/Stubs/ContextStub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class ContextStub : IMauiContext, IServiceProvider
#endif
#if ANDROID
Android.Content.Context _androidContext;
IFontManager _fontManager;
#endif

#if WINDOWS
Expand All @@ -37,6 +38,9 @@ public object GetService(Type serviceType)
if (serviceType == typeof(IAnimationManager))
return _manager ??= _services.GetRequiredService<IAnimationManager>();
#if ANDROID
if (serviceType == typeof(IFontManager))
return _fontManager ??= _services.GetRequiredService<IFontManager>();

if (serviceType == typeof(Android.Content.Context))
return MauiProgramDefaults.DefaultContext;

Expand Down