File: Android\VisualElementTracker.cs
Web Access
Project: src\src\Compatibility\Core\src\Compatibility.csproj (Microsoft.Maui.Controls.Compatibility)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Android.Content;
using Android.Views;
using AndroidX.CoordinatorLayout.Widget;
using AndroidX.Fragment.App;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Handlers;
using AView = Android.Views.View;
using Object = Java.Lang.Object;
 
namespace Microsoft.Maui.Controls.Compatibility.Platform.Android
{
	public class VisualElementTracker : IDisposable
	{
		readonly EventHandler<EventArg<VisualElement>> _batchCommittedHandler;
		readonly IList<string> _batchedProperties = new List<string>();
		readonly PropertyChangedEventHandler _propertyChangedHandler;
		Context _context;
 
		bool _disposed;
 
		VisualElement _element;
		bool _initialUpdateNeeded = true;
		bool _layoutNeeded;
		IVisualElementRenderer _renderer;
		AttachTracker _attachTracker;
		public VisualElementTracker(IVisualElementRenderer renderer)
		{
			_batchCommittedHandler = HandleRedrawNeeded;
			_propertyChangedHandler = HandlePropertyChanged;
 
			_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
			_context = renderer.View.Context;
			_renderer.ElementChanged += RendererOnElementChanged;
 
			VisualElement view = renderer.Element;
			SetElement(null, view);
 
			renderer.View.SetCameraDistance(3600);
			_attachTracker = AttachTracker.Instance;
			renderer.View.AddOnAttachStateChangeListener(_attachTracker);
 
		}
 
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}
 
		protected virtual void Dispose(bool disposing)
		{
			if (_disposed)
				return;
 
			_disposed = true;
 
			if (disposing)
			{
				SetElement(_element, null);
 
				if (_renderer != null)
				{
					_renderer.ElementChanged -= RendererOnElementChanged;
 
					if (_renderer.View.IsAlive() && _attachTracker.IsAlive())
					{
						_renderer.View.RemoveOnAttachStateChangeListener(_attachTracker);
						_attachTracker = null;
					}
 
					_renderer = null;
					_context = null;
				}
			}
		}
 
		public void UpdateLayout()
		{
			Performance.Start(out string reference);
 
			VisualElement view = _renderer.Element;
			AView aview = _renderer.View;
 
			var headlessOffset = CompressedLayout.GetHeadlessOffset(view);
			var x = (int)_context.ToPixels(view.X + headlessOffset.X);
			var y = (int)_context.ToPixels(view.Y + headlessOffset.Y);
			var width = Math.Max(0, (int)_context.ToPixels(view.Width));
			var height = Math.Max(0, (int)_context.ToPixels(view.Height));
 
 
			if (aview is MauiViewGroup formsViewGroup)
			{
				Performance.Start(reference, "MeasureAndLayout");
				formsViewGroup.MeasureAndLayout(MeasureSpecFactory.MakeMeasureSpec(width, MeasureSpecMode.Exactly), MeasureSpecFactory.MakeMeasureSpec(height, MeasureSpecMode.Exactly), x, y, x + width, y + height);
				Performance.Stop(reference, "MeasureAndLayout");
			}
			else if ((aview is LayoutViewGroup || aview is ContentViewGroup || aview is CoordinatorLayout || aview is FragmentContainerView) && width == 0 && height == 0)
			{
				// Nothing to do here; just chill.
			}
			else
			{
				Performance.Start(reference, "Measure");
				aview.Measure(MeasureSpecFactory.MakeMeasureSpec(width, MeasureSpecMode.Exactly), MeasureSpecFactory.MakeMeasureSpec(height, MeasureSpecMode.Exactly));
				Performance.Stop(reference, "Measure");
 
				Performance.Start(reference, "Layout");
				aview.Layout(x, y, x + width, y + height);
				Performance.Stop(reference, "Layout");
			}
 
			// We have to make sure to update the ClipBounds to match the new size of the ViewGroup
			UpdateClipToBounds();
			UpdateClip();
 
			Performance.Stop(reference);
 
			//On Width or Height changes, the anchors needs to be updated
			UpdateAnchorX();
			UpdateAnchorY();
		}
 
