### Description of Change

Update the net9.0 branch with all the latest things.
This commit is contained in:
Matthew Leibowitz 2024-08-22 06:12:32 +08:00 коммит произвёл GitHub
Родитель 79bdab0011 f129f6edda
Коммит 11aa1bdaf0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
39 изменённых файлов: 755 добавлений и 157 удалений

Просмотреть файл

@ -73,6 +73,7 @@
"src\\TestUtils\\src\\DeviceTests\\TestUtils.DeviceTests.csproj",
"src\\TestUtils\\src\\Microsoft.Maui.IntegrationTests\\Microsoft.Maui.IntegrationTests.csproj",
"src\\TestUtils\\src\\TestUtils\\TestUtils.csproj",
"src\\TestUtils\\src\\UITest.Analyzers\\UITest.Analyzers.csproj",
"src\\TestUtils\\src\\UITest.Appium\\UITest.Appium.csproj",
"src\\TestUtils\\src\\UITest.Core\\UITest.Core.csproj",
"src\\TestUtils\\src\\UITest.NUnit\\UITest.NUnit.csproj",

Просмотреть файл

@ -13,6 +13,11 @@ string officialBuildId = Argument("officialbuildid", "");
string testFilter = Argument("test-filter", EnvironmentVariable("TEST_FILTER"));
var rootFolder = Context.Environment.WorkingDirectory;
if (rootFolder.FullPath.EndsWith("/devices", StringComparison.OrdinalIgnoreCase))
rootFolder = rootFolder.Combine("../../").Collapse();
var arcadeBin = MakeAbsolute(new DirectoryPath("./artifacts/bin/"));
string TestTFM = Argument("testtfm", "");
@ -55,12 +60,12 @@ Task("dotnet")
if(!string.IsNullOrEmpty(nugetSource))
{
EnsureDirectoryExists(nugetSource);
var originalNuget = File("./NuGet.config");
var originalNuget = File($"{rootFolder}/NuGet.config");
ReplaceTextInFiles(originalNuget, "<add key=\"nuget-only\" value=\"true\" />", "");
ReplaceTextInFiles(originalNuget, "NUGET_ONLY_PLACEHOLDER", nugetSource);
}
DotNetBuild("./src/DotNet/DotNet.csproj", new DotNetBuildSettings
DotNetBuild($"{rootFolder}/src/DotNet/DotNet.csproj", new DotNetBuildSettings
{
MSBuildSettings = new DotNetMSBuildSettings()
.EnableBinaryLogger($"{GetLogDirectory()}/dotnet-{configuration}-{DateTime.UtcNow.ToFileTimeUtc()}.binlog")
@ -109,7 +114,7 @@ Task("dotnet-buildtasks")
.IsDependentOn("dotnet")
.Does(() =>
{
RunMSBuildWithDotNet("./Microsoft.Maui.BuildTasks.slnf");
RunMSBuildWithDotNet($"{rootFolder}/Microsoft.Maui.BuildTasks.slnf");
})
.OnError(exception =>
{
@ -166,6 +171,7 @@ Task("dotnet-build")
});
Task("dotnet-samples")
.IsDependentOn("dotnet-buildtasks")
.Does(() =>
{
var tempDir = PrepareSeparateBuildContext("samplesTest");

Просмотреть файл

@ -1,11 +1,11 @@
#addin nuget:?package=Cake.Android.Adb&version=3.2.0
#addin nuget:?package=Cake.Android.AvdManager&version=2.2.0
#load "../cake/helpers.cake"
#load "../cake/dotnet.cake"
#load "./devices-shared.cake"
#load "./uitests-shared.cake"
const int DefaultApiLevel = 30;
Information("Local Dotnet: {0}", localDotnet);
string DEFAULT_ANDROID_PROJECT = "../../src/Controls/tests/TestCases.Android.Tests/Controls.TestCases.Android.Tests.csproj";
var projectPath = Argument("project", EnvironmentVariable("ANDROID_TEST_PROJECT") ?? DEFAULT_ANDROID_PROJECT);
var testDevice = Argument("device", EnvironmentVariable("ANDROID_TEST_DEVICE") ?? $"android-emulator-64_{DefaultApiLevel}");
@ -23,9 +23,6 @@ var deviceSkin = Argument("skin", EnvironmentVariable("ANDROID_TEST_SKIN") ?? "N
var androidAvd = "DEVICE_TESTS_EMULATOR";
var androidAvdImage = "";
var deviceArch = "";
bool deviceBoot = Argument("boot", true);
bool deviceBootWait = Argument("wait", true);
var androidVersion = Argument("apiversion", EnvironmentVariable("ANDROID_PLATFORM_VERSION") ?? DefaultApiLevel.ToString());
// Directory setup
@ -59,6 +56,7 @@ var dotnetToolPath = GetDotnetToolPath();
Setup(context =>
{
LogSetupInfo(dotnetToolPath);
PerformCleanupIfNeeded(deviceCleanupEnabled);
DetermineDeviceCharacteristics(testDevice, DefaultApiLevel);
@ -88,11 +86,12 @@ Task("test")
});
Task("uitest-build")
.IsDependentOn("dotnet-buildtasks")
.Does(() =>
{
ExecuteBuildUITestApp(testAppProjectPath, testDevice, binlogDirectory, configuration, targetFramework, "", dotnetToolPath);
});
Task("uitest")
.Does(() =>
{
@ -100,6 +99,7 @@ Task("uitest")
});
Task("cg-uitest")
.IsDependentOn("dotnet-buildtasks")
.Does(() =>
{
ExecuteCGLegacyUITests(projectPath, testAppProjectPath, testAppPackageName, testDevice, testResultsPath, configuration, targetFramework, dotnetToolPath, testAppInstrumentation);
@ -461,7 +461,7 @@ void HandleVirtualDevice(AndroidEmulatorToolSettings emuSettings, AndroidAvdMana
void CleanUpVirtualDevice(AndroidEmulatorProcess emulatorProcess, AndroidAvdManagerToolSettings avdSettings)
{
// no virtual device was used
if (emulatorProcess == null || !deviceBoot || TARGET.ToLower() == "boot")
if (emulatorProcess == null || !deviceBoot || targetBoot)
return;
//stop and cleanup the emulator

Просмотреть файл

@ -1,6 +1,4 @@
#load "../cake/helpers.cake"
#load "../cake/dotnet.cake"
#load "./devices-shared.cake"
#load "./uitests-shared.cake"
// Argument handling
string DEFAULT_MAC_PROJECT = "../../src/Controls/tests/TestCases.Mac.Tests/Controls.TestCases.Mac.Tests.csproj";

Просмотреть файл

@ -207,7 +207,18 @@ void LogSetupInfo(string toolPath)
string GetDotnetToolPath()
{
var isLocalDotnet = GetBuildVariable("workloads", "local") == "local";
var toolPath = isLocalDotnet ? $"{MakeAbsolute(Directory("../../bin/dotnet/")).ToString()}/dotnet" : DotnetToolPathDefault;
string toolPath;
if(IsRunningOnWindows())
{
toolPath = isLocalDotnet ? $"{MakeAbsolute(Directory("../../bin/dotnet/")).ToString()}/dotnet" : null;
}
else
{
toolPath = isLocalDotnet ? $"{MakeAbsolute(Directory("../../bin/dotnet/")).ToString()}/dotnet" : DotnetToolPathDefault;
}
Information(isLocalDotnet ? "Using local dotnet" : "Using system dotnet");
return toolPath;
}

Просмотреть файл

@ -1,14 +1,13 @@
#addin nuget:?package=Cake.AppleSimulator&version=0.2.0
#load "../cake/helpers.cake"
#load "../cake/dotnet.cake"
#load "./devices-shared.cake"
#load "./uitests-shared.cake"
const string DefaultVersion = "17.2";
const string DefaultTestDevice = $"ios-simulator-64_{DefaultVersion}";
// Required arguments
string DEFAULT_IOS_PROJECT = "../../src/Controls/tests/TestCases.iOS.Tests/Controls.TestCases.iOS.Tests.csproj";
var projectPath = Argument("project", EnvironmentVariable("IOS_TEST_PROJECT") ?? DEFAULT_IOS_PROJECT);
var testDevice = Argument("device", EnvironmentVariable("IOS_TEST_DEVICE") ?? $"ios-simulator-64_{DefaultVersion}");
var testDevice = Argument("device", EnvironmentVariable("IOS_TEST_DEVICE") ?? DefaultTestDevice);
var targetFramework = Argument("tfm", EnvironmentVariable("TARGET_FRAMEWORK") ?? $"{DotnetVersion}-ios");
var binlogArg = Argument("binlog", EnvironmentVariable("IOS_TEST_BINLOG") ?? "");
var testApp = Argument("app", EnvironmentVariable("IOS_TEST_APP") ?? "");
@ -45,6 +44,12 @@ var dotnetToolPath = GetDotnetToolPath();
Setup(context =>
{
LogSetupInfo(dotnetToolPath);
if (!deviceBoot)
{
return;
}
PerformCleanupIfNeeded(deviceCleanupEnabled, false);
// Device or simulator setup
@ -59,10 +64,21 @@ Setup(context =>
}
});
Teardown(context => PerformCleanupIfNeeded(deviceCleanupEnabled, true));
Teardown(context =>
{
if (!deviceBoot || targetBoot)
{
return;
}
PerformCleanupIfNeeded(deviceCleanupEnabled, true);
});
Task("Cleanup");
// Todo this doesn't work for iOS currently
// Task("boot");
Task("Build")
.WithCriteria(!string.IsNullOrEmpty(projectPath))
.Does(() =>
@ -78,10 +94,10 @@ Task("Test")
});
Task("uitest-build")
.IsDependentOn("dotnet-buildtasks")
.Does(() =>
{
ExecuteBuildUITestApp(testAppProjectPath, testDevice, binlogDirectory, configuration, targetFramework, runtimeIdentifier, dotnetToolPath);
});
Task("uitest")
@ -92,6 +108,7 @@ Task("uitest")
});
Task("cg-uitest")
.IsDependentOn("dotnet-buildtasks")
.Does(() =>
{
ExecuteCGLegacyUITests(projectPath, testAppProjectPath, testDevice, testResultsPath, configuration, targetFramework, runtimeIdentifier, iosVersion, dotnetToolPath);
@ -385,7 +402,7 @@ string GetDefaultRuntimeIdentifier(string testDeviceIdentifier)
void InstallIpa(string testApp, string testAppPackageName, string testDevice, string testResultsDirectory, string version, string toolPath)
{
Information("Install with xharness: {0}", testApp);
Information("Install with xharness: {0} testDevice:{1}", testApp, testDevice);
var settings = new DotNetToolSettings
{
ToolPath = toolPath,
@ -397,6 +414,7 @@ void InstallIpa(string testApp, string testAppPackageName, string testDevice, st
$"--targets=\"{testDevice}\" " +
$"--output-directory=\"{testResultsDirectory}\" " +
$"--verbosity=\"Debug\" ");
if (testDevice.Contains("device"))
{
if (string.IsNullOrEmpty(DEVICE_UDID))
@ -435,12 +453,14 @@ void InstallIpa(string testApp, string testAppPackageName, string testDevice, st
}
else
{
var simulatorName = "XHarness";
Information("Looking for simulator: {0} iosversion {1}", simulatorName, iosVersionToRun);
var UDID = GetUDID(testDevice, dotnetToolPath);
var sims = ListAppleSimulators();
var simXH = sims.Where(s => s.Name.Contains(simulatorName) && s.Name.Contains(iosVersionToRun)).FirstOrDefault();
var simXH = sims.Where(s => s.UDID == UDID).FirstOrDefault();
if (simXH == null)
{
throw new Exception("No simulator was found to run tests on.");
}
deviceToRun = simXH.UDID;
DEVICE_NAME = simXH.Name;
@ -454,6 +474,40 @@ void InstallIpa(string testApp, string testAppPackageName, string testDevice, st
}
}
string GetUDID(string testDevice, string tool)
{
Information("Looking for simulator: {0}", testDevice);
string result = string.Empty;
DotNetTool("tool", new DotNetToolSettings
{
ToolPath = tool,
ArgumentCustomization = args => args.Append($"run xharness apple device {testDevice}"),
SetupProcessSettings = processSettings =>
{
processSettings.RedirectStandardOutput = true;
processSettings.RedirectedStandardOutputHandler = line =>
{
// The output from this command returns the UDID of the simulator
// and NULL so we're filtering out the NULL
if (!string.IsNullOrWhiteSpace(line))
{
result = line;
}
return line;
};
}
});
if(!string.IsNullOrWhiteSpace(result))
Information("Yay we found your device: {0}", result);
else
Information("No device found installed: {0}", testDevice);
return result;
}
void GetSimulators(string version, string tool)
{
Information("Getting simulators for version {0}", version);

Просмотреть файл

@ -0,0 +1,13 @@
#load "../cake/helpers.cake"
if (!IsCIBuild() && GetBuildVariable("workloads", "notset") == "notset")
{
SetEnvironmentVariable("workloads", "global");
}
#load "../cake/dotnet.cake"
#load "./devices-shared.cake"
bool deviceBoot = Argument("boot", TARGET.ToLower() != "uitest-build");
bool targetBoot = TARGET.ToLower() == "boot";
bool deviceBootWait = Argument("wait", true);

Просмотреть файл

@ -1,6 +1,4 @@
#load "../cake/helpers.cake"
#load "../cake/dotnet.cake"
#load "./devices-shared.cake"
#load "./uitests-shared.cake"
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

Просмотреть файл

@ -49,9 +49,6 @@ steps:
- pwsh: echo "##vso[task.prependpath]$(DotNet.Dir)"
displayName: 'Add .NET to PATH'
- pwsh: ./build.ps1 --target=dotnet-buildtasks --configuration="${{ parameters.configuration }}"
displayName: 'Build the MSBuild Tasks'
- pwsh: ./build.ps1 --target=dotnet-samples --configuration="${{ parameters.configuration }}" --${{ parameters.platform }} --verbosity=diagnostic --usenuget=false --runtimevariant="${{ parameters.runtimeVariant }}"
displayName: 'Build the samples'

Просмотреть файл

@ -43,9 +43,6 @@ steps:
- pwsh: echo "##vso[task.prependpath]$(DotNet.Dir)"
displayName: 'Add .NET to PATH'
- pwsh: ./build.ps1 --target=dotnet-buildtasks --configuration="${{ parameters.configuration }}"
displayName: 'Build the MSBuild Tasks'
- pwsh: ./build.ps1 --target=${{ parameters.targetSample }} --configuration="${{ parameters.configuration }}" --${{ parameters.platform }} --verbosity=diagnostic
displayName: 'Build the Legacy ControlGallery'

Просмотреть файл

@ -136,7 +136,7 @@ stages:
androidApiLevels: [ 30 ]
iosVersions: [ '17.2' ]
provisionatorChannel: ${{ parameters.provisionatorChannel }}
${{ if or(parameters.CompatibilityTests, ne(variables['Build.Reason'], 'PullRequest')) }}:
${{ if parameters.CompatibilityTests }}:
runCompatibilityTests: true
projects:
- name: controls

Просмотреть файл

@ -784,7 +784,8 @@ namespace Microsoft.Maui.Controls
internal override void OnSetDynamicResource(BindableProperty property, string key, SetterSpecificity specificity)
{
base.OnSetDynamicResource(property, key, specificity);
DynamicResources[property] = (key, specificity);
if (!DynamicResources.TryGetValue(property, out var existing) || existing.Item2 < specificity)
DynamicResources[property] = (key, specificity);
if (this.TryGetResource(key, out var value))
OnResourceChanged(property, value, specificity);
}

Просмотреть файл

@ -62,7 +62,7 @@ namespace Microsoft.Maui.Controls.Platform
bool HasAnyGestures()
{
return _panGestureHandler.HasAnyGestures() || _tapGestureHandler.HasAnyGestures() || _swipeGestureHandler.HasAnyGestures();
return (_panGestureHandler?.HasAnyGestures() ?? false) || (_tapGestureHandler?.HasAnyGestures() ?? false) || (_swipeGestureHandler?.HasAnyGestures() ?? false);
}
// This is needed because GestureRecognizer callbacks can be delayed several hundred milliseconds

Просмотреть файл

@ -187,6 +187,13 @@ namespace Microsoft.Maui.Controls.Platform
internal static void JumpToIndexAsync(ListViewBase list, int index, ScrollToPosition scrollToPosition)
{
var scrollViewer = list.GetFirstDescendant<ScrollViewer>();
if (scrollViewer is null)
{
// If ScrollViewer is not found, do nothing.
return;
}
var con = list.ContainerFromIndex(index);
if (con is UIElement uIElement)
{
@ -201,8 +208,25 @@ namespace Microsoft.Maui.Controls.Platform
public static async Task JumpToItemAsync(ListViewBase list, object targetItem, ScrollToPosition scrollToPosition)
{
if(!list.IsLoaded)
{
list.OnLoaded(async () =>
{
// If the ListView is not loaded, wait for it to load and reinvoke the JumpToItem.
await JumpToItemAsync(list, targetItem, scrollToPosition);
});
return;
}
var scrollViewer = list.GetFirstDescendant<ScrollViewer>();
if (scrollViewer is null)
{
// If ScrollViewer is not found, do nothing.
return;
}
var tcs = new TaskCompletionSource<object>();
Func<Task> adjust = null;
@ -271,6 +295,17 @@ namespace Microsoft.Maui.Controls.Platform
public static async Task AnimateToItemAsync(ListViewBase list, object targetItem, ScrollToPosition scrollToPosition)
{
if (!list.IsLoaded)
{
list.OnLoaded(async () =>
{
// If the ListView is not loaded, wait for it to load and reinvoke the AnimateToItem.
await AnimateToItemAsync(list, targetItem, scrollToPosition);
});
return;
}
var scrollViewer = list.GetFirstDescendant<ScrollViewer>();
if (scrollViewer == null)

Просмотреть файл

@ -224,23 +224,30 @@ Skip = "Fails: https://github.com/dotnet/maui/issues/17664"
})
};
var desiredItemIndex = 134; // 13th group (letter: N), 4th item
var firstVisibleItemIndex = -1;
var lastVisibleItemIndex = -1;
collectionView.Scrolled += (s, e) =>
{
firstVisibleItemIndex = e.FirstVisibleItemIndex;
lastVisibleItemIndex = e.LastVisibleItemIndex;
};
await CreateHandlerAndAddToWindow<CollectionViewHandler>(collectionView, async handler =>
{
collectionView.ScrollTo(index: 4, groupIndex: 13, animate: false); // Item "N_4"
await Task.Delay(500);
int retryCount = 3;
bool foundItem = false;
Assert.True(desiredItemIndex >= firstVisibleItemIndex &&
desiredItemIndex <= lastVisibleItemIndex);
while (retryCount > 0 && !foundItem)
{
retryCount--;
await Task.Delay(500);
for (int i = 0; i < collectionView.LogicalChildrenInternal.Count; i++)
{
var item = collectionView.LogicalChildrenInternal[i];
if (item is Label label && label.Text.Equals("n_4", StringComparison.OrdinalIgnoreCase))
{
foundItem = true;
break;
}
}
}
Assert.True(foundItem);
});
}

Просмотреть файл

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Maui.Controls.Sample.Issues"
x:Class="Maui.Controls.Sample.Issues.Issue14825"
x:DataType="local:Issue14825">
<VerticalStackLayout>
<Label Text="WebView"/>
<WebView x:Name="myWebView" WidthRequest="400" HeightRequest="150">
<WebView.Source>
<HtmlWebViewSource>
<HtmlWebViewSource.Html>
<![CDATA[
<html>
<body>
<H1>.NET MAUI</H1>
<p>Welcome to WebView. 👍</p>
</body>
</html>
]]>
</HtmlWebViewSource.Html>
</HtmlWebViewSource>
</WebView.Source>
</WebView>
<VerticalStackLayout x:Name="screenshotResult" AutomationId="screenshotResult"/>
<Button AutomationId="Capture" Text="Capture" Clicked="CaptureButton_Clicked"/>
<Label AutomationId="TestInstructions" Margin="0,30,0,0" FontAttributes="Bold"
Text="Instructions: Click the capture button and expect a WebView screenshot to appear."/>
</VerticalStackLayout>
</ContentPage>

Просмотреть файл

@ -0,0 +1,26 @@
#nullable enable
namespace Maui.Controls.Sample.Issues;
[XamlCompilation(XamlCompilationOptions.Compile)]
[Issue(IssueTracker.Github, 14825, "Capture WebView screenshot", PlatformAffected.UWP)]
public partial class Issue14825 : ContentPage
{
public Issue14825()
{
InitializeComponent();
}
private async void CaptureButton_Clicked(object sender, EventArgs e)
{
IScreenshotResult? result = await myWebView.CaptureAsync();
if (result != null)
{
// Intentionally no "using" because ImageSource requires a valid stream.
Stream stream = await result.OpenReadAsync(ScreenshotFormat.Png, 100);
screenshotResult.Add(new Label() { Text = $"Your screenshot ({myWebView.Width}x{myWebView.Height}):" });
screenshotResult.Add(new Image() { Source = ImageSource.FromStream(() => stream), WidthRequest = myWebView.Width, HeightRequest = myWebView.Height });
}
}
}

Просмотреть файл

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue17865"
xmlns:ns="clr-namespace:Maui.Controls.Sample.Issues"
Title="Issue 17865">
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height="Auto"/>
<RowDefinition
Height="*"/>
</Grid.RowDefinitions>
<HorizontalStackLayout
Padding="5">
<Button
AutomationId="WaitForStubControl"
Text="Reveal Last Item"
Clicked="OnButtonClicked"/>
</HorizontalStackLayout>
<CollectionView
x:Name="collectionView"
Grid.Row="1"
ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Label
Margin="5,2"
Text="{Binding ItemText}"
HorizontalOptions="Fill"
TextColor="Gray"/>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>

Просмотреть файл

@ -0,0 +1,71 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;
namespace Maui.Controls.Sample.Issues
{
internal class Issue17865Model
{
public Issue17865Model(int itemIndex)
{
ItemText = $"Item #{itemIndex}";
}
public string ItemText { get; }
}
internal class Issue17865ViewModel : BindableObject
{
ObservableCollection<Issue17865Model> items = new();
public Issue17865ViewModel()
{
Populate();
}
void Populate()
{
for (int i = 0; i < 100; i++)
{
items.Add(new Issue17865Model(i));
}
}
public ObservableCollection<Issue17865Model> Items => items;
}
[XamlCompilation(XamlCompilationOptions.Compile)]
[Issue(IssueTracker.Github, 17865, "CollectionView throws NRE when ScrollTo method is called from a handler of event Window.Created", PlatformAffected.UWP)]
public partial class Issue17865 : ContentPage
{
readonly Issue17865ViewModel _viewModel;
public Issue17865()
{
InitializeComponent();
collectionView.Loaded += CollectionView_Loaded;
BindingContext = _viewModel = new Issue17865ViewModel();
}
private void CollectionView_Loaded(object sender, EventArgs e)
{
RevealLastItem();
}
public void RevealLastItem()
{
var item = _viewModel.Items.Last();
collectionView.ScrollTo(item, null, ScrollToPosition.MakeVisible, false);
}
private void OnButtonClicked(object sender, EventArgs e)
{
RevealLastItem();
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue21437"
xmlns:issues="clr-namespace:Maui.Controls.Sample.Issues"
Title="Issue21437">
<VerticalStackLayout BindableLayout.ItemsSource="{Binding Items}" HorizontalOptions="Start">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Label Text="{Binding .}" AutomationId="{Binding .}" HorizontalOptions="Start">
<Label.GestureRecognizers>
<TapGestureRecognizer
NumberOfTapsRequired="2"
Command="{Binding TapCommand, Source={RelativeSource AncestorType={x:Type issues:Issue21437}}}"
CommandParameter="{Binding .}" />
</Label.GestureRecognizers>
</Label>
</DataTemplate>
</BindableLayout.ItemTemplate>
</VerticalStackLayout>
</ContentPage>

Просмотреть файл

@ -0,0 +1,21 @@
using System.Collections.ObjectModel;
namespace Maui.Controls.Sample.Issues;
[Issue(IssueTracker.Github, 21437, "Removing TapGestureRecognizer with at least 2 taps causes Exception", PlatformAffected.Android)]
public partial class Issue21437 : ContentPage
{
public ObservableCollection<string> Items { get; } = new() { "Item1", "Item2", "Item3" };
public Command TapCommand => new Command<string>(obj =>
{
Items.Remove(obj);
});
public Issue21437()
{
InitializeComponent();
BindingContext = this;
}
}

Просмотреть файл

@ -93,4 +93,4 @@ namespace Maui.Controls.Sample
return window;
}
}
}
}

Просмотреть файл

@ -0,0 +1,28 @@
#if WINDOWS
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;
namespace Microsoft.Maui.TestCases.Tests.Issues;
public class Issue14825 : _IssuesUITest
{
public override string Issue => "Capture WebView screenshot";
public Issue14825(TestDevice device) : base(device)
{
}
[Test]
[Category(UITestCategories.WebView)]
public void ValidateWebViewScreenshot()
{
App.WaitForElement("TestInstructions");
// Click the capture button to capture a WebView screenshot.
App.Click("Capture");
VerifyScreenshot();
}
}
#endif

Просмотреть файл

@ -0,0 +1,28 @@
#if WINDOWS
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;
namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class Issue17865 : _IssuesUITest
{
const string ButtonId = "WaitForStubControl";
public Issue17865(TestDevice device) : base(device) { }
public override string Issue => "CollectionView throws NRE when ScrollTo method is called from a handler of event Window.Created";
[Test]
[Category(UITestCategories.CollectionView)]
public void Issue17865Test()
{
App.WaitForElement(ButtonId);
App.Click(ButtonId);
// NOTE: Without crashes the test has passed.
}
}
}
#endif

Просмотреть файл

@ -0,0 +1,26 @@
#if ANDROID
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;
namespace Microsoft.Maui.TestCases.Tests.Issues;
public class Issue21437 : _IssuesUITest
{
public override string Issue => "Removing TapGestureRecognizer with at least 2 taps causes Exception";
public Issue21437(TestDevice device)
: base(device)
{ }
[Test]
[Category(UITestCategories.Gestures)]
public void ExceptionShouldNotBeThrown()
{
_ = App.WaitForElement("Item2");
App.DoubleClick("Item2");
//The test passes if no exception is thrown
}
}
#endif

Просмотреть файл

@ -1,5 +1,6 @@
using System.Reflection;
using NUnit.Framework;
using OpenQA.Selenium.Appium.iOS;
using UITest.Appium;
using UITest.Appium.NUnit;
using UITest.Core;
@ -107,6 +108,7 @@ namespace Microsoft.Maui.TestCases.Tests
void Verify(string? name)
{
string deviceName = GetTestConfig().GetProperty<string>("DeviceName") ?? string.Empty;
// Remove the XHarness suffix if present
deviceName = deviceName.Replace(" - created by XHarness", "", StringComparison.Ordinal);
@ -136,16 +138,21 @@ namespace Microsoft.Maui.TestCases.Tests
break;
case TestDevice.iOS:
if (deviceName == "iPhone Xs (iOS 17.2)")
var platformVersion = (string)((AppiumApp)App).Driver.Capabilities.GetCapability("platformVersion");
var device = (string)((AppiumApp)App).Driver.Capabilities.GetCapability("deviceName");
if (deviceName == "iPhone Xs (iOS 17.2)" || (device.Contains(" Xs", StringComparison.OrdinalIgnoreCase) && platformVersion == "17.2"))
{
environmentName = "ios";
}
else if (deviceName == "iPhone X (iOS 16.4)")
else if (deviceName == "iPhone X (iOS 16.4)" || (device.Contains(" X", StringComparison.OrdinalIgnoreCase) && platformVersion == "16.4"))
{
environmentName = "ios-iphonex";
}
else
{
Assert.Fail($"iOS visual tests should be run on iPhone Xs (iOS 17.2) or iPhone X (iOS 16.4) simulator images, but the current device is '{deviceName}'. Follow the steps on the MAUI UI testing wiki.");
}
break;

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 17 KiB

Просмотреть файл

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Unreported010">
<ContentPage.Resources>
<Color x:Key="Foo">Blue</Color>
<Style TargetType="Button">
<Setter Property="BackgroundColor" Value="{DynamicResource Foo}"/>
</Style>
</ContentPage.Resources>
<VerticalStackLayout>
<Button x:Name="button0" Text="Hello World" BackgroundColor="{DynamicResource Foo}"/>
</VerticalStackLayout>
</ContentPage>

Просмотреть файл

@ -0,0 +1,47 @@
using System;
using System.Threading.Tasks;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls.Core.UnitTests;
using Microsoft.Maui.Dispatching;
using Microsoft.Maui.UnitTests;
using NUnit.Framework;
namespace Microsoft.Maui.Controls.Xaml.UnitTests;
public partial class Unreported010
{
public Unreported010()
{
InitializeComponent();
}
public Unreported010(bool useCompiledXaml)
{
//this stub will be replaced at compile time
}
[TestFixture]
class Test
{
[SetUp]
public void Setup()
{
Application.SetCurrentApplication(new MockApplication());
DispatcherProvider.SetCurrent(new DispatcherProviderStub());
}
[TearDown] public void TearDown() => AppInfo.SetCurrent(null);
[Test]
public void LocalDynamicResources([Values(false, true)] bool useCompiledXaml)
{
var page = new Unreported010(useCompiledXaml);
Assert.That(page.button0.BackgroundColor, Is.EqualTo(Colors.Blue));
page.Resources["Foo"] = Colors.Red;
Assert.That(page.button0.BackgroundColor, Is.EqualTo(Colors.Red));
}
}
}

Просмотреть файл

@ -0,0 +1,18 @@
package com.microsoft.maui.glide;
import android.util.Log;
public class GlideLogging {
private static final String TAG = "Glide";
private static final boolean IS_VERBOSE_LOGGABLE = Log.isLoggable(TAG, Log.VERBOSE);
public static boolean isVerboseLoggable() {
return IS_VERBOSE_LOGGABLE;
}
public static void v(String message) {
if (IS_VERBOSE_LOGGABLE) {
Log.v(TAG, message);
}
}
}

Просмотреть файл

@ -2,13 +2,16 @@ package com.microsoft.maui.glide;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
import com.microsoft.maui.ImageLoaderCallback;
import com.microsoft.maui.glide.GlideLogging;
import com.microsoft.maui.glide.fallback.ImageLoaderCallbackModelLoaderFactory;
import com.microsoft.maui.glide.font.FontModel;
import com.microsoft.maui.glide.font.FontModelLoaderFactory;
@ -33,4 +36,13 @@ public class MauiGlideModule extends AppGlideModule {
public boolean isManifestParsingEnabled() {
return false;
}
}
@Override
public void applyOptions(Context context, GlideBuilder builder) {
// Glide is checking for the log level only on some classes, so we have to do it ourselves here.
// Command: adb shell setprop log.tag.Glide VERBOSE
if (GlideLogging.isVerboseLoggable()) {
builder.setLogLevel(Log.VERBOSE);
}
}
}

Просмотреть файл

@ -44,7 +44,7 @@
<ProjectReference Include="..\..\Graphics\src\Graphics.Win2D\Graphics.Win2D.csproj" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.Contains('-android'))">
<PackageReference Include="Xamarin.Android.Glide" Version="4.16.0.5" />
<PackageReference Include="Xamarin.Android.Glide" Version="$(_XamarinAndroidGlideVersion)" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.LiveData" Version="2.8.3.1" />
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.11.0.1" />
<PackageReference Include="Xamarin.AndroidX.SwipeRefreshLayout" Version="1.1.0.22" />

Просмотреть файл

@ -15,6 +15,14 @@ namespace Microsoft.Maui.Graphics
{
public class MauiDrawable : PaintDrawable
{
static Join? JoinMiter;
static Join? JoinBevel;
static Join? JoinRound;
static Cap? CapButt;
static Cap? CapSquare;
static Cap? CapRound;
readonly AContext? _context;
readonly float _density;
@ -298,20 +306,13 @@ namespace Microsoft.Maui.Graphics
public void SetBorderLineJoin(LineJoin lineJoin)
{
Join? aLineJoin = Join.Miter;
switch (lineJoin)
Join? aLineJoin = lineJoin switch
{
case LineJoin.Miter:
aLineJoin = Join.Miter;
break;
case LineJoin.Bevel:
aLineJoin = Join.Bevel;
break;
case LineJoin.Round:
aLineJoin = Join.Round;
break;
}
LineJoin.Miter => JoinMiter ??= Join.Miter,
LineJoin.Bevel => JoinBevel ??= Join.Bevel,
LineJoin.Round => JoinRound ??= Join.Round,
_ => JoinMiter ??= Join.Miter,
};
if (_strokeLineJoin == aLineJoin)
return;
@ -323,20 +324,13 @@ namespace Microsoft.Maui.Graphics
public void SetBorderLineCap(LineCap lineCap)
{
Cap? aLineCap = Cap.Butt;
switch (lineCap)
Cap? aLineCap = lineCap switch
{
case LineCap.Butt:
aLineCap = Cap.Butt;
break;
case LineCap.Square:
aLineCap = Cap.Square;
break;
case LineCap.Round:
aLineCap = Cap.Round;
break;
}
LineCap.Butt => CapButt ??= Cap.Butt,
LineCap.Square => CapSquare ??= Cap.Square,
LineCap.Round => CapRound ??= Cap.Round,
_ => CapButt ??= Cap.Butt,
};
if (_strokeLineCap == aLineCap)
return;

Просмотреть файл

@ -8,22 +8,21 @@ namespace Microsoft.Maui.Platform
public static void UpdateBorderStroke(this AView platformView, IBorderStroke border)
{
// Always set the drawable first
platformView.UpdateMauiDrawable(border);
MauiDrawable? mauiDrawable = null;
platformView.UpdateMauiDrawable(border, ref mauiDrawable);
var borderShape = border.Shape;
MauiDrawable? mauiDrawable = platformView.Background as MauiDrawable;
if (mauiDrawable == null && borderShape == null)
if (mauiDrawable is null)
return;
if (border.Shape is null)
return;
mauiDrawable?.SetBorderBrush(border.Stroke);
mauiDrawable?.SetBorderWidth(border.StrokeThickness);
platformView.UpdateStrokeDashPattern(border);
platformView.UpdateStrokeDashOffset(border);
mauiDrawable?.SetBorderMiterLimit(border.StrokeMiterLimit);
mauiDrawable?.SetBorderLineCap(border.StrokeLineCap);
mauiDrawable?.SetBorderLineJoin(border.StrokeLineJoin);
mauiDrawable.SetBorderBrush(border.Stroke);
mauiDrawable.SetBorderWidth(border.StrokeThickness);
platformView.UpdateStrokeDashPattern(border, mauiDrawable);
platformView.UpdateStrokeDashOffset(border, mauiDrawable);
mauiDrawable.SetBorderMiterLimit(border.StrokeMiterLimit);
mauiDrawable.SetBorderLineCap(border.StrokeLineCap);
mauiDrawable.SetBorderLineJoin(border.StrokeLineJoin);
}
public static void UpdateStrokeShape(this AView platformView, IBorderStroke border)
@ -34,7 +33,7 @@ namespace Microsoft.Maui.Platform
if (mauiDrawable == null && borderShape == null)
return;
platformView.UpdateMauiDrawable(border);
platformView.UpdateMauiDrawable(border, ref mauiDrawable);
}
public static void UpdateStroke(this AView platformView, IBorderStroke border)
@ -45,7 +44,7 @@ namespace Microsoft.Maui.Platform
if (mauiDrawable == null && stroke.IsNullOrEmpty())
return;
platformView.UpdateMauiDrawable(border);
platformView.UpdateMauiDrawable(border, ref mauiDrawable);
mauiDrawable?.SetBorderBrush(border.Stroke);
}
@ -60,29 +59,34 @@ namespace Microsoft.Maui.Platform
mauiDrawable?.SetBorderWidth(border.StrokeThickness);
}
public static void UpdateStrokeDashPattern(this AView platformView, IBorderStroke border)
public static void UpdateStrokeDashPattern(this AView platformView, IBorderStroke border) =>
UpdateStrokeDashPattern(platformView, border, platformView.Background as MauiDrawable);
internal static void UpdateStrokeDashPattern(this AView platformView, IBorderStroke border, MauiDrawable? mauiDrawable)
{
var strokeDashPattern = border.StrokeDashPattern;
MauiDrawable? mauiDrawable = platformView.Background as MauiDrawable;
bool hasBorder = border.Shape != null && border.Stroke != null;
if (mauiDrawable == null && !hasBorder && (strokeDashPattern == null || strokeDashPattern.Length == 0))
if (mauiDrawable is null)
return;
mauiDrawable?.SetBorderDash(border.StrokeDashPattern, border.StrokeDashOffset);
var strokeDashPattern = border.StrokeDashPattern;
bool hasBorder = border.Shape != null && border.Stroke != null;
if (!hasBorder && (strokeDashPattern == null || strokeDashPattern.Length == 0))
return;
mauiDrawable.SetBorderDash(strokeDashPattern, border.StrokeDashOffset);
}
public static void UpdateStrokeDashOffset(this AView platformView, IBorderStroke border)
public static void UpdateStrokeDashOffset(this AView platformView, IBorderStroke border) =>
UpdateStrokeDashOffset(platformView, border, platformView.Background as MauiDrawable);
internal static void UpdateStrokeDashOffset(this AView platformView, IBorderStroke border, MauiDrawable? mauiDrawable)
{
MauiDrawable? mauiDrawable = platformView.Background as MauiDrawable;
if (mauiDrawable is null)
return;
bool hasBorder = border.Shape != null && border.Stroke != null;
if (mauiDrawable == null && !hasBorder)
if (!hasBorder)
return;
mauiDrawable?.SetBorderDash(border.StrokeDashPattern, border.StrokeDashOffset);
mauiDrawable.SetBorderDash(border.StrokeDashPattern, border.StrokeDashOffset);
}
public static void UpdateStrokeMiterLimit(this AView platformView, IBorderStroke border)
@ -127,16 +131,15 @@ namespace Microsoft.Maui.Platform
mauiDrawable.InvalidateBorderBounds();
}
internal static void UpdateMauiDrawable(this AView platformView, IBorderStroke border)
internal static void UpdateMauiDrawable(this AView platformView, IBorderStroke border, ref MauiDrawable? mauiDrawable)
{
bool hasBorder = border.Shape != null;
if (!hasBorder)
return;
MauiDrawable? mauiDrawable = platformView.Background as MauiDrawable;
if (mauiDrawable == null)
mauiDrawable ??= platformView.Background as MauiDrawable;
if (mauiDrawable is null)
{
mauiDrawable = new MauiDrawable(platformView.Context);

Просмотреть файл

@ -20,10 +20,10 @@ namespace Microsoft.Maui.Platform
{
public partial class WrapperView : Grid, IDisposable
{
readonly Canvas _shadowCanvas;
Canvas? _shadowCanvas;
SpriteVisual? _shadowVisual;
DropShadow? _dropShadow;
UI.Xaml.Shapes.Rectangle? _shadowHost;
Rectangle? _shadowHost;
WSize _shadowHostSize;
Path? _borderPath;
@ -31,11 +31,6 @@ namespace Microsoft.Maui.Platform
public WrapperView()
{
_shadowCanvas = new Canvas();
_borderPath = new Path();
Children.Add(_shadowCanvas);
Children.Add(_borderPath);
}
long _visibilityDependencyPropertyCallbackToken;
@ -44,15 +39,17 @@ namespace Microsoft.Maui.Platform
get { return _child; }
internal set
{
if (_child != null)
if (_child is not null)
{
_child.SizeChanged -= OnChildSizeChanged;
_child.UnregisterPropertyChangedCallback(VisibilityProperty, _visibilityDependencyPropertyCallbackToken);
Children.Remove(_child);
}
if (value == null)
if (value is null)
{
return;
}
_child = value;
_child.SizeChanged += OnChildSizeChanged;
@ -76,19 +73,25 @@ namespace Microsoft.Maui.Platform
void UpdateClip()
{
if (Child == null)
if (Child is null)
{
return;
}
var clipGeometry = Clip;
if (clipGeometry == null)
if (clipGeometry is null)
{
return;
}
double width = Child.ActualWidth;
double height = Child.ActualHeight;
if (height <= 0 && width <= 0)
{
return;
}
var visual = ElementCompositionPreview.GetElementVisual(Child);
var compositor = visual.Compositor;
@ -118,27 +121,41 @@ namespace Microsoft.Maui.Platform
void UpdateBorder()
{
if (Border == null)
if (Border is null)
{
return;
}
if (_borderPath is null)
{
_borderPath = new();
int index = _shadowCanvas is not null ? 1 : 0;
Children.Insert(index, _borderPath);
}
IShape? borderShape = Border.Shape;
_borderPath?.UpdateBorderShape(borderShape, ActualWidth, ActualHeight);
_borderPath.UpdateBorderShape(borderShape, ActualWidth, ActualHeight);
_borderPath?.UpdateStroke(Border.Stroke);
_borderPath?.UpdateStrokeThickness(Border.StrokeThickness);
_borderPath?.UpdateStrokeDashPattern(Border.StrokeDashPattern);
_borderPath?.UpdateBorderDashOffset(Border.StrokeDashOffset);
_borderPath?.UpdateStrokeMiterLimit(Border.StrokeMiterLimit);
_borderPath?.UpdateStrokeLineCap(Border.StrokeLineCap);
_borderPath?.UpdateStrokeLineJoin(Border.StrokeLineJoin);
_borderPath.UpdateStroke(Border.Stroke);
_borderPath.UpdateStrokeThickness(Border.StrokeThickness);
_borderPath.UpdateStrokeDashPattern(Border.StrokeDashPattern);
_borderPath.UpdateBorderDashOffset(Border.StrokeDashOffset);
_borderPath.UpdateStrokeMiterLimit(Border.StrokeMiterLimit);
_borderPath.UpdateStrokeLineCap(Border.StrokeLineCap);
_borderPath.UpdateStrokeLineJoin(Border.StrokeLineJoin);
}
partial void ShadowChanged()
{
if (HasShadow)
{
UpdateShadowAsync().FireAndForget(IPlatformApplication.Current?.Services?.CreateLogger(nameof(WrapperView)));
}
else
{
CreateShadowAsync().FireAndForget(IPlatformApplication.Current?.Services?.CreateLogger(nameof(WrapperView)));
}
}
void OnChildSizeChanged(object sender, SizeChangedEventArgs e)
@ -153,7 +170,7 @@ namespace Microsoft.Maui.Platform
void OnChildVisibilityChanged(DependencyObject sender, DependencyProperty dp)
{
// OnChildSizeChanged does not fire for Visibility changes to child
if (sender is FrameworkElement child && _shadowCanvas.Children.Count > 0)
if (sender is FrameworkElement child && _shadowCanvas?.Children.Count > 0)
{
var shadowHost = _shadowCanvas.Children[0];
shadowHost.Visibility = child.Visibility;
@ -162,14 +179,20 @@ namespace Microsoft.Maui.Platform
void DisposeShadow()
{
if (_shadowCanvas == null)
if (_shadowCanvas is null)
{
return;
}
if (_shadowHost is not null)
{
ElementCompositionPreview.SetElementChildVisual(_shadowHost, null);
}
if (_shadowCanvas.Children.Count > 0)
{
_shadowCanvas.Children.RemoveAt(0);
}
if (_shadowVisual != null)
{
@ -187,16 +210,28 @@ namespace Microsoft.Maui.Platform
async Task CreateShadowAsync()
{
if (Child == null || Shadow == null || Shadow.Paint == null)
{
return;
}
var visual = ElementCompositionPreview.GetElementVisual(Child);
if (Clip != null && visual.Clip == null)
{
return;
}
double width = _shadowHostSize.Width;
double height = _shadowHostSize.Height;
if (_shadowCanvas is null)
{
_shadowCanvas = new();
// Shadow canvas must be the first child. The order of children (i.e. shadow canvas and border path) matters.
Children.Insert(0, _shadowCanvas);
}
var ttv = Child.TransformToVisual(_shadowCanvas);
global::Windows.Foundation.Point offset = ttv.TransformPoint(new global::Windows.Foundation.Point(0, 0));
@ -229,7 +264,9 @@ namespace Microsoft.Maui.Platform
async Task UpdateShadowAsync()
{
if (_dropShadow != null)
{
await SetShadowPropertiesAsync(_dropShadow, Shadow);
}
UpdateShadowSize();
}
@ -241,12 +278,16 @@ namespace Microsoft.Maui.Platform
float width = (float)_shadowHostSize.Width;
if (width <= 0)
{
width = (float)frameworkElement.ActualWidth;
}
float height = (float)_shadowHostSize.Height;
if (height <= 0)
{
height = (float)frameworkElement.ActualHeight;
}
if (_shadowVisual is not null)
{
@ -283,7 +324,9 @@ namespace Microsoft.Maui.Platform
dropShadow.Opacity = opacity;
if (shadowColor != null)
{
dropShadow.Color = shadowColor.ToWindowsColor();
}
dropShadow.Offset = new Vector3((float)offset.X, (float)offset.Y, 0);
dropShadow.Mask = await Child.GetAlphaMaskAsync();

Просмотреть файл

@ -28,7 +28,7 @@
<Using Include="BenchmarkDotNet.Order" />
<Using Include="BenchmarkDotNet.Running" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.10" />
<PackageReference Include="Xamarin.Android.Glide" Version="4.14.2.1" />
<PackageReference Include="Xamarin.Android.Glide" Version="$(_XamarinAndroidGlideVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Controls\src\Core\Controls.Core.csproj" />

Просмотреть файл

@ -1,12 +1,12 @@
using Android.Content;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.OS;
using Bumptech.Glide;
using Bumptech.Glide.Request.Target;
using Bumptech.Glide.Request.Transition;
using Java.Lang;
using Microsoft.Maui.Storage;
using AImageView = Android.Widget.ImageView;
using Path = System.IO.Path;
namespace Benchmarks.Droid;
@ -20,6 +20,7 @@ public class ImageBenchmark
Handler? handler;
Context? context;
string? imageFilename;
Typeface? defaultTypeface;
[GlobalSetup]
public void GlobalSetup()
@ -29,6 +30,7 @@ public class ImageBenchmark
imageView = new AImageView(context);
glide = Glide.Get(context);
handler = new Handler(Looper.MainLooper!);
defaultTypeface = Typeface.Default;
var imageName = "dotnet_bot.png";
var cacheDir = FileSystem.CacheDirectory;
@ -70,6 +72,25 @@ public class ImageBenchmark
await callback.SuccessTask;
}
[Benchmark]
public async Task ImageHelperFromFont()
{
var callback = new Callback();
handler!.Post(() =>
{
Microsoft.Maui.PlatformInterop.LoadImageFromFont(
context,
Color.Aquamarine,
"A",
defaultTypeface,
24,
callback);
});
await callback.SuccessTask;
}
class Callback : Java.Lang.Object, Microsoft.Maui.IImageLoaderCallback
{
readonly TaskCompletionSource<Drawable?> tcsDrawable = new TaskCompletionSource<Drawable?>();

Просмотреть файл

@ -261,6 +261,11 @@
WorkingDirectory="$(MauiRootDirectory)"
EnvironmentVariables="DOTNET_MULTILEVEL_LOOKUP=0"
/>
<Exec
Command="&quot;$(DotNetToolPath)&quot; workload update --print-rollback"
WorkingDirectory="$(MauiRootDirectory)"
EnvironmentVariables="DOTNET_MULTILEVEL_LOOKUP=0"
/>
<Touch Files="$(DotNetPacksDirectory).stamp" AlwaysCreate="true" />
</Target>

Просмотреть файл

@ -5,13 +5,9 @@ using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Microsoft.Maui.ApplicationModel;
#if WINDOWS
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Imaging;
#else
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media.Imaging;
#endif
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.Maui.Media
{
@ -32,32 +28,59 @@ namespace Microsoft.Maui.Media
public async Task<IScreenshotResult> CaptureAsync(UIElement element)
{
var bmp = new RenderTargetBitmap();
if (element is WebView2 webView)
{
InMemoryRandomAccessStream stream = new();
await webView.CoreWebView2.CapturePreviewAsync(Web.WebView2.Core.CoreWebView2CapturePreviewImageFormat.Png, stream);
// NOTE: Return to the main thread so we can access view properties such as
// width and height. Do not ConfigureAwait!
await bmp.RenderAsync(element);
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream);
PixelDataProvider provider = await decoder.GetPixelDataAsync();
byte[] byteArray = provider.DetachPixelData();
// get the view information first
var width = bmp.PixelWidth;
var height = bmp.PixelHeight;
return new ScreenshotResult((int)decoder.PixelWidth, (int)decoder.PixelHeight, byteArray, decoder.DpiX, decoder.DpiY);
}
else
{
var bmp = new RenderTargetBitmap();
// then potentially move to a different thread
var pixels = await bmp.GetPixelsAsync().AsTask().ConfigureAwait(false);
// NOTE: Return to the main thread so we can access view properties such as
// width and height. Do not ConfigureAwait!
await bmp.RenderAsync(element);
return new ScreenshotResult(width, height, pixels);
// get the view information first
var width = bmp.PixelWidth;
var height = bmp.PixelHeight;
// then potentially move to a different thread
IBuffer pixels = await bmp.GetPixelsAsync().AsTask().ConfigureAwait(false);
return new ScreenshotResult(width, height, pixels);
}
}
}
partial class ScreenshotResult
{
readonly byte[] bytes;
readonly double _dpiX;
readonly double _dpiY;
readonly byte[] _bytes;
internal ScreenshotResult(int width, int height, byte[] bytes, double dpiX, double dpiY)
{
Width = width;
Height = height;
_bytes = bytes;
_dpiX = dpiX;
_dpiY = dpiY;
}
public ScreenshotResult(int width, int height, IBuffer pixels)
{
Width = width;
Height = height;
bytes = pixels?.ToArray() ?? throw new ArgumentNullException(nameof(pixels));
_bytes = pixels.ToArray() ?? throw new ArgumentNullException(nameof(pixels));
_dpiX = 96;
_dpiY = 96;
}
async Task<Stream> PlatformOpenReadAsync(ScreenshotFormat format, int quality)
@ -74,14 +97,14 @@ namespace Microsoft.Maui.Media
}
Task<byte[]> PlatformToPixelBufferAsync() =>
Task.FromResult(bytes);
Task.FromResult(_bytes);
async Task EncodeAsync(ScreenshotFormat format, IRandomAccessStream ms)
{
var f = ToBitmapEncoder(format);
var encoder = await BitmapEncoder.CreateAsync(f, ms).AsTask().ConfigureAwait(false);
encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, (uint)Width, (uint)Height, 96, 96, bytes);
encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, (uint)Width, (uint)Height, _dpiX, _dpiY, _bytes);
await encoder.FlushAsync().AsTask().ConfigureAwait(false);
}