Created
May 15, 2018 08:29
-
-
Save RomaRudyak/0c3d2f96bea5fde4d188020d89243476 to your computer and use it in GitHub Desktop.
PoC for customizing Default Phone master detail page on iOS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using UIKit; | |
using Xamarin.Forms; | |
using Xamarin.Forms.PlatformConfiguration.iOSSpecific; | |
using Xamarin.Forms.Platform.iOS; | |
using Xamarin.Forms.Internals; | |
using PointF = CoreGraphics.CGPoint; | |
using Page = Xamarin.Forms.Page; | |
using NavigationPage = Xamarin.Forms.NavigationPage; | |
using VisualElement = Xamarin.Forms.VisualElement; | |
using System.ComponentModel; | |
using System.Linq; | |
using System.Threading; | |
[assembly: ExportRenderer( | |
typeof(MasterDetailPage), | |
typeof(drawer_test.iOS.Renderer.MyMasterDetailPageRenderer), | |
UIUserInterfaceIdiom.Phone | |
)] | |
namespace drawer_test.iOS.Renderer | |
{ | |
public class MyMasterDetailPageRenderer : UIViewController, IVisualElementRenderer, IEffectControlProvider | |
{ | |
UIView _clickOffView; | |
UIViewController _detailController; | |
bool _disposed; | |
EventTracker _events; | |
UIViewController _masterController; | |
UIPanGestureRecognizer _panGesture; | |
bool _presented; | |
UIGestureRecognizer _tapGesture; | |
VisualElementTracker _tracker; | |
Page Page => Element as Page; | |
public MyMasterDetailPageRenderer() | |
{ | |
} | |
MasterDetailPage MasterDetailPage => Element as MasterDetailPage; | |
bool Presented | |
{ | |
get { return _presented; } | |
set | |
{ | |
if (_presented == value) | |
return; | |
_presented = value; | |
LayoutChildren(true); | |
if (value) | |
AddClickOffView(); | |
else | |
RemoveClickOffView(); | |
((IElementController)Element).SetValueFromRenderer(Xamarin.Forms.MasterDetailPage.IsPresentedProperty, value); | |
} | |
} | |
public VisualElement Element { get; private set; } | |
public event EventHandler<VisualElementChangedEventArgs> ElementChanged; | |
public SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) | |
{ | |
return NativeView.GetSizeRequest(widthConstraint, heightConstraint); | |
} | |
public UIView NativeView | |
{ | |
get { return View; } | |
} | |
public void SetElement(VisualElement element) | |
{ | |
var oldElement = Element; | |
Element = element; | |
Element.SizeChanged += PageOnSizeChanged; | |
_masterController = new ChildViewController(); | |
_detailController = new ChildViewController(); | |
_clickOffView = new UIView(); | |
_clickOffView.BackgroundColor = new Color(0, 0, 0, 0).ToUIColor(); | |
Presented = ((MasterDetailPage)Element).IsPresented; | |
OnElementChanged(new VisualElementChangedEventArgs(oldElement, element)); | |
EffectUtilities.RegisterEffectControlProvider(this, oldElement, element); | |
//if (element != null) | |
//element.SendViewInitialized(NativeView); | |
} | |
public void SetElementSize(Size size) | |
{ | |
Element.Layout(new Rectangle(Element.X, Element.Y, size.Width, size.Height)); | |
} | |
public UIViewController ViewController | |
{ | |
get { return this; } | |
} | |
public override void ViewDidAppear(bool animated) | |
{ | |
base.ViewDidAppear(animated); | |
Page.SendAppearing(); | |
} | |
public override void ViewDidDisappear(bool animated) | |
{ | |
base.ViewDidDisappear(animated); | |
Page?.SendDisappearing(); | |
} | |
public override void ViewDidLayoutSubviews() | |
{ | |
base.ViewDidLayoutSubviews(); | |
LayoutChildren(false); | |
} | |
public override void ViewDidLoad() | |
{ | |
base.ViewDidLoad(); | |
_tracker = new VisualElementTracker(this); | |
_events = new EventTracker(this); | |
_events.LoadEvents(View); | |
((MasterDetailPage)Element).PropertyChanged += HandlePropertyChanged; | |
_tapGesture = new UITapGestureRecognizer(() => | |
{ | |
if (Presented) | |
Presented = false; | |
}); | |
_clickOffView.AddGestureRecognizer(_tapGesture); | |
PackContainers(); | |
UpdateMasterDetailContainers(); | |
UpdateBackground(); | |
UpdatePanGesture(); | |
} | |
public override void WillRotate(UIInterfaceOrientation toInterfaceOrientation, double duration) | |
{ | |
if (!MasterDetailPage.ShouldShowSplitMode && _presented) | |
Presented = false; | |
base.WillRotate(toInterfaceOrientation, duration); | |
} | |
protected override void Dispose(bool disposing) | |
{ | |
if (disposing && !_disposed) | |
{ | |
Element.SizeChanged -= PageOnSizeChanged; | |
Element.PropertyChanged -= HandlePropertyChanged; | |
if (_tracker != null) | |
{ | |
_tracker.Dispose(); | |
_tracker = null; | |
} | |
if (_events != null) | |
{ | |
_events.Dispose(); | |
_events = null; | |
} | |
if (_tapGesture != null) | |
{ | |
if (_clickOffView != null && _clickOffView.GestureRecognizers.Contains(_tapGesture)) | |
{ | |
_clickOffView.RemoveGestureRecognizer(_tapGesture); | |
_clickOffView.Dispose(); | |
} | |
_tapGesture.Dispose(); | |
} | |
if (_panGesture != null) | |
{ | |
if (View != null && View.GestureRecognizers.Contains(_panGesture)) | |
View.RemoveGestureRecognizer(_panGesture); | |
_panGesture.Dispose(); | |
} | |
EmptyContainers(); | |
Page.SendDisappearing(); | |
_disposed = true; | |
} | |
base.Dispose(disposing); | |
} | |
protected virtual void OnElementChanged(VisualElementChangedEventArgs e) | |
{ | |
var changed = ElementChanged; | |
if (changed != null) | |
changed(this, e); | |
} | |
void AddClickOffView() | |
{ | |
View.Add(_clickOffView); | |
_clickOffView.Frame = _detailController.View.Frame; | |
} | |
void EmptyContainers() | |
{ | |
foreach (var child in _detailController.View.Subviews.Concat(_masterController.View.Subviews)) | |
child.RemoveFromSuperview(); | |
foreach (var vc in _detailController.ChildViewControllers.Concat(_masterController.ChildViewControllers)) | |
vc.RemoveFromParentViewController(); | |
} | |
void HandleMasterPropertyChanged(object sender, PropertyChangedEventArgs e) | |
{ | |
if (e.PropertyName == Page.IconProperty.PropertyName || e.PropertyName == Page.TitleProperty.PropertyName) | |
UpdateLeftBarButton(); | |
} | |
void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) | |
{ | |
if (e.PropertyName == "Master" || e.PropertyName == "Detail") | |
UpdateMasterDetailContainers(); | |
else if (e.PropertyName == Xamarin.Forms.MasterDetailPage.IsPresentedProperty.PropertyName) | |
Presented = ((MasterDetailPage)Element).IsPresented; | |
else if (e.PropertyName == Xamarin.Forms.MasterDetailPage.IsGestureEnabledProperty.PropertyName) | |
UpdatePanGesture(); | |
else if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) | |
UpdateBackground(); | |
else if (e.PropertyName == Page.BackgroundImageProperty.PropertyName) | |
UpdateBackground(); | |
} | |
void LayoutChildren(bool animated) | |
{ | |
var frame = Element.Bounds.ToRectangleF(); | |
var masterFrame = frame; | |
masterFrame.Width = (int)(Math.Min(masterFrame.Width, masterFrame.Height) * 0.8); | |
var isRTL = (Element as IVisualElementController)?.EffectiveFlowDirection.IsRightToLeft() == true; | |
if (isRTL) | |
{ | |
masterFrame.X = (int)(masterFrame.Width * .25); | |
} | |
_detailController.View.Frame = masterFrame; | |
var target = frame; | |
if (Presented) | |
target.X += masterFrame.Width; | |
if (isRTL) | |
{ | |
target.X = target.X * -1; | |
} | |
if (animated) | |
{ | |
UIView.BeginAnimations("Flyout"); | |
var view = _masterController.View; | |
view.Frame = target; | |
UIView.SetAnimationCurve(UIViewAnimationCurve.EaseOut); | |
UIView.SetAnimationDuration(250); | |
UIView.CommitAnimations(); | |
} | |
else | |
_masterController.View.Frame = target; | |
MasterDetailPage.DetailBounds = new Rectangle(masterFrame.X, 0, masterFrame.Width, masterFrame.Height); | |
MasterDetailPage.MasterBounds = new Rectangle(0, 0, frame.Width, frame.Height); | |
if (Presented) | |
_clickOffView.Frame = _detailController.View.Frame; | |
} | |
void PackContainers() | |
{ | |
var f = _masterController.View.Frame; | |
_masterController.View.Frame = new Rectangle(f.X + f.Width, f.Y, f.Width, f.Height).ToRectangleF(); | |
_detailController.View.BackgroundColor = new UIColor(1, 1, 1, 1); | |
View.AddSubview(_detailController.View); | |
View.AddSubview(_masterController.View); | |
AddChildViewController(_detailController); | |
AddChildViewController(_masterController); | |
} | |
void PageOnSizeChanged(object sender, EventArgs eventArgs) | |
{ | |
LayoutChildren(false); | |
} | |
void RemoveClickOffView() | |
{ | |
_clickOffView.RemoveFromSuperview(); | |
} | |
void UpdateBackground() | |
{ | |
if (!string.IsNullOrEmpty(((Page)Element).BackgroundImage)) | |
View.BackgroundColor = UIColor.FromPatternImage(UIImage.FromBundle(((Page)Element).BackgroundImage)); | |
else if (Element.BackgroundColor == Color.Default) | |
View.BackgroundColor = UIColor.White; | |
else | |
View.BackgroundColor = Element.BackgroundColor.ToUIColor(); | |
} | |
void UpdateMasterDetailContainers() | |
{ | |
((MasterDetailPage)Element).Master.PropertyChanged -= HandleMasterPropertyChanged; | |
EmptyContainers(); | |
if (Platform.GetRenderer(((MasterDetailPage)Element).Master) == null) | |
Platform.SetRenderer(((MasterDetailPage)Element).Master, Platform.CreateRenderer(((MasterDetailPage)Element).Master)); | |
if (Platform.GetRenderer(((MasterDetailPage)Element).Detail) == null) | |
Platform.SetRenderer(((MasterDetailPage)Element).Detail, Platform.CreateRenderer(((MasterDetailPage)Element).Detail)); | |
var masterRenderer = Platform.GetRenderer(((MasterDetailPage)Element).Master); | |
var detailRenderer = Platform.GetRenderer(((MasterDetailPage)Element).Detail); | |
((MasterDetailPage)Element).Master.PropertyChanged += HandleMasterPropertyChanged; | |
_masterController.View.AddSubview(masterRenderer.NativeView); | |
_masterController.AddChildViewController(masterRenderer.ViewController); | |
_detailController.View.AddSubview(detailRenderer.NativeView); | |
_detailController.AddChildViewController(detailRenderer.ViewController); | |
SetNeedsStatusBarAppearanceUpdate(); | |
} | |
void UpdateLeftBarButton() | |
{ | |
var masterDetailPage = Element as MasterDetailPage; | |
if (!(masterDetailPage?.Detail is NavigationPage)) | |
return; | |
var detailRenderer = Platform.GetRenderer(masterDetailPage.Detail) as UINavigationController; | |
UIViewController firstPage = detailRenderer?.ViewControllers.FirstOrDefault(); | |
if (firstPage != null) | |
SetMasterLeftBarButton(firstPage, masterDetailPage); | |
} | |
public override UIViewController ChildViewControllerForStatusBarHidden() | |
{ | |
if (((MasterDetailPage)Element).Detail != null) | |
return (UIViewController)Platform.GetRenderer(((MasterDetailPage)Element).Detail); | |
else | |
return base.ChildViewControllerForStatusBarHidden(); | |
} | |
void UpdatePanGesture() | |
{ | |
var model = (MasterDetailPage)Element; | |
if (!model.IsGestureEnabled) | |
{ | |
if (_panGesture != null) | |
View.RemoveGestureRecognizer(_panGesture); | |
return; | |
} | |
if (_panGesture != null) | |
{ | |
View.AddGestureRecognizer(_panGesture); | |
return; | |
} | |
UITouchEventArgs shouldReceive = (g, t) => !(t.View is UISlider); | |
var center = new PointF(); | |
_panGesture = new UIPanGestureRecognizer(g => | |
{ | |
var isRTL = (Element as IVisualElementController)?.EffectiveFlowDirection.IsRightToLeft() == true; | |
int directionModifier = isRTL ? -1 : 1; | |
switch (g.State) | |
{ | |
case UIGestureRecognizerState.Began: | |
center = g.LocationInView(g.View); | |
break; | |
case UIGestureRecognizerState.Changed: | |
var currentPosition = g.LocationInView(g.View); | |
var motion = currentPosition.X - center.X; | |
motion = motion * directionModifier; | |
var detailView = _detailController.View; | |
var targetFrame = detailView.Frame; | |
if (Presented) | |
targetFrame.X = (nfloat)Math.Max(0, _masterController.View.Frame.Width + Math.Min(0, motion)); | |
else | |
targetFrame.X = (nfloat)Math.Min(_masterController.View.Frame.Width, Math.Max(0, motion)); | |
targetFrame.X = targetFrame.X * directionModifier; | |
detailView.Frame = targetFrame; | |
break; | |
case UIGestureRecognizerState.Ended: | |
var detailFrame = _detailController.View.Frame; | |
var masterFrame = _masterController.View.Frame; | |
if (Presented) | |
{ | |
if (detailFrame.X * directionModifier < masterFrame.Width * .75) | |
Presented = false; | |
else | |
LayoutChildren(true); | |
} | |
else | |
{ | |
if (detailFrame.X * directionModifier > masterFrame.Width * .25) | |
Presented = true; | |
else | |
LayoutChildren(true); | |
} | |
break; | |
} | |
}); | |
_panGesture.ShouldReceiveTouch = shouldReceive; | |
_panGesture.MaximumNumberOfTouches = 2; | |
View.AddGestureRecognizer(_panGesture); | |
} | |
internal static async void SetMasterLeftBarButton(UIViewController containerController, MasterDetailPage masterDetailPage) | |
{ | |
if (!masterDetailPage.ShouldShowToolbarButton()) | |
{ | |
containerController.NavigationItem.LeftBarButtonItem = null; | |
} | |
else | |
{ | |
EventHandler handler = delegate | |
{ | |
masterDetailPage.IsPresented = !masterDetailPage.IsPresented; | |
}; | |
bool shouldUseIcon = masterDetailPage.Master.Icon != null; | |
if (shouldUseIcon) | |
{ | |
try | |
{ | |
UIImage image = await Xamarin.Forms.Internals.Registrar | |
.Registered | |
.GetHandlerForObject<IImageSourceHandler>( | |
masterDetailPage.Master.Icon) | |
.LoadImageAsync( | |
masterDetailPage.Master.Icon, | |
default(CancellationToken), | |
1f); | |
containerController.NavigationItem.LeftBarButtonItem = new UIBarButtonItem(image, UIBarButtonItemStyle.Plain, handler); | |
} | |
catch (Exception) | |
{ | |
shouldUseIcon = false; | |
} | |
} | |
if (!shouldUseIcon) | |
{ | |
containerController.NavigationItem.LeftBarButtonItem = new UIBarButtonItem(masterDetailPage.Master.Title, UIBarButtonItemStyle.Plain, handler); | |
} | |
} | |
} | |
class ChildViewController : UIViewController | |
{ | |
public override void ViewDidLayoutSubviews() | |
{ | |
foreach (var vc in ChildViewControllers) | |
vc.View.Frame = View.Bounds; | |
} | |
} | |
void IEffectControlProvider.RegisterEffect(Effect effect) | |
{ | |
VisualElementRenderer<VisualElement>.RegisterEffect(effect, View); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment