Merge `main` into `net9.0` (#24341)
### Description of Change Update the net9.0 branch with all the latest things.
This commit is contained in:
Коммит
11aa1bdaf0
|
@ -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;
|
||||
|
|
Двоичные данные
src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ValidateWebViewScreenshot.png
Normal file
Двоичные данные
src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ValidateWebViewScreenshot.png
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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=""$(DotNetToolPath)" 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);
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче