diff --git a/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Controls/ImageTests/UnoSamples_Tests.Image.cs b/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Controls/ImageTests/UnoSamples_Tests.Image.cs index a3a8e76637b4..2d25e27dcc47 100644 --- a/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Controls/ImageTests/UnoSamples_Tests.Image.cs +++ b/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Controls/ImageTests/UnoSamples_Tests.Image.cs @@ -436,6 +436,40 @@ public void Image_Source_Nullify() ImageAssert.AreEqual(beforeLoad, afterClear, physicalRect, tolerance: PixelTolerance.Exclusive(1)); } + [Test] + [AutoRetry] + public void BitmapImage_is_SetSource_After_Delay() + { + Run("Uno.UI.Samples.UITests.Image.ImageSourceDelay"); + + var SUT = _app.Marked("imgControl"); + var txt = _app.Marked("txtStatus"); + var btn1 = _app.Marked("btnLoadBmp1"); + var btn2 = _app.Marked("btnLoadBmp2"); + + _app.WaitForElement(SUT, null, TimeSpan.FromSeconds(10),null, null); + _app.WaitForElement(txt); + + _app.WaitForElement(btn1); + _app.WaitForElement(btn2); + + _app.FastTap(btn1); + + _app.WaitForText(txt, "Bmp1"); + + var screenRect = _app.GetPhysicalRect(SUT); + + using var before = TakeScreenshot("Before"); + + _app.FastTap(btn2); + + _app.WaitForText(txt, "Bmp2"); + + using var after = TakeScreenshot("After"); + + ImageAssert.AreNotEqual(before, after, screenRect); + } + private void WaitForBitmapOrSvgLoaded() { var isLoaded = _app.Marked("isLoaded"); diff --git a/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Media/ImageBrushTests/ImageBrush_Tests.cs b/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Media/ImageBrushTests/ImageBrush_Tests.cs new file mode 100644 index 000000000000..cf3078440d73 --- /dev/null +++ b/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Media/ImageBrushTests/ImageBrush_Tests.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using SamplesApp.UITests.TestFramework; +using Uno.UITest.Helpers; +using Uno.UITest.Helpers.Queries; + +namespace SamplesApp.UITests.Windows_UI_Xaml_Media.ImageBrushTests +{ + [TestFixture] + public partial class ImageBrush_Tests : SampleControlUITestBase + { + [Test] + [AutoRetry] + [ActivePlatforms(Platform.Android, Platform.iOS)] + public void When_ImageBrush_Source_URI_Changes() + { + Run("Uno.UI.Samples.UITests.ImageBrushTestControl.ImageBrushChangingURI"); + + var border = _app.Marked("brCont"); + var txt = _app.Marked("txtStatus"); + + _app.WaitForElement(border); + _app.WaitForElement(txt); + + var screenRect = _app.GetPhysicalRect(border); + + using var before = TakeScreenshot("Before", ignoreInSnapshotCompare: true); + + _app.FastTap("btnImage1"); + + _app.WaitForText("txtStatus", "Changed"); + + using var after = TakeScreenshot("After"); + + ImageAssert.AreNotEqual(before, after, screenRect); + } + } +} diff --git a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems index 19db85277d1a..141211fc2c67 100644 --- a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems +++ b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems @@ -1557,6 +1557,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -4161,6 +4165,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -6164,6 +6172,9 @@ ImageAlignment2541.xaml + + ImageSourceDelay.xaml + ImageSourceUrlMsAppDataScheme.xaml @@ -7439,6 +7450,9 @@ LinearGradientBrush_Opacity.xaml + + ImageBrushChangingURI.xaml + ImageBrushShapeStretchesAlignments.xaml diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/ImageTests/ImageSourceDelay.xaml b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/ImageTests/ImageSourceDelay.xaml new file mode 100644 index 000000000000..ab952044430a --- /dev/null +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/ImageTests/ImageSourceDelay.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/ImageTests/ImageSourceDelay.xaml.cs b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/ImageTests/ImageSourceDelay.xaml.cs new file mode 100644 index 000000000000..11f25bda0072 --- /dev/null +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Controls/ImageTests/ImageSourceDelay.xaml.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading.Tasks; +using Uno.UI.Samples.Controls; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.Storage; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Media.Imaging; +using Windows.UI.Xaml.Navigation; + +namespace Uno.UI.Samples.UITests.Image +{ + [SampleControlInfo("Image", "ImageSourceDelay")] + public sealed partial class ImageSourceDelay : UserControl + { + public ImageSourceDelay() + { + this.InitializeComponent(); + } + + private void btnLoadBmp1_Click(object sender, RoutedEventArgs e) + { + imgControl.Source = GetBitmap("ms-appx:///Assets/search.png"); + txtStatus.Text = "Bmp1"; + } + + private void btnLoadBmp2_Click(object sender, RoutedEventArgs e) + { + imgControl.Source = GetBitmap("ms-appx:///Assets/cart.png"); + txtStatus.Text = "Bmp2"; + } + + private BitmapImage GetBitmap(string fname) + { + var bmp = new BitmapImage(); + LoadImage(); + return bmp; + + async void LoadImage() + { + var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri(fname)); + using (var fs = await file.OpenStreamForReadAsync()) + { + await Task.Delay(100); + await bmp.SetSourceAsync(fs.AsRandomAccessStream()); + } + + } + } + } +} diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/ImageBrushTests/ImageBrushChangingURI.xaml b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/ImageBrushTests/ImageBrushChangingURI.xaml new file mode 100644 index 000000000000..f7a4f4543c62 --- /dev/null +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/ImageBrushTests/ImageBrushChangingURI.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/ImageBrushTests/ImageBrushChangingURI.xaml.cs b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/ImageBrushTests/ImageBrushChangingURI.xaml.cs new file mode 100644 index 000000000000..b0a44a856979 --- /dev/null +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/ImageBrushTests/ImageBrushChangingURI.xaml.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Uno.UI.Samples.Controls; +using Uno.UI.Samples.UITests.ImageTests.Models; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +namespace Uno.UI.Samples.UITests.ImageBrushTestControl +{ + [SampleControlInfo("ImageBrushTestControl", "ImageBrushChangingURI")] + public sealed partial class ImageBrushChangingURI : UserControl + { + public static readonly DependencyProperty TileContentProperty = DependencyProperty.Register(nameof(TileContent), typeof(Uri), typeof(ImageBrushChangingURI), new PropertyMetadata(new Uri("https://cdn.pixabay.com/photo/2020/03/09/17/51/narcis-4916584_960_720.jpg"))); + + public Uri TileContent + { + get { return (Uri)GetValue(TileContentProperty); } + set { SetValue(TileContentProperty, value); } + } + + public ImageBrushChangingURI() + { + this.InitializeComponent(); + + TileContent = new Uri("ms-appx:///Assets/rect.png"); + } + + private void OnButton1(object sender, RoutedEventArgs e) + { + TileContent = new Uri("ms-appx:///Assets/cart.png"); + txtStatus.Text = "Changed"; + } + private void OnButton2(object sender, RoutedEventArgs e) + { + TileContent = new Uri("ms-appx:///Assets/rect.png"); + txtStatus.Text = "Changed"; + } + + } +} diff --git a/src/Uno.UI/UI/Xaml/Controls/Border/BorderLayerRenderer.Android.cs b/src/Uno.UI/UI/Xaml/Controls/Border/BorderLayerRenderer.Android.cs index a03005b73c48..53a198198bef 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Border/BorderLayerRenderer.Android.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Border/BorderLayerRenderer.Android.cs @@ -60,8 +60,7 @@ public void UpdateLayer( return; } - var imageHasChanged = newState.BackgroundImageSource != previousLayoutState?.BackgroundImageSource; - var shouldDisposeEagerly = imageHasChanged || newState.BackgroundImageSource == null; + var shouldDisposeEagerly = newState.BackgroundImageSource == null; if (shouldDisposeEagerly) { // Clear previous value anyway in order to make sure the previous values are unset before the new ones. @@ -494,6 +493,8 @@ private class LayoutState : IEquatable public readonly Thickness Padding; public readonly Color? BackgroundFallbackColor; + public readonly long? StateVersion; + public LayoutState(Windows.Foundation.Rect area, Brush background, Thickness borderThickness, Brush borderBrush, CornerRadius cornerRadius, Thickness padding) { Area = area; @@ -506,6 +507,8 @@ public LayoutState(Windows.Foundation.Rect area, Brush background, Thickness bor var imageBrushBackground = Background as ImageBrush; BackgroundImageSource = imageBrushBackground?.ImageSource; + StateVersion = BackgroundImageSource?.StateVersion; + BackgroundColor = (Background as SolidColorBrush)?.Color; BorderBrushColor = (BorderBrush as SolidColorBrush)?.Color; @@ -515,6 +518,7 @@ public LayoutState(Windows.Foundation.Rect area, Brush background, Thickness bor public bool Equals(LayoutState other) { return other != null + && other.StateVersion == StateVersion && other.Area == Area && other.Background == Background && other.BackgroundImageSource == BackgroundImageSource diff --git a/src/Uno.UI/UI/Xaml/Controls/Image/Image.cs b/src/Uno.UI/UI/Xaml/Controls/Image/Image.cs index 3206d0e556d2..d39ac04c54f9 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Image/Image.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Image/Image.cs @@ -87,10 +87,10 @@ private void OnSourceChanged(ImageSource newValue, bool forceReload = false) } else if (newValue is WriteableBitmap wb) { - wb.Invalidated += OnInvalidated; - _sourceDisposable.Disposable = Disposable.Create(() => wb.Invalidated -= OnInvalidated); + newValue.InvalidateSource += OnInvalidateSource; + _sourceDisposable.Disposable = Disposable.Create(() => newValue.InvalidateSource -= OnInvalidateSource); - void OnInvalidated(object sdn, EventArgs args) + void OnInvalidateSource(object sdn, EventArgs args) { _openedSource = null; TryOpenImage(); @@ -138,7 +138,6 @@ void OnInvalidated(object sdn, EventArgs args) _sourceDisposable.Disposable = compositeDisposable; } - TryOpenImage(forceReload); } diff --git a/src/Uno.UI/UI/Xaml/Media/Brush.Android.cs b/src/Uno.UI/UI/Xaml/Media/Brush.Android.cs index 60d6e8b18726..bc9c89ac8756 100644 --- a/src/Uno.UI/UI/Xaml/Media/Brush.Android.cs +++ b/src/Uno.UI/UI/Xaml/Media/Brush.Android.cs @@ -90,33 +90,18 @@ internal static IDisposable AssignAndObserveBrush(Brush b, ColorSetterHandler co } else if (b is ImageBrush imageBrush && imageBrushCallback != null) { - var disposables = new CompositeDisposable(5); + var disposables = new CompositeDisposable(6); imageBrushCallback(); + + void ImageChanged() + { + imageBrushCallback(); + } - imageBrush.RegisterDisposablePropertyChangedCallback( - ImageBrush.ImageSourceProperty, - (_, __) => imageBrushCallback() - ).DisposeWith(disposables); - - imageBrush.RegisterDisposablePropertyChangedCallback( - ImageBrush.StretchProperty, - (_, __) => imageBrushCallback() - ).DisposeWith(disposables); - - imageBrush.RegisterDisposablePropertyChangedCallback( - ImageBrush.AlignmentXProperty, - (_, __) => imageBrushCallback() - ).DisposeWith(disposables); - - imageBrush.RegisterDisposablePropertyChangedCallback( - ImageBrush.AlignmentYProperty, - (_, __) => imageBrushCallback() - ).DisposeWith(disposables); + imageBrush.ImageChanged += ImageChanged; + Disposable.Create(() => imageBrush.ImageChanged -= ImageChanged) + .DisposeWith(disposables); - imageBrush.RegisterDisposablePropertyChangedCallback( - ImageBrush.RelativeTransformProperty, - (_, __) => imageBrushCallback() - ).DisposeWith(disposables); return disposables; } diff --git a/src/Uno.UI/UI/Xaml/Media/ImageBrush.Android.cs b/src/Uno.UI/UI/Xaml/Media/ImageBrush.Android.cs index abb42ba2d144..d01fa0a7ef27 100644 --- a/src/Uno.UI/UI/Xaml/Media/ImageBrush.Android.cs +++ b/src/Uno.UI/UI/Xaml/Media/ImageBrush.Android.cs @@ -25,12 +25,48 @@ public partial class ImageBrush private bool _imageSourceChanged; private Windows.Foundation.Rect _lastDrawRect; private readonly SerialDisposable _refreshPaint = new SerialDisposable(); + private readonly SerialDisposable _sourceDisposable = new SerialDisposable(); + + internal event Action ImageChanged; + + internal override void OnPropertyChanged2(DependencyPropertyChangedEventArgs args) + { + base.OnPropertyChanged2(args); + switch (args.Property.Name) + { + case nameof(Stretch): + case nameof(AlignmentX): + case nameof(AlignmentY): + case nameof(RelativeTransform): + ImageChanged?.Invoke(); + break; + } + } partial void OnSourceChangedPartial(ImageSource newValue, ImageSource oldValue) { + if (newValue is not null) + { + newValue.InvalidateSource += OnInvalidateSource; + _sourceDisposable.Disposable = Disposable.Create(() => newValue.InvalidateSource -= OnInvalidateSource); + + void OnInvalidateSource(object sdn, EventArgs args) + { + _imageSourceChanged = true; + _onImageLoaded?.Invoke(); + ImageChanged?.Invoke(); + } + } + else + { + _sourceDisposable.Disposable = null; + } + + oldValue?.Dispose(); _imageSourceChanged = true; _onImageLoaded?.Invoke(); + ImageChanged?.Invoke(); return; } @@ -72,7 +108,7 @@ private async void RefreshImageAsync(Windows.Foundation.Rect drawRect) await RefreshImage(cd.Token, drawRect); } - + private async Task RefreshImage(CancellationToken ct, Windows.Foundation.Rect drawRect) { if (ImageSource is ImageSource imageSource && (_imageSourceChanged || !imageSource.IsOpened) && !drawRect.HasZeroArea()) diff --git a/src/Uno.UI/UI/Xaml/Media/ImageSource.cs b/src/Uno.UI/UI/Xaml/Media/ImageSource.cs index 7012691a56d2..b821072e4e0c 100644 --- a/src/Uno.UI/UI/Xaml/Media/ImageSource.cs +++ b/src/Uno.UI/UI/Xaml/Media/ImageSource.cs @@ -8,9 +8,10 @@ using Uno.Extensions; using Uno.Foundation.Logging; using Uno.Helpers; +using Uno.UI; using Uno.UI.Xaml.Media; +using Windows.Storage.Streams; using Windows.UI.Xaml.Media.Imaging; -using Uno.UI; #if !IS_UNO using Uno.Web.Query; @@ -26,6 +27,42 @@ public partial class ImageSource : DependencyObject, IDisposable private protected static HttpClient _httpClient; private protected ImageData _imageData; +#if !(__NETSTD__) + internal event EventHandler InvalidateSource; + protected internal long StateVersion + { + get; + protected set ; + } + + protected virtual void OnInvalidateSource() + { + StateVersion++; + InvalidateSource?.Invoke(this, EventArgs.Empty); + } + + #region Stream DependencyProperty + internal Stream Stream + { + get { return (Stream)GetValue(StreamProperty); } + set { SetValue(StreamProperty, value); } + } + + internal static DependencyProperty StreamProperty { get; } = + DependencyProperty.Register("Stream", typeof(Stream), typeof(ImageSource), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.WeakStorage, + (s, e) => ((ImageSource)s)?.OnStreamChanged(e))); + + internal void OnStreamChanged(DependencyPropertyChangedEventArgs e) + { + OnInvalidateSource(); + } + + #endregion + +#endif + + public static class TraceProvider { public static readonly Guid Id = Guid.Parse("{FC4E2720-2DCF-418C-B360-93314AB3B813}"); @@ -54,11 +91,7 @@ private void InitializeDownloader() { Downloader = DefaultDownloader; } - -#if !(__NETSTD__) - internal Stream Stream { get; set; } -#endif - + internal string FilePath { get; private set; } public bool UseTargetSize { get; set; } diff --git a/src/Uno.UI/UI/Xaml/Media/ImageSource.netstd.cs b/src/Uno.UI/UI/Xaml/Media/ImageSource.netstd.cs index 0b9947023c42..fbae9f4d07b2 100644 --- a/src/Uno.UI/UI/Xaml/Media/ImageSource.netstd.cs +++ b/src/Uno.UI/UI/Xaml/Media/ImageSource.netstd.cs @@ -77,7 +77,7 @@ private protected virtual bool TryOpenSourceAsync(CancellationToken ct, int? tar return false; } - private protected void InvalidateSource() + private protected void OnInvalidateSource() { _imageData = default; if (_subscriptions.Count > 0 || this is SvgImageSource) diff --git a/src/Uno.UI/UI/Xaml/Media/Imaging/BitmapImage.cs b/src/Uno.UI/UI/Xaml/Media/Imaging/BitmapImage.cs index 9c88f670596e..a5d53668387a 100644 --- a/src/Uno.UI/UI/Xaml/Media/Imaging/BitmapImage.cs +++ b/src/Uno.UI/UI/Xaml/Media/Imaging/BitmapImage.cs @@ -32,9 +32,7 @@ private void OnUriSourceChanged(DependencyPropertyChangedEventArgs e) UnloadImageData(); } InitFromUri(e.NewValue as Uri); -#if UNO_REFERENCE_API - InvalidateSource(); -#endif + OnInvalidateSource(); } #endregion diff --git a/src/Uno.UI/UI/Xaml/Media/Imaging/BitmapSource.cs b/src/Uno.UI/UI/Xaml/Media/Imaging/BitmapSource.cs index 98210237ab49..e3e9c5a8a084 100644 --- a/src/Uno.UI/UI/Xaml/Media/Imaging/BitmapSource.cs +++ b/src/Uno.UI/UI/Xaml/Media/Imaging/BitmapSource.cs @@ -120,7 +120,7 @@ Task ForceLoad(CancellationToken ct) using var r = ct.Register(() => tcs.TrySetCanceled()); using var s = Subscribe(OnChanged); - InvalidateSource(); + OnInvalidateSource(); await tcs.Task; diff --git a/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.cs b/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.cs index 5b8e92434f23..798adc79817c 100644 --- a/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.cs @@ -91,7 +91,7 @@ public IAsyncAction RenderAsync(UIElement? element, int scaledWidth, int scaledH ?? Window.Current.Content; (_bufferSize, PixelWidth, PixelHeight) = RenderAsBgra8_Premul(elementToRender, ref _buffer, new Size(scaledWidth, scaledHeight)); #if __WASM__ || __SKIA__ - InvalidateSource(); + OnInvalidateSource(); #endif } catch (Exception error) @@ -115,7 +115,7 @@ public IAsyncAction RenderAsync(UIElement? element) (_bufferSize, PixelWidth, PixelHeight) = RenderAsBgra8_Premul(elementToRender, ref _buffer); #if __WASM__ || __SKIA__ - InvalidateSource(); + OnInvalidateSource(); #endif } catch (Exception error) diff --git a/src/Uno.UI/UI/Xaml/Media/Imaging/SvgImageSource.cs b/src/Uno.UI/UI/Xaml/Media/Imaging/SvgImageSource.cs index a89184ecc6aa..9ffbd01fd7c9 100644 --- a/src/Uno.UI/UI/Xaml/Media/Imaging/SvgImageSource.cs +++ b/src/Uno.UI/UI/Xaml/Media/Imaging/SvgImageSource.cs @@ -59,7 +59,7 @@ private void OnUriSourceChanged(DependencyPropertyChangedEventArgs e) } InitFromUri(e.NewValue as Uri); #if __NETSTD__ - InvalidateSource(); + OnInvalidateSource(); #endif } @@ -98,7 +98,7 @@ Task SetSourceAsync( using var x = Subscribe(OnChanged); - InvalidateSource(); + OnInvalidateSource(); return await tcs.Task; diff --git a/src/Uno.UI/UI/Xaml/Media/Imaging/WriteableBitmap.cs b/src/Uno.UI/UI/Xaml/Media/Imaging/WriteableBitmap.cs index 877bb3021029..072f1cc28665 100644 --- a/src/Uno.UI/UI/Xaml/Media/Imaging/WriteableBitmap.cs +++ b/src/Uno.UI/UI/Xaml/Media/Imaging/WriteableBitmap.cs @@ -26,10 +26,8 @@ public WriteableBitmap(int pixelWidth, int pixelHeight) : base() public void Invalidate() { -#if __WASM__ || __SKIA__ - InvalidateSource(); -#endif + OnInvalidateSource(); Invalidated?.Invoke(this, EventArgs.Empty); - } + } } }