Xamarin.Forms и сложности интеграции в Android приложения

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();
        }
    }
}

XamarinFormsAndroid.zip

Опубликовать в Facebook
Опубликовать в Google Plus

4 комментария

  1. Thank you for sharing this info. Your implementation is very elegant!

  2. Классная работа проделана. Жаль, код уже не актуален — часть классов и методов теперь не перекрываемые.

  3. Hi, i’m trying to use this but it doesn’t fully works in XF 2.0, do you have an updated version? Thanks

  4. Да, код, к сожалению, уже не поддерживается. С развитием XForms это становилось сложнее и сложнее.

    К слову, в своем проекте мы отказались от их использования, а в качестве «аналога» в простых случаях на iOS используем XibFree.

    @Alex: Sorry, the code isn’t supported anymore, it was getting more and more complex since XForms evaluation

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *