Skip to content

Commit

Permalink
Clean-up and fix VisualTreeElementExtensions hit testing (#5892)
Browse files Browse the repository at this point in the history
* repro

* HitTestPage

* working implementation

* remove usePlatformViewBounds

* improve HitTestingPage

* remove view.Frame visualization

* use IReadOnlyList

* improve xml docs

* undo MainPage.xaml change

* undo unrelated change

* revert csproj changes

* implement HitTestPage on non-WINDOWS

* Update src/Core/src/Core/Extensions/VisualTreeElementExtensions.cs

Co-authored-by: Jonathan Dick <[email protected]>
  • Loading branch information
antonfirsov and Redth authored Apr 18, 2022
1 parent 4410fdc commit 9ac65b0
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 59 deletions.
37 changes: 37 additions & 0 deletions src/Controls/samples/Controls.Sample/Pages/HitTestingPage.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Pages.HitTestingPage"
BackgroundColor="{DynamicResource SecondaryColor}"
Loaded="ContentPage_Loaded"
Unloaded="ContentPage_Unloaded">

<ScrollView>
<VerticalStackLayout Spacing="25" Padding="30" >

<Label x:Name="SelectionLabel" Text="Selected: -" HorizontalOptions="Center" />
<HorizontalStackLayout>
<Label Text="Rectangle Selection" VerticalOptions="Center" FontSize="8" />
<CheckBox x:Name="RectangleSelectionCheckBox" CheckedChanged="RectangleSelectionCheckBox_CheckedChanged" />
</HorizontalStackLayout>

<Label Text="Lorem ipsum dolor sit ame" FontSize="18" FontAttributes="Bold" HorizontalOptions="Start" />
<Label Text="Lorem ipsum dolor sit ame" FontSize="18" FontAttributes="Bold" HorizontalOptions="End" />

<Button Text="Scale = 1" Scale="1" FontAttributes="Bold" HorizontalOptions="Center" />
<Button Text="Scale = 2" Scale="2" FontAttributes="Bold" HorizontalOptions="Center" />
<Button Text="Rotation = 20" Rotation="20" HorizontalOptions="Center" />

<Ellipse WidthRequest="150" HeightRequest="50" StrokeThickness="10" Stroke="Green" />
<RoundRectangle WidthRequest="300" HeightRequest="200" CornerRadius="40" StrokeThickness="10" Stroke="Green" />

<Image
Source="dotnet_bot.png"
SemanticProperties.Description="Cute dot net bot waving hi to you!"
WidthRequest="125"
HeightRequest="155"
HorizontalOptions="Center" />

</VerticalStackLayout>
</ScrollView>

</ContentPage>
174 changes: 174 additions & 0 deletions src/Controls/samples/Controls.Sample/Pages/HitTestingPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.Maui;
using Microsoft.Maui.Graphics;

namespace Maui.Controls.Sample.Pages
{
public partial class HitTestingPage : IWindowOverlayElement
{
enum State
{
SingleSelection,
RectangleSelectionPickFirst,
RectangleSelectionPickSecond
}

Microsoft.Maui.Controls.Window _window;
WindowOverlay _overlay;
double _firstPosX;
double _firstPosY;
double _currentMousePosX;
double _currentMousePosY;
State _state = State.SingleSelection;
bool _tappedWithoutMove;

Microsoft.Maui.Controls.View[] _allChildren = null;

public HitTestingPage()
{
InitializeComponent();
}

private void RectangleSelectionCheckBox_CheckedChanged(object sender, Microsoft.Maui.Controls.CheckedChangedEventArgs e)
{
_state = RectangleSelectionCheckBox.IsChecked ? State.RectangleSelectionPickFirst : State.SingleSelection;
}

private void ContentPage_Loaded(object sender, EventArgs e)
{
_window = this.GetParentWindow();
_overlay = new WindowOverlay(_window);
_overlay.AddWindowElement(this);
_window.AddOverlay(_overlay);

_allChildren = this.GetVisualTreeDescendants()
.OfType<Microsoft.Maui.Controls.View>()
.Where(x => x != RectangleSelectionCheckBox)
.ToArray();
#if WINDOWS
var platformChildren = _allChildren.Select(v => v.Handler.PlatformView).OfType<Microsoft.UI.Xaml.UIElement>();
foreach (var element in platformChildren)
{
Debug.Print(element.GetType().FullName);
element.Tapped += DoHandleTapped;
element.PointerMoved += DoHandlePointerMoved;
}

void DoHandleTapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs args)
{
var pos = args.GetPosition(null);
Debug.WriteLine($"Tapped {sender.GetType().FullName} @ ({pos.X};{pos.Y})");
HandleTapped(pos.X, pos.Y);
}

void DoHandlePointerMoved(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs args)
{
var pos = args.GetCurrentPoint(null);
HandlePointerMoved(pos.Position.X, pos.Position.Y);
}
#else
RectangleSelectionCheckBox.IsEnabled = false;
_overlay.Tapped += DoHandleOverlayTapped;

void DoHandleOverlayTapped(object sender, WindowOverlayTappedEventArgs e)
{
var p = e.Point;
Debug.Print($"{sender.GetType().Name} tapped! ({p.X};{p.Y})");
_tappedWithoutMove = false; // No mouse move on iOS/Android
HandleTapped(p.X, p.Y);
}
#endif
}



private void ContentPage_Unloaded(object sender, EventArgs e)
{
_overlay.RemoveWindowElement(this);
_window.RemoveOverlay(_overlay);
}

private void HandlePointerMoved(double x, double y)
{
_currentMousePosX = x;
_currentMousePosY = y;
_tappedWithoutMove = false;

if (_state == State.RectangleSelectionPickSecond)
{
_overlay.Invalidate();
}
}

private void HandleTapped(double x, double y)
{
if (_tappedWithoutMove) // "Tapped" may fire multiple times for containers & overlapping controls
return;

_tappedWithoutMove = true;
IEnumerable<IVisualTreeElement> elements = null;

if (_state == State.SingleSelection)
{
elements = VisualTreeElementExtensions.GetVisualTreeElements(this.Window, x, y);
}
else if (_state == State.RectangleSelectionPickFirst)
{
_firstPosX = x;
_firstPosY = y;
_state = State.RectangleSelectionPickSecond;
return;
}
else if (_state == State.RectangleSelectionPickSecond)
{
var rect = GetCurrentRect();
elements = VisualTreeElementExtensions.GetVisualTreeElements(this.Window, rect.Left, rect.Top, rect.Right, rect.Bottom);
_state = State.RectangleSelectionPickFirst;
_overlay.Invalidate();
}

foreach (var c in _allChildren)
{
c.BackgroundColor = null;
}

SelectionLabel.Text = "Selected: " + string.Join(" <- ", elements.Select(x => x.GetType().Name));
var e = elements.FirstOrDefault() as Microsoft.Maui.Controls.View;

if (e != null)
{
e.BackgroundColor = new Microsoft.Maui.Graphics.Color(255, 0, 0);
}
}

// IWindowOverlayElement/IDrawable is implemented to show the rectangle selection lasso

bool IWindowOverlayElement.Contains(Point point)
{
return this.Frame.Contains(point);
}

void IDrawable.Draw(ICanvas canvas, RectF dirtyRect)
{
if (_state == State.RectangleSelectionPickSecond)
{
Rect rect = GetCurrentRect();
canvas.StrokeColor = Colors.Pink;
canvas.StrokeSize = 2;
canvas.DrawRectangle(rect);
}
}

Rect GetCurrentRect()
{
double minX = Math.Min(_firstPosX, _currentMousePosX);
double minY = Math.Min(_firstPosY, _currentMousePosY);
double maxX = Math.Max(_firstPosX, _currentMousePosX);
double maxY = Math.Max(_firstPosY, _currentMousePosY);
return Rect.FromLTRB(minX, minY, maxX, maxY);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ protected override IEnumerable<SectionModel> CreateItems() => new[]

new SectionModel(typeof(OthersPage), "Others Concepts",
"Other options like Graphics."),

new SectionModel(typeof(HitTestingPage), "Hit Testing",
"Demonstrates VisualTreeElementExtensions hit testing methods"),
};
}
}
Loading

0 comments on commit 9ac65b0

Please sign in to comment.