		void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			if (_renderer == null || !_renderer.View.IsAlive())
			{
				return;
			}
 
			if (e.PropertyName == Layout.IsClippedToBoundsProperty.PropertyName)
			{
				UpdateClipToBounds();
				return;
			}
 
			if (_renderer.Element.Batched)
			{
				if (e.PropertyName == VisualElement.XProperty.PropertyName || e.PropertyName == VisualElement.YProperty.PropertyName || e.PropertyName == VisualElement.WidthProperty.PropertyName ||
					e.PropertyName == VisualElement.HeightProperty.PropertyName)
					_layoutNeeded = true;
				else if (e.PropertyName == VisualElement.AnchorXProperty.PropertyName || e.PropertyName == VisualElement.AnchorYProperty.PropertyName || e.PropertyName == VisualElement.ScaleProperty.PropertyName || e.PropertyName == VisualElement.ScaleXProperty.PropertyName || e.PropertyName == VisualElement.ScaleYProperty.PropertyName ||
						 e.PropertyName == VisualElement.RotationProperty.PropertyName || e.PropertyName == VisualElement.RotationXProperty.PropertyName || e.PropertyName == VisualElement.RotationYProperty.PropertyName ||
						 e.PropertyName == VisualElement.IsVisibleProperty.PropertyName || e.PropertyName == VisualElement.OpacityProperty.PropertyName ||
						 e.PropertyName == VisualElement.TranslationXProperty.PropertyName || e.PropertyName == VisualElement.TranslationYProperty.PropertyName)
				{
					if (!_batchedProperties.Contains(e.PropertyName))
						_batchedProperties.Add(e.PropertyName);
				}
				return;
			}
 
			if (e.PropertyName == VisualElement.XProperty.PropertyName || e.PropertyName == VisualElement.YProperty.PropertyName || e.PropertyName == VisualElement.WidthProperty.PropertyName ||
				e.PropertyName == VisualElement.HeightProperty.PropertyName)
				_renderer.View.MaybeRequestLayout();
			else if (e.PropertyName == VisualElement.AnchorXProperty.PropertyName)
				UpdateAnchorX();
			else if (e.PropertyName == VisualElement.AnchorYProperty.PropertyName)
				UpdateAnchorY();
			else if (e.PropertyName == VisualElement.ScaleProperty.PropertyName
					 || e.PropertyName == VisualElement.ScaleXProperty.PropertyName
					 || e.PropertyName == VisualElement.ScaleYProperty.PropertyName)
				UpdateScale();
			else if (e.PropertyName == VisualElement.RotationProperty.PropertyName)
				UpdateRotation();
			else if (e.PropertyName == VisualElement.RotationXProperty.PropertyName)
				UpdateRotationX();
			else if (e.PropertyName == VisualElement.RotationYProperty.PropertyName)
				UpdateRotationY();
			else if (e.PropertyName == VisualElement.IsVisibleProperty.PropertyName)
				UpdateIsVisible();
			else if (e.PropertyName == VisualElement.OpacityProperty.PropertyName)
				UpdateOpacity();
			else if (e.PropertyName == VisualElement.TranslationXProperty.PropertyName)
				UpdateTranslationX();
			else if (e.PropertyName == VisualElement.TranslationYProperty.PropertyName)
				UpdateTranslationY();
			else if (e.PropertyName == VisualElement.IsEnabledProperty.PropertyName)
				UpdateIsEnabled();
			else if (e.PropertyName == VisualElement.ClipProperty.PropertyName)
				UpdateClip();
		}
 
		void HandleRedrawNeeded(object sender, EventArg<VisualElement> e)
		{
			foreach (string propertyName in _batchedProperties)
				HandlePropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			_batchedProperties.Clear();
 
			if (_layoutNeeded)
				_renderer.View.MaybeRequestLayout();
			_layoutNeeded = false;
		}
 
		void HandleViewAttachedToWindow()
		{
			if (_initialUpdateNeeded)
			{
				UpdateNativeView(this, EventArgs.Empty);
				_initialUpdateNeeded = false;
			}
 
			UpdateClipToBounds();
			UpdateClip();
		}
 
		void RendererOnElementChanged(object sender, VisualElementChangedEventArgs args)
		{
			SetElement(args.OldElement, args.NewElement);
		}
 
		void SetElement(VisualElement oldElement, VisualElement newElement)
		{
			if (oldElement != null)
			{
				oldElement.BatchCommitted -= _batchCommittedHandler;
				oldElement.PropertyChanged -= _propertyChangedHandler;
				_context = null;
			}
 
			_element = newElement;
			if (newElement != null)
			{
				newElement.BatchCommitted += _batchCommittedHandler;
				newElement.PropertyChanged += _propertyChangedHandler;
				_context = _renderer.View.Context;
 
				if (oldElement != null)
				{
					AView view = _renderer.View;
 
					// ReSharper disable CompareOfFloatsByEqualityOperator
					if (oldElement.AnchorX != newElement.AnchorX)
						UpdateAnchorX();
					if (oldElement.AnchorY != newElement.AnchorY)
						UpdateAnchorY();
					if (oldElement.IsVisible != newElement.IsVisible)
						UpdateIsVisible();
					if (oldElement.IsEnabled != newElement.IsEnabled)
						view.Enabled = newElement.IsEnabled;
					if (oldElement.Opacity != newElement.Opacity)
						UpdateOpacity();
					if (oldElement.Rotation != newElement.Rotation)
						UpdateRotation();
					if (oldElement.RotationX != newElement.RotationX)
						UpdateRotationX();
					if (oldElement.RotationY != newElement.RotationY)
						UpdateRotationY();
					if (oldElement.Scale != newElement.Scale || oldElement.ScaleX != newElement.ScaleX || oldElement.ScaleY != newElement.ScaleY)
						UpdateScale();
					// ReSharper restore CompareOfFloatsByEqualityOperator
 
					_initialUpdateNeeded = false;
				}
			}
		}
 
		[PortHandler]
		void UpdateAnchorX()
		{
			VisualElement view = _renderer.Element;
			AView aview = _renderer.View;
 
			float currentPivot = aview.PivotX;
			var target = (float)(view.AnchorX * _context.ToPixels(view.Width));
			if (currentPivot != target)
				aview.PivotX = target;
		}
 
		[PortHandler]
		void UpdateAnchorY()
		{
			VisualElement view = _renderer.Element;
			AView aview = _renderer.View;
 
			float currentPivot = aview.PivotY;
			var target = (float)(view.AnchorY * _context.ToPixels(view.Height));
			if (currentPivot != target)
				aview.PivotY = target;
		}
 
		void UpdateClipToBounds()
		{
			if (!(_renderer.Element is Layout layout))
			{
				return;
			}
 
			bool shouldClip = layout.IsClippedToBounds;
 
			if (!(_renderer.View is ViewGroup viewGroup))
			{
				return;
			}
 
			// Forms layouts should not impose clipping on their children	
			viewGroup.SetClipChildren(false);
 
			// But if IsClippedToBounds is true, they _should_ enforce clipping at their own edges	
			viewGroup.ClipBounds = shouldClip ? new global::Android.Graphics.Rect(0, 0, viewGroup.Width, viewGroup.Height) : null;
		}
 
		void UpdateClip()
		{
			var aView = _renderer.View;
			aView?.Invalidate();
		}
 
		[PortHandler]
		void UpdateIsVisible()
		{
			VisualElement view = _renderer.Element;
			AView aview = _renderer.View;
 
			if (view.IsVisible && aview.Visibility != ViewStates.Visible)
				aview.Visibility = ViewStates.Visible;
			if (!view.IsVisible && aview.Visibility != ViewStates.Gone)
				aview.Visibility = ViewStates.Gone;
 
			aview.Invalidate();
			aview.RequestLayout();
		}
 
		void UpdateNativeView(object sender, EventArgs e)
		{
			Performance.Start(out string reference);
 
			VisualElement view = _renderer.Element;
			AView aview = _renderer.View;
 
			if (aview is MauiViewGroup formsViewGroup)
			{
				formsViewGroup.SendBatchUpdate((float)(view.AnchorX * _context.ToPixels(view.Width)),
											   (float)(view.AnchorY * _context.ToPixels(view.Height)),
											   (int)(view.IsVisible ? ViewStates.Visible : ViewStates.Invisible),
											   view.IsEnabled,
											   (float)view.Opacity,
											   (float)view.Rotation,
											   (float)view.RotationX,
											   (float)view.RotationY,
											   (float)view.Scale * (float)view.ScaleX,
											   (float)view.Scale * (float)view.ScaleY,
											   _context.ToPixels(view.TranslationX),
											   _context.ToPixels(view.TranslationY));
			}
			else
			{
				MauiViewGroup.SendViewBatchUpdate(aview,
												   (float)(view.AnchorX * _context.ToPixels(view.Width)),
												   (float)(view.AnchorY * _context.ToPixels(view.Height)),
												   (int)(view.IsVisible ? ViewStates.Visible : ViewStates.Invisible),
												   view.IsEnabled,
												   (float)view.Opacity,
												   (float)view.Rotation,
												   (float)view.RotationX,
												   (float)view.RotationY,
												   (float)view.Scale * (float)view.ScaleX,
												   (float)view.Scale * (float)view.ScaleY,
												   _context.ToPixels(view.TranslationX),
												   _context.ToPixels(view.TranslationY));
			}
 
			Performance.Stop(reference);
		}
 
		[PortHandler]
		void UpdateOpacity()
		{
			Performance.Start(out string reference);
 
			VisualElement view = _renderer.Element;
			AView aview = _renderer.View;
 
			aview.Alpha = (float)view.Opacity;
 
			Performance.Stop(reference);
		}
 
		[PortHandler]
		void UpdateRotation()
		{
			VisualElement view = _renderer.Element;
			AView aview = _renderer.View;
 
			aview.Rotation = (float)view.Rotation;
		}
 
		[PortHandler]
		void UpdateRotationX()
		{
			VisualElement view = _renderer.Element;
			AView aview = _renderer.View;
 
			aview.RotationX = (float)view.RotationX;
		}
 
		[PortHandler]
		void UpdateRotationY()
		{
			VisualElement view = _renderer.Element;
			AView aview = _renderer.View;
 
			aview.RotationY = (float)view.RotationY;
		}
 
		[PortHandler]
		void UpdateScale()
		{
			VisualElement view = _renderer.Element;
			AView aview = _renderer.View;
 
			var scale = view.Scale;
 
			if (double.IsNaN(scale))
				return;
 
			aview.ScaleX = (float)scale * (float)view.ScaleX;
			aview.ScaleY = (float)scale * (float)view.ScaleY;
		}
 
		[PortHandler]
		void UpdateTranslationX()
		{
			VisualElement view = _renderer.Element;
			AView aview = _renderer.View;
 
			aview.TranslationX = _context.ToPixels(view.TranslationX);
		}
 
		[PortHandler]
		void UpdateTranslationY()
		{
			VisualElement view = _renderer.Element;
			AView aview = _renderer.View;
 
			aview.TranslationY = _context.ToPixels(view.TranslationY);
		}
 
		void UpdateIsEnabled()
		{
			_renderer.View.Enabled = _renderer.Element.IsEnabled;
		}
 
		class AttachTracker : Object, AView.IOnAttachStateChangeListener
		{
			public static readonly AttachTracker Instance = new AttachTracker();
 
			public void OnViewAttachedToWindow(AView attachedView)
			{
				var renderer = attachedView as IVisualElementRenderer;
				if (renderer == null || renderer.Tracker == null)
					return;
 
				renderer.Tracker.HandleViewAttachedToWindow();
			}
 
			public void OnViewDetachedFromWindow(AView detachedView)
			{
			}
		}
	}
}