UPDATE: код обновлен для работы с Xamarin.Forms 1.3.4+
Говоря о недостатках Xamarin.Forms в предыдущей заметке, я среди прочего упомянул и сложность интеграции в существующие Android-проекты, а именно трудности так называемого «частичного использования» Xamarin.Forms в андроид-приложениях.
«Частичное использование», с моей точки зрения, это возможность с легкостью взять и добавить технологию в уже существующий проект, и реализовать с её помощью функционал какой-то части приложения. В случае Xamarin.Forms — реализовать какую-нибудь часть экрана/компонент с использованием этой UI-библиотеки (без полного переписывания приложения на новой технологии).
Интеграция в iOS, как я писал в предыдущей заметке, проходит достаточно безболезненно (за исключением проблемы с утечкой памяти, о решении которой я так же написал).
Интеграция в Андроид усложняется тем, что метода, аналогичного iOS’овскому .CreateViewController() здесь не существует.
Xamarin предлагает отнаследоваться от AndroidActivity и использовать метод .SetPage для установки текущей страницы. В стандартном случае, когда приложение целиком написано на Xamarin.Forms это может быть довольно удобно. Но для интеграции в существующий не-Forms проект, чтобы реализовать на Xamarin.Forms лишь одну отдельную View — это довольно-таки неудобно.
Однако — нет ничего неразрешимого. Собрав в кучу все возможные инструменты reverse-ingeneering’a и скопипастив оттуда пару-тройку классов, я собрал небольшой хэлпер-метод, с помощью которого можно легко использовать Forms’овскую Page внутри любого Fragment‘а или Activity:
public static Android.Views.View CreateView(this Page page, Context activity)
К слову, проблемы с утечками памяти в Андроид-версии Xamarin.Forms тоже были, и они были успешно решены. К сожалению, широко рекламируемый в последнее время Профайлер от Xamarin пока что показывает себя намного хуже даже старого доброго HeapShot. Мало того, под шумок HeapShot тоже сломали, и дампы памяти из последних версий Xamarin’a он просто отказывается анализировать (спасает лишь Mac-версия Xamarin.Profiler’a, которая показывает хоть что-то).
К сожалению, такое поведение довольно характерно для ранних стадий продуктов от команды Моно (справедливости ради, Xamarin.Profiler официально до сих пор в альфа-версии).
Переходя к собственно реализации .CreateView — ниже будет полный текст файла, который можно просто включить в свой проект и использовать. А в приложении — пример проекта с использованием подобного расширения.
Во избежание крэшей/утечек памяти при разрушении контейнера, в котором располагается страница (Activity.OnDestroy/Fragment.OnDestroy) нужно не забывать вызывать Dispose() у вьюшки, полученной методом .CreateView(Page page, Context activity).
Удачного использования Xamarin.Forms!
using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Android.App; using Android.Content; using Android.Views; using Android.Widget; using System; using Xamarin.Forms; using Xamarin.Forms.Platform.Android; namespace XamarinFormsHelper { public static class XamarinFormsExtensions { public static Android.Views.View CreateView(this Page page, Context activity) { if (!Forms.IsInitialized) throw new InvalidOperationException("Call Forms.Init (Activity, Bundle) before this"); var platform = new Platform(Forms.Context); platform.SetPage(page); return platform.View; } public static void DisposePage(this Page page) { RemovePageFromMessagingCenter(page); page.OnDescendantRemoved(); } private static MethodInfo _onDescendantRemovedMethod = typeof(Element).GetMethod("OnDescendantRemoved", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); private static void OnDescendantRemoved(this Page page) { _onDescendantRemovedMethod.Invoke(page, new[] { page }); } private static FieldInfo _callbacksField = typeof(MessagingCenter).GetField("callbacks", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); private static Dictionary<Tuple<string, Type, Type>, List<Tuple<WeakReference, Action<object, object>>>> Callbacks { get { return (Dictionary<Tuple<string, Type, Type>, List<Tuple<WeakReference, Action<object, object>>>>)_callbacksField.GetValue(null); } } private static PropertyInfo _platformProperty = typeof(Element).GetProperty("Platform", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); private static IPlatform GetPlatform(this Page page) { return (IPlatform)_platformProperty.GetValue(page); } private static void RemovePageFromMessagingCenter(Page page) { var platform = page.GetPlatform(); if (platform == null) return; foreach (var subscriptions in Callbacks.Values) { subscriptions.RemoveAll(x => x.Item1.IsAlive && x.Item1.Target == platform); } } } internal static class MeasureSpecFactory { public static int MakeMeasureSpec(int size, MeasureSpecMode mode) { return (int)(size + mode); } public static int GetSize(int measureSpec) { return measureSpec & 1073741823; } } internal class PlatformRenderer : ViewGroup { private Platform canvas; private DateTime downTime; private Point downPosition; public PlatformRenderer(Context context, Platform canvas) : base(context) { this.canvas = canvas; this.Focusable = true; this.FocusableInTouchMode = true; } protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) { this.SetMeasuredDimension(MeasureSpec.GetSize(widthMeasureSpec), MeasureSpec.GetSize(heightMeasureSpec)); } protected override void OnLayout(bool changed, int l, int t, int r, int b) { this.SetMeasuredDimension(r - l, b - t); this.canvas.OnLayout(changed, l, t, r, b); } public override bool DispatchTouchEvent(MotionEvent e) { if (e.Action == MotionEventActions.Down) { this.downTime = DateTime.UtcNow; this.downPosition = new Point((double)e.RawX, (double)e.RawY); } if (e.Action != MotionEventActions.Up) return base.DispatchTouchEvent(e); var currentFocus1 = ((Activity)this.Context).CurrentFocus; bool flag = base.DispatchTouchEvent(e); if (currentFocus1 is EditText) { var currentFocus2 = ((Activity)this.Context).CurrentFocus; if (currentFocus1 == currentFocus2 && this.downPosition.Distance(new Point((double)e.RawX, (double)e.RawY)) <= (double)ContextExtensions.ToPixels(this.Context, 20.0) && !(DateTime.UtcNow - this.downTime > TimeSpan.FromMilliseconds(200.0))) { int[] location = new int[2]; currentFocus1.GetLocationOnScreen(location); float num1 = e.RawX + (float)currentFocus1.Left - (float)location[0]; float num2 = e.RawY + (float)currentFocus1.Top - (float)location[1]; if (!new Rectangle((double)currentFocus1.Left, (double)currentFocus1.Top, (double)currentFocus1.Width, (double)currentFocus1.Height).Contains((double)num1, (double)num2)) { ContextExtensions.HideKeyboard(this.Context, currentFocus1); this.RequestFocus(); } } } return flag; } protected override void Dispose(bool disposing) { base.Dispose(disposing); RemoveAllViews(); Platform.GetRenderer(canvas.Page).Dispose(); canvas.Page.DisposePage(); } } public class Platform : BindableObject, IPlatform, IPlatformEngine, INavigation { private static Type _platformType = Type.GetType("Xamarin.Forms.Platform.Android.Platform, Xamarin.Forms.Platform.Android", true); private static BindableProperty _rendererProperty; public static BindableProperty RendererProperty { get { return _rendererProperty ?? (_rendererProperty = (BindableProperty)_platformType.GetField("RendererProperty", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public).GetValue(null)); } } private static BindableProperty _pageContextProperty; public static BindableProperty PageContextProperty { get { return _pageContextProperty ?? (_pageContextProperty = (BindableProperty)_platformType.GetField("PageContextProperty", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public).GetValue(null)); } } //public static readonly BindableProperty PageContextProperty = BindableProperty.CreateAttached("PageContext", typeof(Context), typeof(Platform), (object)null, BindingMode.OneWay, (BindableProperty.ValidateValueDelegate)null, (BindableProperty.BindingPropertyChangedDelegate)null, (BindableProperty.BindingPropertyChangingDelegate)null, (BindableProperty.CoerceValueDelegate)null); private List<Page> Roots = new List<Page>(); //private NavigationModel navModel = new NavigationModel(); private readonly Context context; private readonly PlatformRenderer renderer; private bool popping; private NavigationPage currentNavigationPage; private Page navigationPageCurrentPage; private TabbedPage currentTabbedPage; private Xamarin.Forms.Color defaultActionBarTitleTextColor; private bool ignoreAndroidSelection; #region IPlatform public IPlatformEngine Engine { get { return (IPlatformEngine)this; } } public Page Page { get; private set; } public void SetPage(Page newRoot) { if (newRoot == null) return; if (this.Page != null) { this.renderer.RemoveAllViews(); foreach (IDisposable disposable in Enumerable.Select<Page, IVisualElementRenderer>(Roots, new Func<Page, IVisualElementRenderer>(Platform.GetRenderer))) disposable.Dispose(); //this.navModel = new NavigationModel(); } Roots.Add(newRoot); //this.navModel.Push(newRoot, (Page) null); this.Page = newRoot; this.AddChild((VisualElement)this.Page); //this.Page.Platform = (IPlatform) this; var platformProperty = typeof(Page).GetProperty("Platform", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); platformProperty.SetValue(Page, this); // this.Page.NavigationProxy.Inner = (INavigation) this; } #endregion #region IPlatformEngine public bool Supports3D { get { return true; } } public SizeRequest GetNativeSize(VisualElement view, double widthConstraint, double heightConstraint) { IVisualElementRenderer renderer = Platform.GetRenderer((BindableObject)view); widthConstraint = widthConstraint <= -1.0 ? double.PositiveInfinity : (double)ContextExtensions.ToPixels(this.context, widthConstraint); heightConstraint = heightConstraint <= -1.0 ? double.PositiveInfinity : (double)ContextExtensions.ToPixels(this.context, heightConstraint); int widthConstraint1 = !double.IsPositiveInfinity(widthConstraint) ? MeasureSpecFactory.MakeMeasureSpec((int)widthConstraint, MeasureSpecMode.AtMost) : MeasureSpecFactory.MakeMeasureSpec(0, MeasureSpecMode.Unspecified); int heightConstraint1 = !double.IsPositiveInfinity(heightConstraint) ? MeasureSpecFactory.MakeMeasureSpec((int)heightConstraint, MeasureSpecMode.AtMost) : MeasureSpecFactory.MakeMeasureSpec(0, MeasureSpecMode.Unspecified); SizeRequest desiredSize = renderer.GetDesiredSize(widthConstraint1, heightConstraint1); if (desiredSize.Minimum == Size.Zero) desiredSize.Minimum = desiredSize.Request; return new SizeRequest(new Size(ContextExtensions.FromPixels(this.context, desiredSize.Request.Width), ContextExtensions.FromPixels(this.context, desiredSize.Request.Height)), new Size(ContextExtensions.FromPixels(this.context, desiredSize.Minimum.Width), ContextExtensions.FromPixels(this.context, desiredSize.Minimum.Height))); } #endregion #region INavigation public IReadOnlyList<Page> NavigationStack { get; private set; } public IReadOnlyList<Page> ModalStack { get; private set; } public void RemovePage(Page page) { throw new NotImplementedException(); } public void InsertPageBefore(Page page, Page before) { throw new NotImplementedException(); } public Task PushAsync(Page page) { throw new InvalidOperationException("PushAsync is not supported globally on Android, please use a NavigationPage."); } public Task<Page> PopAsync() { throw new InvalidOperationException("PopAsync is not supported globally on Android, please use a NavigationPage."); } public Task PopToRootAsync() { throw new InvalidOperationException("PopToRootAsync is not supported globally on Android, please use a NavigationPage."); } public Task PushModalAsync(Page page) { return null; } public Task<Page> PopModalAsync() { return null; } public Task PushAsync(Page page, bool animated) { throw new NotImplementedException(); } public Task<Page> PopAsync(bool animated) { throw new NotImplementedException(); } public Task PopToRootAsync(bool animated) { throw new NotImplementedException(); } public Task PushModalAsync(Page page, bool animated) { throw new NotImplementedException(); } public Task<Page> PopModalAsync(bool animated) { throw new NotImplementedException(); } #endregion public Platform(Context context) { this.context = context; renderer = new PlatformRenderer(context, this); } public ViewGroup View { get { return (ViewGroup)renderer; } } public static implicit operator ViewGroup(Platform canvas) { return (ViewGroup)canvas.renderer; } public static Context GetPageContext(BindableObject bindable) { return (Context)bindable.GetValue(Platform.PageContextProperty); } public static void SetPageContext(BindableObject bindable, Context context) { bindable.SetValue(Platform.PageContextProperty, (object)context); } public static IVisualElementRenderer GetRenderer(BindableObject bindable) { return (IVisualElementRenderer)bindable.GetValue(Platform.RendererProperty); } public static void SetRenderer(BindableObject bindable, IVisualElementRenderer value) { bindable.SetValue(Platform.RendererProperty, (object)value); } private void AddChild(VisualElement view) { if (Platform.GetRenderer((BindableObject)view) != null) return; Platform.SetPageContext((BindableObject)view, this.context); IVisualElementRenderer renderer = RendererFactory.GetRenderer(view); Platform.SetRenderer((BindableObject)view, renderer); this.renderer.AddView(renderer.ViewGroup); } internal void OnLayout(bool changed, int l, int t, int r, int b) { if (changed) { foreach (VisualElement visualElement in Roots) visualElement.Layout(new Rectangle(0.0, 0.0, ContextExtensions.FromPixels(this.context, (double)(r - l)), ContextExtensions.FromPixels(this.context, (double)(b - t)))); } foreach (IVisualElementRenderer visualElementRenderer in Enumerable.Select<Page, IVisualElementRenderer>(Roots, new Func<Page, IVisualElementRenderer>(Platform.GetRenderer))) visualElementRenderer.UpdateLayout(); } } }
Thank you for sharing this info. Your implementation is very elegant!
Классная работа проделана. Жаль, код уже не актуален — часть классов и методов теперь не перекрываемые.
Hi, i’m trying to use this but it doesn’t fully works in XF 2.0, do you have an updated version? Thanks
Да, код, к сожалению, уже не поддерживается. С развитием XForms это становилось сложнее и сложнее.
К слову, в своем проекте мы отказались от их использования, а в качестве «аналога» в простых случаях на iOS используем XibFree.
@Alex: Sorry, the code isn’t supported anymore, it was getting more and more complex since XForms evaluation