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