Пишем код

Заметки о .net разработке

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

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

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:

<br />
public static Android.Views.View CreateView(this Page page, Context activity)<br />

К слову, проблемы с утечками памяти в Андроид-версии 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!

<br />
using System.Collections.Generic;<br />
using System.Linq;<br />
using System.Reflection;<br />
using System.Threading.Tasks;<br />
using Android.App;<br />
using Android.Content;<br />
using Android.Views;<br />
using Android.Widget;<br />
using System;<br />
using Xamarin.Forms;<br />
using Xamarin.Forms.Platform.Android;<br />
namespace XamarinFormsHelper<br />
{<br />
    public static class XamarinFormsExtensions<br />
    {<br />
        public static Android.Views.View CreateView(this Page page, Context activity)<br />
        {<br />
            if (!Forms.IsInitialized)<br />
                throw new InvalidOperationException("Call Forms.Init (Activity, Bundle) before this");</p>
<p>            var platform = new Platform(Forms.Context);<br />
            platform.SetPage(page);</p>
<p>            return platform.View;</p>
<p>        }</p>
<p>        public static void DisposePage(this Page page)<br />
        {<br />
            RemovePageFromMessagingCenter(page);</p>
<p>            page.OnDescendantRemoved();<br />
        }</p>
<p>        private static MethodInfo _onDescendantRemovedMethod = typeof(Element).GetMethod("OnDescendantRemoved", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);<br />
        private static void OnDescendantRemoved(this Page page)<br />
        {<br />
            _onDescendantRemovedMethod.Invoke(page, new[] { page });<br />
        }</p>
<p>        private static FieldInfo _callbacksField = typeof(MessagingCenter).GetField("callbacks", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);<br />
        private static Dictionary<Tuple<string, Type, Type>, List<Tuple<WeakReference, Action<object, object>>>> Callbacks<br />
        {<br />
            get<br />
            {<br />
                return (Dictionary<Tuple<string, Type, Type>, List<Tuple<WeakReference, Action<object, object>>>>)_callbacksField.GetValue(null);<br />
            }<br />
        }</p>
<p>        private static PropertyInfo _platformProperty = typeof(Element).GetProperty("Platform", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);<br />
        private static IPlatform GetPlatform(this Page page)<br />
        {<br />
            return (IPlatform)_platformProperty.GetValue(page);<br />
        }</p>
<p>        private static void RemovePageFromMessagingCenter(Page page)<br />
        {<br />
            var platform = page.GetPlatform();</p>
<p>            if (platform == null)<br />
                return;</p>
<p>            foreach (var subscriptions in Callbacks.Values)<br />
            {<br />
                subscriptions.RemoveAll(x => x.Item1.IsAlive && x.Item1.Target == platform);<br />
            }<br />
        }<br />
    }</p>
<p>    internal static class MeasureSpecFactory<br />
    {<br />
        public static int MakeMeasureSpec(int size, MeasureSpecMode mode)<br />
        {<br />
            return (int)(size + mode);<br />
        }</p>
<p>        public static int GetSize(int measureSpec)<br />
        {<br />
            return measureSpec & 1073741823;<br />
        }<br />
    }<br />
    internal class PlatformRenderer : ViewGroup<br />
    {<br />
        private Platform canvas;<br />
        private DateTime downTime;<br />
        private Point downPosition;</p>
<p>        public PlatformRenderer(Context context, Platform canvas)<br />
            : base(context)<br />
        {<br />
            this.canvas = canvas;<br />
            this.Focusable = true;<br />
            this.FocusableInTouchMode = true;<br />
        }</p>
<p>        protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)<br />
        {<br />
            this.SetMeasuredDimension(MeasureSpec.GetSize(widthMeasureSpec), MeasureSpec.GetSize(heightMeasureSpec));<br />
        }</p>
<p>        protected override void OnLayout(bool changed, int l, int t, int r, int b)<br />
        {<br />
            this.SetMeasuredDimension(r - l, b - t);<br />
            this.canvas.OnLayout(changed, l, t, r, b);<br />
        }</p>
<p>        public override bool DispatchTouchEvent(MotionEvent e)<br />
        {<br />
            if (e.Action == MotionEventActions.Down)<br />
            {<br />
                this.downTime = DateTime.UtcNow;<br />
                this.downPosition = new Point((double)e.RawX, (double)e.RawY);<br />
            }<br />
            if (e.Action != MotionEventActions.Up)<br />
                return base.DispatchTouchEvent(e);<br />
            var currentFocus1 = ((Activity)this.Context).CurrentFocus;<br />
            bool flag = base.DispatchTouchEvent(e);<br />
            if (currentFocus1 is EditText)<br />
            {<br />
                var currentFocus2 = ((Activity)this.Context).CurrentFocus;<br />
                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)))<br />
                {<br />
                    int[] location = new int[2];<br />
                    currentFocus1.GetLocationOnScreen(location);<br />
                    float num1 = e.RawX + (float)currentFocus1.Left - (float)location[0];<br />
                    float num2 = e.RawY + (float)currentFocus1.Top - (float)location[1];<br />
                    if (!new Rectangle((double)currentFocus1.Left, (double)currentFocus1.Top, (double)currentFocus1.Width, (double)currentFocus1.Height).Contains((double)num1, (double)num2))<br />
                    {<br />
                        ContextExtensions.HideKeyboard(this.Context, currentFocus1);<br />
                        this.RequestFocus();<br />
                    }<br />
                }<br />
            }<br />
            return flag;<br />
        }</p>
<p>        protected override void Dispose(bool disposing)<br />
        {<br />
            base.Dispose(disposing);</p>
<p>            RemoveAllViews();<br />
            Platform.GetRenderer(canvas.Page).Dispose();<br />
            canvas.Page.DisposePage();<br />
        }<br />
    }</p>
<p>    public class Platform : BindableObject, IPlatform, IPlatformEngine, INavigation<br />
    {<br />
        private static Type _platformType = Type.GetType("Xamarin.Forms.Platform.Android.Platform, Xamarin.Forms.Platform.Android", true);<br />
        private static BindableProperty _rendererProperty;<br />
        public static BindableProperty RendererProperty<br />
        {<br />
            get { return _rendererProperty ?? (_rendererProperty = (BindableProperty)_platformType.GetField("RendererProperty", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public).GetValue(null)); }<br />
        }<br />
        private static BindableProperty _pageContextProperty;<br />
        public static BindableProperty PageContextProperty<br />
        {<br />
            get { return _pageContextProperty ?? (_pageContextProperty = (BindableProperty)_platformType.GetField("PageContextProperty", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public).GetValue(null)); }<br />
        }<br />
        //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);<br />
        private List<Page> Roots = new List<Page>();<br />
        //private NavigationModel navModel = new NavigationModel();<br />
        private readonly Context context;<br />
        private readonly PlatformRenderer renderer;<br />
        private bool popping;<br />
        private NavigationPage currentNavigationPage;<br />
        private Page navigationPageCurrentPage;<br />
        private TabbedPage currentTabbedPage;<br />
        private Xamarin.Forms.Color defaultActionBarTitleTextColor;<br />
        private bool ignoreAndroidSelection;</p>
<p>        #region IPlatform</p>
<p>        public IPlatformEngine Engine<br />
        {<br />
            get<br />
            {<br />
                return (IPlatformEngine)this;<br />
            }<br />
        }</p>
<p>        public Page Page { get; private set; }</p>
<p>        public void SetPage(Page newRoot)<br />
        {<br />
            if (newRoot == null)<br />
                return;<br />
            if (this.Page != null)<br />
            {<br />
                this.renderer.RemoveAllViews();<br />
                foreach (IDisposable disposable in Enumerable.Select<Page, IVisualElementRenderer>(Roots, new Func<Page, IVisualElementRenderer>(Platform.GetRenderer)))<br />
                    disposable.Dispose();<br />
                //this.navModel = new NavigationModel();<br />
            }<br />
            Roots.Add(newRoot);<br />
            //this.navModel.Push(newRoot, (Page) null);<br />
            this.Page = newRoot;<br />
            this.AddChild((VisualElement)this.Page);<br />
            //this.Page.Platform = (IPlatform) this;<br />
            var platformProperty = typeof(Page).GetProperty("Platform", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);<br />
            platformProperty.SetValue(Page, this);</p>
<p>            // this.Page.NavigationProxy.Inner = (INavigation) this;<br />
        }</p>
<p>        #endregion</p>
<p>        #region IPlatformEngine</p>
<p>        public bool Supports3D<br />
        {<br />
            get<br />
            {<br />
                return true;<br />
            }<br />
        }</p>
<p>        public SizeRequest GetNativeSize(VisualElement view, double widthConstraint, double heightConstraint)<br />
        {<br />
            IVisualElementRenderer renderer = Platform.GetRenderer((BindableObject)view);<br />
            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; }</p>
<p>        public IReadOnlyList<Page> ModalStack { get; private set; }</p>
<p>        public void RemovePage(Page page)<br />
        {<br />
            throw new NotImplementedException();<br />
        }</p>
<p>        public void InsertPageBefore(Page page, Page before)<br />
        {<br />
            throw new NotImplementedException();<br />
        }</p>
<p>        public Task PushAsync(Page page)<br />
        {<br />
            throw new InvalidOperationException("PushAsync is not supported globally on Android, please use a NavigationPage.");<br />
        }</p>
<p>        public Task<Page> PopAsync()<br />
        {<br />
            throw new InvalidOperationException("PopAsync is not supported globally on Android, please use a NavigationPage.");<br />
        }</p>
<p>        public Task PopToRootAsync()<br />
        {<br />
            throw new InvalidOperationException("PopToRootAsync is not supported globally on Android, please use a NavigationPage.");<br />
        }</p>
<p>        public Task PushModalAsync(Page page)<br />
        {<br />
            return null;<br />
        }</p>
<p>        public Task<Page> PopModalAsync()<br />
        {<br />
            return null;<br />
        }</p>
<p>        public Task PushAsync(Page page, bool animated)<br />
        {<br />
            throw new NotImplementedException();<br />
        }</p>
<p>        public Task<Page> PopAsync(bool animated)<br />
        {<br />
            throw new NotImplementedException();<br />
        }</p>
<p>        public Task PopToRootAsync(bool animated)<br />
        {<br />
            throw new NotImplementedException();<br />
        }</p>
<p>        public Task PushModalAsync(Page page, bool animated)<br />
        {<br />
            throw new NotImplementedException();<br />
        }</p>
<p>        public Task<Page> PopModalAsync(bool animated)<br />
        {<br />
            throw new NotImplementedException();<br />
        }</p>
<p>        #endregion</p>
<p>        public Platform(Context context)<br />
        {<br />
            this.context = context;<br />
            renderer = new PlatformRenderer(context, this);<br />
        }</p>
<p>        public ViewGroup View<br />
        {<br />
            get { return (ViewGroup)renderer; }<br />
        }</p>
<p>        public static implicit operator ViewGroup(Platform canvas)<br />
        {<br />
            return (ViewGroup)canvas.renderer;<br />
        }</p>
<p>        public static Context GetPageContext(BindableObject bindable)<br />
        {<br />
            return (Context)bindable.GetValue(Platform.PageContextProperty);<br />
        }</p>
<p>        public static void SetPageContext(BindableObject bindable, Context context)<br />
        {<br />
            bindable.SetValue(Platform.PageContextProperty, (object)context);<br />
        }</p>
<p>        public static IVisualElementRenderer GetRenderer(BindableObject bindable)<br />
        {<br />
            return (IVisualElementRenderer)bindable.GetValue(Platform.RendererProperty);<br />
        }</p>
<p>        public static void SetRenderer(BindableObject bindable, IVisualElementRenderer value)<br />
        {<br />
            bindable.SetValue(Platform.RendererProperty, (object)value);<br />
        }</p>
<p>        private void AddChild(VisualElement view)<br />
        {<br />
            if (Platform.GetRenderer((BindableObject)view) != null)<br />
                return;<br />
            Platform.SetPageContext((BindableObject)view, this.context);<br />
            IVisualElementRenderer renderer = RendererFactory.GetRenderer(view);<br />
            Platform.SetRenderer((BindableObject)view, renderer);<br />
            this.renderer.AddView(renderer.ViewGroup);<br />
        }</p>
<p>        internal void OnLayout(bool changed, int l, int t, int r, int b)<br />
        {<br />
            if (changed)<br />
            {<br />
                foreach (VisualElement visualElement in Roots)<br />
                    visualElement.Layout(new Rectangle(0.0, 0.0, ContextExtensions.FromPixels(this.context, (double)(r - l)), ContextExtensions.FromPixels(this.context, (double)(b - t))));<br />
            }<br />
            foreach (IVisualElementRenderer visualElementRenderer in Enumerable.Select<Page, IVisualElementRenderer>(Roots, new Func<Page, IVisualElementRenderer>(Platform.GetRenderer)))<br />
                visualElementRenderer.UpdateLayout();<br />
        }<br />
    }<br />
}</p>
<p>

XamarinFormsAndroid.zip

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

Written by Shaddix

Ноябрь 26th, 2014 at 5:19 пп

Posted in Android,xamarin

4 комментария to 'Xamarin.Forms и сложности интеграции в Android приложения'

Subscribe to comments with RSS or TrackBack to 'Xamarin.Forms и сложности интеграции в Android приложения'.

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

    Pat Gulotta

    26 Мар 15 at 00:43

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

    Дмитрий

    29 Дек 15 at 16:04

  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

    Alex Rainman

    29 Янв 16 at 21:45

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

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

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

    Shaddix

    23 Фев 16 at 14:12

Leave a Reply