Продолжим рассмотрение нового подхода разработки (1 часть, 2 часть) и в этой статье рассмотрим использование нативных контролов на базе механизма презентаций для iOS. В качестве примера мы превратим TSpinBox в нативный для iOS.
Код рабочего проекта: Скачать
Содержание
UIStepper — нативный аналог TSpinBox на iOS
Примечание. Перед тем, как мы рассмотрим создание нативного представления настоятельно рекомендую ознакомиться с концепцией нативных контролов в iOS. А именно View Programming Guide for iOS и iOS Target-Action механизмом.
TSpinBox — это компонент предназначенный для ввода целых или вещественных чисел. Этот компонент использует TEdit для осуществления ввода чисел. А дополнительные кнопки служат для автоматического изменения числа на дельту, указанную в TSpinBox.Increment. Так этот компонент выглядит под Windows.
По скольку TSpinBox является наследником TEdit (который поддерживает презентации), то TSpinBox так же поддерживает использование презентаций.
Среди нативных компонентов iOS, такого компонента не существует. Однако есть компонент очень близкий к нашему — это UIStepper. Отличается он от TSpinBox тем, что у него нет отображения вводимого числа и у него нет ввода числа напрямую. Ввод осуществляется только путем нажатия на кнопки «+» и «-«
UIStepper так же поддерживает ввод целых и вещественных чисел. Рассмотрим, как в TSpinBox использовать UIStepper для iOS.
Базовое нативное представление для iOS
TiOSNativeView является наследником UIView
Для использования нативных контролов, как нативное представление, FireMonkey содержит базовый класс TiOSNativeView. Это представление по сути является нативным контролом UIView. Это значит, что вы можете напрямую пользоваться всеми методами UIView. UIView является основой всех визуальных нативных компонентов iOS. UIView является аналогом TControl в FireMonkey. UIView обеспечивает:
- Вложенность контролов
- Позиционирование, повороты, скалирование
- Отрисовку
- Обработку событий
- Возможность использование жестов
- Анимация
Заметьте, что список очень похож на функциональность класса TControl. Нативное представление TiOSNativeView обеспечивает:
- Позиционирование нативного контрола согласно позиции TPresentedControl. Другими словами при изменении позиции контрола FireMonkey, TiOSNativeView осуществляет расположение нативного контрола в месте расположения FireMonkey контрола
- Реализует события работы с мышкой (Touch)
- Работа с фокусом
- Встраивание нативных контролов друг в друга (Z-Order).
- Клиппинг вложенных нативных контролов.
TiOSNativeControl является наследником UIControl
Следующим базовым классом нативного iOS представления является TiOSNativeControl. Он является наследником от TiOSNativeView. И добавляет функциональность по работе с iOS Target-Action механизмом. Это представление по сути является UIControl.
Создание нативного iOS представление для TSpinBox
Я не буду подробно описывать процесс создания представления. Он достаточно хорошо рассмотрен в 2 части статьи на примере TEdit. По аналогии, представление поместим в одноименный с название TSpinBox модуль FMX.SpinBox.iOS.pas. Суффикс iOS означает, что это нативное представление для iOS платформы.
UIStepper является наследником UIControl, поэтому в качестве базового представления берем TiOSNativeControl
UIStepper -> UIControl -> UIView
Я назвал представление TiOSNativeSpinBox.
type { TiOSNativeSpinBox } TiOSNativeSpinBox = class(TiOSNativeControl) end;
Для него, как и в случае с TEdit нужно создать отдельное прокси TiOSSpinBoxProxy.
{ TiOSSpinBoxProxy } TiOSSpinBoxProxy = class(TPresentationProxy) protected function CreateReceiver: TObject; override; end; implementation { TiOSSpinBoxProxy } function TiOSSpinBoxProxy.CreateReceiver: TObject; begin Result := TiOSNativeSpinBox.Create; end;
Сразу же зарегистрируем наше представление в фабрике представлений:
initialization TPresentationProxyFactory.Current.Register(TSpinBox, TControlType.Platform, TiOSSpinBoxProxy); end.
Как мы помним в фабрике все представления регистрируются с указанием уникального имени. Но в в нашем случае при регистрации представления мы указываем класс контрола, ControlType и класс прокси нашего представления, вместо использования строкового идентификатора. При таком подходе имя представления будет сгенерировано автоматически и будет равно: «SpinBox-native«. Этот способ более удобен, так как он не требует от разработчика вдаваться в подробности генерации имени. И этот способ полность. эквивалентен такой регистрации:
initialization TPresentationProxyFactory.Current.Register('SpinBox-native', TiOSSpinBoxProxy); end.
Не торопитесь запускать текущий код, так как нам нужно каким-то способом указать, что TiOSNativeSpinBox — это на самом деле UIStepper.
TiOSNativeSpinBox как объект UIStepper
Для того, чтобы сказать, что наше представление является на самом деле объектом UIStepper, Delphi-IOS мост (работа с нативным API iOS из делфи) содержит специальный виртуальный метод:
function TOCLocal.GetObjectiveCClass: PTypeInfo; override;
Этот метод должен вернуть указатель на тип нативного контрола. Работа iOS-Delphi моста работает так, что мы должны расширить интерфейс контрола UIStepper.
IFMXUIStepper = interface(UIStepper) ['{460C2A47-6D76-4945-BCBF-4A14B2147C48}'] end;
А затем вернуть IFMXUIStepper в GetObjectiveCClass:
function TiOSNativeSpinBox.GetObjectiveCClass: PTypeInfo; begin Result := TypeInfo(IFMXUIStepper); end;
Теперь при создании экземпляра TiOSNativeSpinBox, мост вытащит информацию о интерфейсе нативного класса и создаст нативный контрол UIStepper,и а так же вяжет его с TiOSNativeSpinBox.
Не будем вдаваться в детали реализации моста. Так как это сложная и большая тема, которая не входит в рамки данной статьи. Для нас главное то, что если мы хотим создать на делфи стороне нативный объект из iOS и отнаследоваться от него, нам нужно :
- Взять в качестве базового класса TOCLocal (в нашем случае это базовый класс для TiOSNativeView)
- Создать отдельный Delphi интерфейс с уникальным GUID, отнаследованный от интерфейса расширяемого нативного класса (в нашем случае IFMXUIStepper).
- Вернуть указатель на этот интерфейс в перекрытом методе GetObjectiveCClass.
Запомните это, таким способом осуществляется наследование нативных iOS классов на Delphi стороне.
Вот теперь если вы запустите ваше приложение с TSpinBox на iOS, то вы уже увидите нативный UIStepper на месте TSpinBox. До этого не забудьте переключить ControlType у TSpinBox в Platform. Я сделал это в событии TForm.OnCreate:
procedure TForm1.FormCreate(Sender: TObject); begin SpinBox1.ControlType := TControlType.Platform; end;
Чтобы получить прямой доступ к интерфейсу UIStepper у нативного контрола, я написал небольшой хелпер (Код только хелпера, без предыдущих методов и классов):
TiOSNativeSpinBox = class(TiOSNativeControl) private function GetView: UIStepper; public property View: UIStepper read GetView; end; implementation function TiOSNativeSpinBox.GetView: UIStepper; begin Result := inherited GetView<UIStepper>; end;
Он использует базовый шаблонный метод GetView, которые возвращает интерфейс для доступа к нативному контролу.
Синхронизация настроек работы TSpinBox с UIStepper
Ну что же, на этом этапе у нас уже есть нативное представление, но оно ни как не реагирует на настройки TSpinBox, так как мы не реализовали этот функционал:
- Value — текущее значение
- Increment — шаг изменения значения
- Min — нижняя допустимая граница для Value
- Max — Верхняя допустимая граница для Value
- RepeatClick — изменять значение один раз за клик или при зажатом клике менять каждый определенный интервал времени.
- ValueType — тип значения (целое, вещественное)
- DecimalDigits — количество цифр после запятой (актуально для ValueType = Float)
Все эти данные хранятся в моделе TSpinBoxModel (TEditBoxModel):
TSpinBoxModel = class(TEditBoxModel) public const DefaultHorzAlign = TTextAlign.Center; DefaultRepeatClick = False; private FRepeatClick: Boolean; procedure SetRepeatClick(const Value: Boolean); protected function GetTextSettingsClass: TTextSettingsInfo.TCustomTextSettingsClass; override; public constructor Create; override; /// <summary>Need makes several clicks until the user didn't raise a finger from the screen.</summary> property RepeatClick: Boolean read FRepeatClick write SetRepeatClick; end;
При изменении эти значения модель отсылает следующие сообщения:
- Min, Max, Value — MM_VALUERANGE_CHANGED
- RepeatClick — MM_SPINBOX_REPEATCLICK_CHANGED
- ValueType — MM_VALUETYPE_CHANGED
- DecimalDigits — MM_DECIMALDIGITS_CHANGED
Соответственно, перехватываем эти сообщения и настраиваем нативный UIStepper согласно нашим настройкам. Но перед этим нам нужно получить модель TSpinBox .
Получение модели TSpinBox в представлении
Когда презентационный контрол загружает представление, он отправляет свою модель представлению через сообщение PM_SET_MODEL. Перехватываем это сообщение и достаем из сообщения модель:
private [Weak] FModel: TSpinBoxModel; protected { Messages from PresentationProxy } procedure PMSetModel(var AMessage: TDispatchMessageWithValue<TMessageSender>); message PM_SET_MODEL; public property Model: TSpinBoxModel read FModel; end;
Реализация метода:
procedure TiOSNativeSpinBox.PMSetModel(var AMessage: TDispatchMessageWithValue<TMessageSender>); begin if AMessage.Value is TSpinBoxModel then begin FModel := TSpinBoxModel(AMessage.Value); FModel.Receiver := Self; end else raise EPresentationWrongModel.Create(Format(SWrongModelClassType, [TSpinBoxModel.ClassName, AMessage.Value.ClassName])); end;
При получении модели, мы должны указать, что получатель уведомлений от модели будет являться наше представление. В противном случае мы не будем знать в представлении, когда произошли изменения данных модели.
Первоначальная иниициализация представления
При загрузки представления нам нужно сделать первичную настройку нативного контрола и задать ему актуальные данные, согласно данным в модели. После создания представления, презентационный контрол TPresentedControl шлет сообщение PM_INIT для выполнения первичной инициализации представления. Перехватываем и реализуем:
TiOSNativeSpinBox = class(TiOSNativeControl) protected { Messages from PresentationProxy } procedure PMInit(var AMessage: TDispatchMessage); message PM_INIT; end; implementation procedure TiOSNativeSpinBox.PMInit(var AMessage: TDispatchMessage); begin View.setMinimumValue(Model.ValueRange.Min); View.setMaximumValue(Model.ValueRange.Max); View.setValue(Model.Value); View.setAutorepeat(Model.RepeatClick); View.setStepValue(Model.HorzIncrement); end;
Далее, уже по ходу работы контрола, модель при изменении своих данных будет отсылать нам уведомления.
Задание допустимых границ значения и текущего значения
Когда меняются параметры модели Min, Max, Value модель отсылает сообщения с кодом MM_VALUERANGE_CHANGED. Перехватываем сообщение и задаем эти значения нативному UIStepper:
TiOSNativeSpinBox = class(TiOSNativeControl) { Messages from Model } procedure MMValueRangeChanged(var AMessage: TDispatchMessage); message MM_VALUERANGE_CHANGED; implementation procedure TiOSNativeSpinBox.MMValueRangeChanged(var AMessage: TDispatchMessage); begin View.setMinimumValue(Model.ValueRange.Min); View.setMaximumValue(Model.ValueRange.Max); View.setValue(Model.Value); end;
Реализация RepeatClick
При изменении свойства RepeatClick, модель шлет сообщение MM_SPINBOX_REPEATCLICK_CHANGED. Перехватываем и задаем соответственную опцию у UIStepper.
TiOSNativeSpinBox = class(TiOSNativeControl) protected { Messages from Model } procedure MMRepeatClickChanged(var AMessage: TDispatchMessage); message MM_SPINBOX_REPEATCLICK_CHANGED; end; implementation procedure TiOSNativeSpinBox.MMRepeatClickChanged(var AMessage: TDispatchMessage); begin View.setAutorepeat(Model.RepeatClick); end;
Реализация OnChange
UIStepper через iOS Target-Action механизм уведомляет о изменении значения. Чтобы подписаться на уведомления нам нужно:
- Создать метод класса, который будет вызываться при изменении значения в UIStepper.
TiOSNativeSpinBox = class(TiOSNativeControl) public procedure ValueChanged; cdecl; end; implementation procedure TiOSNativeSpinBox.ValueChanged; begin Model.DisableNotify; try Model.Value := View.value; finally Model.EnableNotify; end; Model.Change; end;
- Зарегистрировать его в UIStepper, как метод, который должен быть вызыван после изменения значения в UIStepper. Для этого воспользуемся в конструкторе методом RegisterNativeEventHandler.
constructor TiOSNativeSpinBox.Create; begin inherited; RegisterNativeEventHandler('ValueChanged', UIControlEventValueChanged); end;
- Добавить этот метод в интерфейс IFMXUIStepper:
{ TiOSNativeSpinBox } IFMXUIStepper = interface(UIStepper) ['{460C2A47-6D76-4945-BCBF-4A14B2147C48}'] procedure ValueChanged; cdecl; end;
Обратите внимание, что все методы работающие с нативным API должны использовать способы вызова языка С cdecl.
UIStepper и фиксированный размер
UIStepper имеет фиксированный размер. Это значит, что наше нативное представление должно сказать презентационному контролу, что:
- У него полностью фиксированная ширина и высота (PM_GET_ADJUST_TYPE)
- Вернуть размер нативного контрола (PM_GET_ADJUST_SIZE)
Для чего это нужно? Это нужно, чтобы наш нативный контрол корректно выравнялся в FMX дереве контролов. Поэтому прехватываем два сообщения PM_GET_ADJUST_TYPE и PM_GET_ADJUST_SIZE и возвращаем нужные значения:
TiOSNativeSpinBox = class(TiOSNativeControl) protected { Messages from PresentationProxy } procedure PMGetAdjustSize(var AMessage: TDispatchMessageWithValue<TSizeF>); message PM_GET_ADJUST_SIZE; procedure PMGetAdjustType(var AMessage: TDispatchMessageWithValue<TAdjustType>); message PM_GET_ADJUST_TYPE; end; implementation procedure TiOSNativeSpinBox.PMGetAdjustSize(var AMessage: TDispatchMessageWithValue<TSizeF>); begin AMessage.Value := TSizeF.Create(View.frame.size.width, View.frame.size.height); end; procedure TiOSNativeSpinBox.PMGetAdjustType(var AMessage: TDispatchMessageWithValue<TAdjustType>); begin AMessage.Value := TAdjustType.FixedSize; end;
По скольку мы не знаем заранее какой размер будет у UIStepper, мы напарямую запрашиваем его размеры в PMGetAdjustSize.
Результат
Вот теперь вы получили полноценное нативное представления для TSpinBox. По такой же схеме делаются нативные представления для любого нативного контрола. После запуска, мы можем наблюдать, как TSpinBox использует нативный контрол.
Код рабочего проекта: Скачать
Уведомление: Fire Monkey - Yaroslav Brovin » Создание нативных представлений для iOS. TSpinBox и UIStepper. Часть 3
After this
SpinBox.ControlType := TControlType.Platform;
SpinBox.Value := 7; // ignored by View
Any value change in code does not reach the iOS View. If I try to increment or decrement the value via GUI interaction the former value is used.
Workaround
SpinBox.ControlType := TControlType.Styled;
SpinBox.Value := 7;
SpinBox.ControlType := TControlType.Platform;
It is better settings the limits before changing the value.
Original Code:
procedure TiOSNativeSpinBox.MMValueRangeChanged(var AMessage: TDispatchMessage);
begin
View.setValue(Model.Value);
View.setMinimumValue(Model.ValueRange.Min);
View.setMaximumValue(Model.ValueRange.Max);
end;
Modified Code:
procedure TiOSNativeSpinBox.MMValueRangeChanged(var AMessage: TDispatchMessage);
begin
View.setMinimumValue(Model.ValueRange.Min);
View.setMaximumValue(Model.ValueRange.Max);
View.setValue(Model.Value);
end;