diff --git a/Ooui.Forms/VisualElementRenderer.cs b/Ooui.Forms/VisualElementRenderer.cs index f6c28ea..52a5d3d 100644 --- a/Ooui.Forms/VisualElementRenderer.cs +++ b/Ooui.Forms/VisualElementRenderer.cs @@ -8,6 +8,9 @@ namespace Ooui.Forms { public class VisualElementRenderer : Ooui.Element, IVisualElementRenderer where TElement : VisualElement { + bool disposedValue = false; // To detect redundant calls + VisualElementTracker _tracker; + readonly Color _defaultColor = Color.Clear; readonly PropertyChangedEventHandler _propertyChangedHandler; @@ -62,10 +65,10 @@ namespace Ooui.Forms if (element.BackgroundColor != Xamarin.Forms.Color.Default || (oldElement != null && element.BackgroundColor != oldElement.BackgroundColor)) SetBackgroundColor (element.BackgroundColor); - //if (_tracker == null) { - // _tracker = new VisualElementTracker (this); - // _tracker.NativeControlUpdated += (sender, e) => UpdateNativeWidget (); - //} + if (_tracker == null) { + _tracker = new VisualElementTracker (this); + _tracker.NativeControlUpdated += (sender, e) => UpdateNativeWidget (); + } //if (AutoPackage && _packager == null) { // _packager = new VisualElementPackager (this); @@ -130,37 +133,18 @@ namespace Ooui.Forms element.SendViewInitialized (nativeView); } - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - protected virtual void Dispose (bool disposing) { if (!disposedValue) { if (disposing) { - // TODO: dispose managed state (managed objects). } - - // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. - // TODO: set large fields to null. - disposedValue = true; } } - // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. - // ~VisualElementRenderer() { - // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. - // Dispose(false); - // } - - // This code added to correctly implement the disposable pattern. public void Dispose () { - // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose (true); - // TODO: uncomment the following line if the finalizer is overridden above. - // GC.SuppressFinalize(this); } - #endregion } } diff --git a/Ooui.Forms/VisualElementTracker.cs b/Ooui.Forms/VisualElementTracker.cs index ae3ffa0..a8f2f8d 100644 --- a/Ooui.Forms/VisualElementTracker.cs +++ b/Ooui.Forms/VisualElementTracker.cs @@ -1,12 +1,218 @@ -namespace Ooui.Forms -{ - internal class VisualElementTracker - { - private VisualElementRenderer visualElementRenderer; +using System; +using System.ComponentModel; +using System.Threading; +using Xamarin.Forms; +using Xamarin.Forms.Internals; - public VisualElementTracker(VisualElementRenderer visualElementRenderer) +namespace Ooui.Forms +{ + public class VisualElementTracker + { + readonly EventHandler> _batchCommittedHandler; + + readonly PropertyChangedEventHandler _propertyChangedHandler; + readonly EventHandler _sizeChangedEventHandler; + bool _disposed; + VisualElement _element; + + // Track these by hand because the calls down into iOS are too expensive + bool _isInteractive; + Rectangle _lastBounds; +#if !__MOBILE__ + Rectangle _lastParentBounds; +#endif + int _updateCount; + + public VisualElementTracker (IVisualElementRenderer renderer) { - this.visualElementRenderer = visualElementRenderer; + if (renderer == null) + throw new ArgumentNullException (nameof (renderer)); + + _propertyChangedHandler = HandlePropertyChanged; + _sizeChangedEventHandler = HandleSizeChanged; + _batchCommittedHandler = HandleRedrawNeeded; + + Renderer = renderer; + renderer.ElementChanged += OnRendererElementChanged; + SetElement (null, renderer.Element); + } + + IVisualElementRenderer Renderer { get; set; } + + public void Dispose () + { + Dispose (true); + } + + public event EventHandler NativeControlUpdated; + + protected virtual void Dispose (bool disposing) + { + if (_disposed) + return; + + _disposed = true; + + if (disposing) { + SetElement (_element, null); + + Renderer.ElementChanged -= OnRendererElementChanged; + Renderer = null; + } + } + + void HandlePropertyChanged (object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == VisualElement.XProperty.PropertyName || e.PropertyName == VisualElement.YProperty.PropertyName || e.PropertyName == VisualElement.WidthProperty.PropertyName || + e.PropertyName == VisualElement.HeightProperty.PropertyName || e.PropertyName == VisualElement.AnchorXProperty.PropertyName || e.PropertyName == VisualElement.AnchorYProperty.PropertyName || + e.PropertyName == VisualElement.TranslationXProperty.PropertyName || e.PropertyName == VisualElement.TranslationYProperty.PropertyName || e.PropertyName == VisualElement.ScaleProperty.PropertyName || + e.PropertyName == VisualElement.RotationProperty.PropertyName || e.PropertyName == VisualElement.RotationXProperty.PropertyName || e.PropertyName == VisualElement.RotationYProperty.PropertyName || + e.PropertyName == VisualElement.IsVisibleProperty.PropertyName || e.PropertyName == VisualElement.IsEnabledProperty.PropertyName || + e.PropertyName == VisualElement.InputTransparentProperty.PropertyName || e.PropertyName == VisualElement.OpacityProperty.PropertyName) + UpdateNativeControl (); // poorly optimized + } + + void HandleRedrawNeeded (object sender, EventArgs e) + { + UpdateNativeControl (); + } + + void HandleSizeChanged (object sender, EventArgs e) + { + UpdateNativeControl (); + } + + void OnRendererElementChanged (object s, VisualElementChangedEventArgs e) + { + if (_element == e.NewElement) + return; + + SetElement (_element, e.NewElement); + } + + void OnUpdateNativeControl () + { + var view = Renderer.Element; + var uiview = Renderer.NativeView; + + if (view == null || view.Batched) + return; + + var shouldInteract = !view.InputTransparent && view.IsEnabled; + if (_isInteractive != shouldInteract) { + _isInteractive = shouldInteract; + } + + var boundsChanged = _lastBounds != view.Bounds; + var viewParent = view.RealParent as VisualElement; + var parentBoundsChanged = _lastParentBounds != (viewParent == null ? Rectangle.Zero : viewParent.Bounds); + var thread = !boundsChanged; + + var anchorX = (float)view.AnchorX; + var anchorY = (float)view.AnchorY; + var translationX = (float)view.TranslationX; + var translationY = (float)view.TranslationY; + var rotationX = (float)view.RotationX; + var rotationY = (float)view.RotationY; + var rotation = (float)view.Rotation; + var scale = (float)view.Scale; + var width = (float)view.Width; + var height = (float)view.Height; + var x = (float)view.X; + var y = (float)view.Y; + var opacity = (float)view.Opacity; + var isVisible = view.IsVisible; + + var updateTarget = Interlocked.Increment (ref _updateCount); + + if (updateTarget != _updateCount) + return; + var parent = view.RealParent; + + if (isVisible && uiview.IsHidden) { + uiview.IsHidden = false; + } + + if (!isVisible && !uiview.IsHidden) { + uiview.IsHidden = true; + } + + parentBoundsChanged = true; + bool shouldUpdate = width > 0 && height > 0 && parent != null && (boundsChanged || parentBoundsChanged); + if (shouldUpdate) { + var visualParent = parent as VisualElement; + float newY = visualParent == null ? y : Math.Max (0, (float)(visualParent.Height - y - view.Height)); + var target = new Rectangle (x, newY, width, height); + + uiview.Style.Left = x; + uiview.Style.Top = newY; + uiview.Style.Width = width; + uiview.Style.Height = height; + } + else if (width <= 0 || height <= 0) { + return; + } + uiview.Style.Opacity = opacity; + + //var transform = 0; + //const double epsilon = 0.001; + //caLayer.AnchorPoint = new PointF (anchorX - 0.5f, anchorY - 0.5f); + + //// position is relative to anchor point + //if (Math.Abs (anchorX - .5) > epsilon) + // transform = transform.Translate ((anchorX - .5f) * width, 0, 0); + //if (Math.Abs (anchorY - .5) > epsilon) + // transform = transform.Translate (0, (anchorY - .5f) * height, 0); + + //if (Math.Abs (translationX) > epsilon || Math.Abs (translationY) > epsilon) + // transform = transform.Translate (translationX, translationY, 0); + + //if (Math.Abs (scale - 1) > epsilon) + // transform = transform.Scale (scale); + + //// not just an optimization, iOS will not "pixel align" a view which has m34 set + //if (Math.Abs (rotationY % 180) > epsilon || Math.Abs (rotationX % 180) > epsilon) + // transform.m34 = 1.0f / -400f; + + //if (Math.Abs (rotationX % 360) > epsilon) + // transform = transform.Rotate (rotationX * (float)Math.PI / 180.0f, 1.0f, 0.0f, 0.0f); + //if (Math.Abs (rotationY % 360) > epsilon) + // transform = transform.Rotate (rotationY * (float)Math.PI / 180.0f, 0.0f, 1.0f, 0.0f); + + //transform = transform.Rotate (rotation * (float)Math.PI / 180.0f, 0.0f, 0.0f, 1.0f); + //caLayer.Transform = transform; + + _lastBounds = view.Bounds; + _lastParentBounds = viewParent?.Bounds ?? Rectangle.Zero; + } + + void SetElement (VisualElement oldElement, VisualElement newElement) + { + if (oldElement != null) { + oldElement.PropertyChanged -= _propertyChangedHandler; + oldElement.SizeChanged -= _sizeChangedEventHandler; + oldElement.BatchCommitted -= _batchCommittedHandler; + } + + _element = newElement; + + if (newElement != null) { + newElement.BatchCommitted += _batchCommittedHandler; + newElement.SizeChanged += _sizeChangedEventHandler; + newElement.PropertyChanged += _propertyChangedHandler; + + UpdateNativeControl (); + } + } + + void UpdateNativeControl () + { + if (_disposed) + return; + + OnUpdateNativeControl (); + + NativeControlUpdated?.Invoke (this, EventArgs.Empty); } } -} \ No newline at end of file +} diff --git a/Ooui/Style.cs b/Ooui/Style.cs index a57b5a1..4be8590 100644 --- a/Ooui/Style.cs +++ b/Ooui/Style.cs @@ -232,6 +232,11 @@ namespace Ooui } } + public Value Opacity { + get => this["opacity"]; + set => this["opacity"] = value; + } + public Value Order { get => this["order"]; set => this["order"] = value;