|
using System;
using System.Diagnostics.CodeAnalysis;
using CoreAnimation;
using CoreGraphics;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Platform;
using UIKit;
using static Microsoft.Maui.Primitives.Dimension;
namespace Microsoft.Maui.Platform
{
public partial class WrapperView : UIView, IDisposable, IUIViewLifeCycleEvents, ICrossPlatformLayoutBacking
{
bool _fireSetNeedsLayoutOnParentWhenWindowAttached;
WeakReference<ICrossPlatformLayout>? _crossPlatformLayoutReference;
ICrossPlatformLayout? ICrossPlatformLayoutBacking.CrossPlatformLayout
{
get => _crossPlatformLayoutReference != null && _crossPlatformLayoutReference.TryGetTarget(out var v) ? v : null;
set => _crossPlatformLayoutReference = value == null ? null : new WeakReference<ICrossPlatformLayout>(value);
}
internal ICrossPlatformLayout? CrossPlatformLayout
{
get => ((ICrossPlatformLayoutBacking)this).CrossPlatformLayout;
set => ((ICrossPlatformLayoutBacking)this).CrossPlatformLayout = value;
}
double _lastMeasureHeight = double.NaN;
double _lastMeasureWidth = double.NaN;
CAShapeLayer? _maskLayer;
CAShapeLayer? _backgroundMaskLayer;
CAShapeLayer? _shadowLayer;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "_borderView is a SubView")]
UIView? _borderView;
public WrapperView()
{
}
public WrapperView(CGRect frame)
: base(frame)
{
}
internal bool IsMeasureValid(double widthConstraint, double heightConstraint)
{
// Check the last constraints this View was measured with; if they're the same,
// then the current measure info is already correct and we don't need to repeat it
return heightConstraint == _lastMeasureHeight && widthConstraint == _lastMeasureWidth;
}
internal void CacheMeasureConstraints(double widthConstraint, double heightConstraint)
{
_lastMeasureWidth = widthConstraint;
_lastMeasureHeight = heightConstraint;
}
CAShapeLayer? MaskLayer
{
get => _maskLayer;
set
{
var layer = GetLayer();
if (layer is not null && _maskLayer is not null)
layer.Mask = null;
_maskLayer = value;
if (layer is not null)
layer.Mask = value;
}
}
CAShapeLayer? BackgroundMaskLayer
{
get => _backgroundMaskLayer;
set
{
var backgroundLayer = GetBackgroundLayer();
if (backgroundLayer is not null && _backgroundMaskLayer is not null)
backgroundLayer.Mask = null;
_backgroundMaskLayer = value;
if (backgroundLayer is not null)
backgroundLayer.Mask = value;
}
}
CAShapeLayer? ShadowLayer
{
get => _shadowLayer;
set
{
_shadowLayer?.RemoveFromSuperLayer();
_shadowLayer = value;
if (_shadowLayer != null)
Layer.InsertSublayer(_shadowLayer, 0);
}
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
var subviews = Subviews;
if (subviews.Length == 0)
return;
if (_borderView is not null)
BringSubviewToFront(_borderView);
var child = subviews[0];
child.Frame = Bounds;
if (MaskLayer is not null)
MaskLayer.Frame = Bounds;
if (BackgroundMaskLayer is not null)
BackgroundMaskLayer.Frame = Bounds;
if (ShadowLayer is not null)
ShadowLayer.Frame = Bounds;
if (_borderView is not null)
_borderView.Frame = Bounds;
SetClip();
SetShadow();
SetBorder();
var boundWidth = Bounds.Width;
var boundHeight = Bounds.Height;
if (!IsMeasureValid(boundWidth, boundHeight))
{
CrossPlatformLayout?.CrossPlatformMeasure(boundWidth, boundHeight);
CacheMeasureConstraints(boundWidth, boundHeight);
}
CrossPlatformLayout?.CrossPlatformArrange(Bounds.ToRectangle());
}
internal void Disconnect()
{
MaskLayer = null;
BackgroundMaskLayer = null;
ShadowLayer = null;
_borderView?.RemoveFromSuperview();
}
// TODO obsolete or delete this for NET9
public new void Dispose()
{
Disconnect();
base.Dispose();
}
public override CGSize SizeThatFits(CGSize size)
{
var subviews = Subviews;
CGSize returnSize;
if (subviews.Length == 0)
{
returnSize = base.SizeThatFits(size);
}
else
{
var child = subviews[0];
// Calling SizeThatFits on an ImageView always returns the image's dimensions, so we need to call the extension method
// This also affects ImageButtons
if (child is UIImageView imageView)
{
returnSize = imageView.SizeThatFitsImage(size);
}
else if (CrossPlatformLayout is not null)
{
returnSize = CrossPlatformLayout.CrossPlatformMeasure(size.Width, size.Height).ToCGSize();
}
else if (child is UIButton imageButton && imageButton.ImageView?.Image is not null && imageButton.CurrentTitle is null)
{
returnSize = imageButton.ImageView.SizeThatFitsImage(size);
}
else
{
returnSize = child.SizeThatFits(size);
}
}
CacheMeasureConstraints(size.Width, size.Height);
return returnSize;
}
internal CGSize SizeThatFitsWrapper(CGSize originalSpec, double virtualViewWidth, double virtualViewHeight, IView view)
{
var subviews = Subviews;
CGSize returnSize;
var widthConstraint = IsExplicitSet(virtualViewWidth) ? virtualViewWidth : originalSpec.Width;
var heightConstraint = IsExplicitSet(virtualViewHeight) ? virtualViewHeight : originalSpec.Height;
if (subviews.Length == 0)
{
returnSize = base.SizeThatFits(originalSpec);
}
else
{
var child = subviews[0];
if (child is UIImageView || (child is UIButton imageButton && imageButton.ImageView?.Image is not null && imageButton.CurrentTitle is null))
{
if (CrossPlatformLayout is not null)
{
returnSize = CrossPlatformLayout.CrossPlatformMeasure(widthConstraint, heightConstraint);
}
else
{
returnSize = SizeThatFits(new CGSize(widthConstraint, heightConstraint));
}
}
else if (CrossPlatformLayout is not null)
{
returnSize = CrossPlatformLayout.CrossPlatformMeasure(widthConstraint, heightConstraint);
}
else
{
returnSize = SizeThatFits(originalSpec);
}
}
CacheMeasureConstraints(widthConstraint, heightConstraint);
return returnSize;
}
public override void SetNeedsLayout()
{
base.SetNeedsLayout();
TryToInvalidateSuperView(false);
}
private protected void TryToInvalidateSuperView(bool onlyIfPending)
{
if (onlyIfPending && !_fireSetNeedsLayoutOnParentWhenWindowAttached)
{
return;
}
// We check for Window to avoid scenarios where an invalidate might propagate up the tree
// To a SuperView that's been disposed which will cause a crash when trying to access it
if (Window is not null)
{
_fireSetNeedsLayoutOnParentWhenWindowAttached = false;
this.Superview?.SetNeedsLayout();
}
else
{
_fireSetNeedsLayoutOnParentWhenWindowAttached = true;
}
}
partial void ClipChanged()
{
SetClip();
}
partial void ShadowChanged()
{
SetShadow();
}
partial void BorderChanged() => SetBorder();
void SetClip()
{
var mask = MaskLayer;
var backgroundMask = BackgroundMaskLayer;
if (mask is null && Clip is null)
return;
var frame = Frame;
var bounds = new RectF(0, 0, (float)frame.Width, (float)frame.Height);
var path = _clip?.PathForBounds(bounds);
var nativePath = path?.AsCGPath();
mask ??= MaskLayer = new StaticCAShapeLayer();
mask.Path = nativePath;
var backgroundLayer = GetBackgroundLayer();
// We wrap some controls for certain visual effects like applying background gradient etc.
// For this reason, we have to clip the background layer as well if it exists.
if (backgroundLayer is null)
return;
backgroundMask ??= BackgroundMaskLayer = new StaticCAShapeLayer();
backgroundMask.Path = nativePath;
}
void SetShadow()
{
var shadowLayer = ShadowLayer;
if (shadowLayer == null && Shadow == null)
return;
shadowLayer ??= ShadowLayer = new StaticCAShapeLayer();
var frame = Frame;
var bounds = new RectF(0, 0, (float)frame.Width, (float)frame.Height);
shadowLayer.FillColor = new CGColor(0, 0, 0, 1);
var path = _clip?.PathForBounds(bounds);
var nativePath = path?.AsCGPath();
shadowLayer.Path = nativePath;
if (Shadow == null)
shadowLayer.ClearShadow();
else
shadowLayer.SetShadow(Shadow);
}
void SetBorder()
{
if (Border == null)
{
_borderView?.RemoveFromSuperview();
return;
}
if (_borderView is null)
{
AddSubview(_borderView = new UIView(Bounds) { UserInteractionEnabled = false });
}
_borderView.UpdateMauiCALayer(Border);
}
CALayer? GetLayer()
{
var sublayers = Layer?.Sublayers;
if (sublayers is null)
return null;
foreach (var subLayer in sublayers)
if (subLayer.Delegate is not null)
return subLayer;
return Layer;
}
CALayer? GetBackgroundLayer()
{
var sublayers = Layer?.Sublayers;
if (sublayers is null)
return null;
foreach (var subLayer in sublayers)
if (subLayer.Name == ViewExtensions.BackgroundLayerName)
return subLayer;
return Layer;
}
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = IUIViewLifeCycleEvents.UnconditionalSuppressMessage)]
EventHandler? _movedToWindow;
event EventHandler? IUIViewLifeCycleEvents.MovedToWindow
{
add => _movedToWindow += value;
remove => _movedToWindow -= value;
}
public override void MovedToWindow()
{
base.MovedToWindow();
_movedToWindow?.Invoke(this, EventArgs.Empty);
TryToInvalidateSuperView(true);
}
}
} |