Мій досвід з MAUI. Створюємо прапорець з текстом за допомогою Handlers
Вітаю! Мене звати Ігор, я маю деякий досвід у розробці мобільних застосунків за допомогою MAUI. Сьогодні ми продовжимо розглядати MAUI на прикладах.
У минулій статті ми додавали текст для стандартного прапорця і використовували для цього ContentView, у якому розміщували Grid або HorizontalStackLayout з CheckBox та Label всередині.
Рисунок 1 — Схема прапорця з текстом, що оснований на ContentView
Головним плюсом такого підходу є повна кросплатформність. Тобто ми створюємо цей компонент, використовуючи готові абстрактні елементи керування. Такий прапорець із текстом буде виглядати майже однаково для всіх платформ, і нам не доведеться працювати з нативною частиною.
Мінусом є те, що використано багато компонентів. Потенційно це може погано вплинути на швидкодію. Наприклад, на сторінці ми додали CollectionView, який у шаблоні має ось такий складений із декількох частин прапорець. Під час гортання переліку застосунок буде повільніше малювати ось такий складений прапорець, аніж нативний прапорець із текстом.
Рисунок 2. Схема відображення прапорця з текстом в CollectionView
Тому наша ціль сьогодні використати повністю нативну реалізацію прапорця з текстом. Це дозволить значно спростити ієрархію відображення елементів. Мінусом буде те, що доведеться для кожної платформи реалізовувати відображення тексту власноруч.
Унаслідок цього схема буде приблизно така:
Рисунок 3. Спрощена схема CollectionView та нативної реалізації прапорця з текстом
Насправді справжня схема буде трохи іншою, але про це згодом.
Отже, як ми знаємо з першої статті, нам потрібно створити загальний клас для прапорця, загальний Handler з мапінгом властивостей і Handlers для кожної платформи.
Створення загального класу
Додамо код:
using Font = Microsoft.Maui.Font; namespace AndriyCo.Tcu.MauiClient.Controls; public interface IMaterialCheckBox : ICheckBox, IText { } public class MaterialCheckBox : CheckBox, IMaterialCheckBox { public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(IText), default); public static readonly BindableProperty TextColorProperty = BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(IText), default); public static readonly BindableProperty FontProperty = BindableProperty.Create(nameof(Font), typeof(Font), typeof(IText), default); public static readonly BindableProperty CharacterSpacingProperty = BindableProperty.Create(nameof(CharacterSpacing), typeof(double), typeof(IText), default); public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } public Color TextColor { get => (Color)GetValue(TextColorProperty); set => SetValue(TextColorProperty, value); } public Font Font { get => (Font)GetValue(FontProperty); set => SetValue(FontProperty, value); } public double CharacterSpacing { get => (double)GetValue(CharacterSpacingProperty); set => SetValue(CharacterSpacingProperty, value); } }
Ми створили клас MaterialCheckBox і інтерфейс IMaterialCheckBox, які наслідуються від стандартних CheckBox та ICheckBox відповідно.
Додали властивості для відображення тексту й тепер можемо переходити до створення Handlers.
Створення Handlers
Створимо загальний Handler:
using Microsoft.Maui.Handlers; #if IOS || MACCATALYST using PlatformView = Microsoft.Maui.Platform.MauiCheckBox; #elif ANDROID using PlatformView = Google.Android.Material.CheckBox.MaterialCheckBox; #elif WINDOWS using PlatformView = Microsoft.UI.Xaml.Controls.CheckBox; #elif NET || (NET6_0 && !IOS && !ANDROID && !TIZEN) using PlatformView = System.Object; #endif namespace AndriyCo.Tcu.MauiClient.Handlers; public interface IMaterialCheckBoxHandler : ICheckBoxHandler { new IMaterialCheckBox VirtualView { get; } new PlatformView PlatformView { get; } } public partial class MaterialCheckBoxHandler : IMaterialCheckBoxHandler { public new IMaterialCheckBox VirtualView => (IMaterialCheckBox)base.VirtualView; PlatformView IMaterialCheckBoxHandler.PlatformView => PlatformView; public static IPropertyMapper<IMaterialCheckBox, IMaterialCheckBoxHandler> TextMapper = new PropertyMapper<IMaterialCheckBox, IMaterialCheckBoxHandler>(Mapper) { [nameof(IMaterialCheckBox.Text)] = MapText, [nameof(IMaterialCheckBox.TextColor)] = MapTextColor, }; public MaterialCheckBoxHandler() : base(TextMapper) { } public MaterialCheckBoxHandler(IPropertyMapper mapper, CommandMapper commandMapper = null) : base(mapper, commandMapper) { } }
У блоці using ми зазначили, чим буде PlatformView для кожної платформи. Створили інтерфейс та partial клас MaterialCheckBoxHandler для Handler і додали мапінг властивостей до методів, що змінюють ці властивості на нативному рівні.
Handler для Android
Реалізація тексту для прапорця доволі проста:
using Microsoft.Maui.Handlers; using Microsoft.Maui.Platform; namespace AndriyCo.Tcu.MauiClient.Handlers; public partial class MaterialCheckBoxHandler : CheckBoxHandler { private static void MapText(IMaterialCheckBoxHandler handler, IMaterialCheckBox virtualView) => handler.PlatformView.Text = virtualView.Text; private static void MapTextColor(IMaterialCheckBoxHandler handler, IMaterialCheckBox virtualView) => handler.PlatformView.UpdateTextColor(virtualView); }
Створюємо клас MaterialCheckBoxHandler.Android і реалізуємо методи MapText і MapTextColor для Android-прапорця.
Handler для iOS
Реалізація для iOS трохи складніша. Для цієї платформи Microsoft створили власний клас MauiCheckBox, який наслідується від нативного UIButton. Це доволі поширена в них практика: так вони роблять свою надбудову над нативними елементами, щоб потім зручніше використовувати їх у Handlers. Подібне ми також робили у першій статті, створивши клас-обгортку AlertDialogView над класом-builder MaterialAlertDialogBuilder.
Ось такий вигляд має MauiCheckBox:
public class MauiCheckBox : UIButton { public EventHandler? CheckedChanged; public MauiCheckBox(); public bool IsChecked { get; set; } public bool IsEnabled { get; set; } public Color? CheckBoxTintColor { get; set; } public override bool Enabled { get; set; } public override UIAccessibilityTrait AccessibilityTraits { get; set; } public override string? AccessibilityValue { get; set; } public override void LayoutSubviews(); public override CGSize SizeThatFits(CGSize size); protected override void Dispose(bool disposing); protected virtual UIImage GetCheckBoxImage(); }
У нативному прапорці для iOS відсутня властивість Text чи щось подібне. Тому доведеться додати Label до MauiCheckBox, що існує.
Створюємо клас MaterialCheckBoxHandler.iOS:
using AndriyCo.Tcu.MauiClient.Controls; using CoreGraphics; using Microsoft.Maui.Handlers; using Microsoft.Maui.Platform; using UIKit; namespace AndriyCo.Tcu.MauiClient.Handlers; public partial class MaterialCheckBoxHandler : CheckBoxHandler { protected UILabel Label { get; private set; } protected override MauiCheckBox CreatePlatformView() { MauiCheckBox mauiCheckBox = base.CreatePlatformView(); Label = new UILabel(GetPositionForLabel(mauiCheckBox)); Label.Lines = 3; Label.LineBreakMode = UILineBreakMode.WordWrap; Label.MinimumScaleFactor = 14; mauiCheckBox.Add(Label); } private CGRect GetPositionForLabel(MauiCheckBox mauiCheckBox) => new(mauiCheckBox.ImageView.Bounds.Width + mauiCheckBox.ImageView.LayoutMargins.Right * 2, 0, mauiCheckBox.Bounds.Width - mauiCheckBox.ImageView.Bounds.Width, mauiCheckBox.Bounds.Height); private static void MapText(IMaterialCheckBoxHandler handler, IMaterialCheckBox virtualView) { if (!(handler is MaterialCheckBoxHandler checkBoxHandler)) return; checkBoxHandler.Label.Text = virtualView.Text; } private static void MapTextColor(IMaterialCheckBoxHandler handler, IMaterialCheckBox virtualView) { if (!(handler is MaterialCheckBoxHandler checkBoxHandler)) return; checkBoxHandler.Label.TextColor = virtualView.TextColor.ToPlatform(); } }
Ми перевизначили метод CreatePlatformView, у якому додатково зробили UILabel і додали до створеного MauiCheckBox.
У методах мапінгу для зміни тексту ми тепер звертаємось до Label.
До речі, саме цей спосіб додавання тексту до прапорця для iOS я не тестував на MAUI. Колись я робив за схожим принципом для Xamarin, і все працювало. Думаю, шанс, що такий варіант не спрацює для MAUI, дуже малий. У будь-якому разі принцип саме такий: на рівні Handler якимось способом прив’язати нативний прапорець до нативного UILabel, а потім в Handler використати властивості UILabel для зміни тексту.
Узагалі, можна було б зробити трохи по-іншому: створити окремий клас TextMauiCheckBox (або MauiTextCheckBox), у ньому додати UILabel, а потім використовувати його в Handler як окремий нативний елемент.
Орієнтовний приклад: у теці Platforms -> iOS додамо клас MauiTextCheckBox:
using CoreGraphics; using Microsoft.Maui.Platform; using UIKit; namespace AndriyCo.Tcu.MauiClient.Platforms.iOS; public class MauiTextCheckBox : MauiCheckBox { protected UILabel Label { get; set; } public string Text { get => Label.Text; set => Label.Text = value; } public UIColor TextColor { get => Label.TextColor; set => Label.TextColor = value; } public override void LayoutSubviews() { base.LayoutSubviews(); SetupLabel(); } private void SetupLabel() { if (Label != null) return; CGRect labelPosition = GetLabelPosition(); Label = new UILabel(labelPosition) { Lines = 3, LineBreakMode = UILineBreakMode.WordWrap, MinimumScaleFactor = 14 }; Add(Label); } private CGRect GetLabelPosition() => new(ImageView.Bounds.Width + ImageView.LayoutMargins.Right * 2, 0, Bounds.Width - ImageView.Bounds.Width, Bounds.Height); }
Ми створили окремий клас, де описали позиціонування та створення мітки з текстом. Тепер буде простіше використовувати цей клас у Handler.
У загальному Handler потрібно вказати таке:
#if IOS || MACCATALYST using PlatformView = AndriyCo.Tcu.MauiClient.Platforms.iOS.MauiTextCheckBox;
А також зробити перетворення:
PlatformView IMaterialCheckBoxHandler.PlatformView => (PlatformView)PlatformView;
Спеціалізований Handler для iOS:
using AndriyCo.Tcu.MauiClient.Controls; using AndriyCo.Tcu.MauiClient.Platforms.iOS; using Microsoft.Maui.Handlers; using Microsoft.Maui.Platform; using UIKit; namespace AndriyCo.Tcu.MauiClient.Handlers; public partial class MaterialCheckBoxHandler : CheckBoxHandler { protected override MauiCheckBox CreatePlatformView() => new MauiTextCheckBox(); private static void MapText(IMaterialCheckBoxHandler handler, IMaterialCheckBox virtualView) => handler.PlatformView.Text = virtualView.Text; private static void MapTextColor(IMaterialCheckBoxHandler handler, IMaterialCheckBox virtualView) => handler.PlatformView.TextColor = virtualView.TextColor.ToPlatform(); }
Тепер він має такий самий вигляд, як і в Android.
Використання готового елемента
Реєструємо Handler в класі MauiProgram:
MauiAppBuilder builder = MauiApp.CreateBuilder(); builder.ConfigureMauiHandlers(handlers => { handlers.AddHandler(typeof(MaterialCheckBox), typeof(MaterialCheckBoxHandler)); });
І тепер можна додавати MaterialCheckBox до наших сторінок.
Візьмемо будь-яку сторінку з проєкту. Не забуваймо додати простір імен:
<ContentPage … xmlns:controls="clr-namespace:AndriyCo.Tcu.MauiClient.Controls" …> Додаємо прапорці: <VerticalStackLayout Padding="10" Spacing="5"> <controls:MaterialCheckBox Text="Наслідувати націнку верхньої групи" IsChecked="{Binding IsInheritedMarkup, Mode=TwoWay}"/> … <controls:MaterialCheckBox Text="Наслідувати перелік цін від верхньої групи" IsChecked="{Binding IsInheritedPriceList, Mode=TwoWay}"/> </VerticalStackLayout
Результат:
SEQ Рисунок *ARABIC. Прапорці з текстом в Android
Висновки
Отже, у цій та попередній статтях я показав два способи створення власних елементів керування. Перший — використання ContentView зі стандартними елементами керування; другий — використання нативних елементів керування через механізм Handlers.
І в першому, і в другому випадках відображено нативні елементи, бо так працює MAUI. Але в першому способі ми використовуємо більше абстракцій, тому застосунок може витрачати більше ресурсів.
З цієї схеми видно, що прапорець із текстом складається з ContentView, у якому перебуває Grid або StackLayout для позиціонування двох окремих елементів: прапорця та мітки з текстом. У свою чергу прапорець та мітка — це два окремих елементи на нативному рівні.
Така схема є більш витратною за ресурсами. Grid або StackLayout має вирахувати розміри та позицію внутрішніх елементів. Окрім того, це все ще й розташовується в ContentView, який теж має власну поведінку відображення.
У переліках, наприклад, бажано не використовувати багато рівнів відображення, інакше існує ризик затримок під час гортання.
Другий спосіб створення власних компонентів кращий за швидкодією, але його потрібно реалізувати для кожної платформи.
У цьому випадку на загальному рівні у нас лише один клас з необхідним API для керування прапорцем. На відміну від минулого способу, де на цьому рівні використовуються ContentView + Grid (StackLayout) + CheckBox + Label. Другий спосіб спрощує ієрархію елементів, що добре впливає на швидкодію.
Переважно я надаю перевагу використанню Handlers для створення власних компонентів або розширенню тих, що існують. Але іноді зручніше використовувати ContentView з готовими компонентами. Наприклад, у мене в проєкті є BreadCrumb (навігаційна панель), який є ContentView та містить в собі Grid та CollectionView. Створювати такий комплексний візуальний компонент було б складно на нативному рівні, і таке рішення повторювало б багато функціоналу зі стандартного CollectionView.
Додатково зазначу те, що хоч MAUI і має свої недоліки, про які неодноразово згадували в коментарях, але ця технологія надає гнучкий функціонал для створення застосунків. Так, деякі компоненти не мають достатньо функцій. Але у вас є змога зробити те, що вам потрібно, декількома способами. MAUI містить в собі всі недоліки кросплатформових технологій, але також надає майже необмежений доступ до нативних функцій.
Отже, до наступних зустрічей!
26 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів