Создание нативных представлений для iOS. TSpinBox и UIStepper. Часть 3

native_iOS_SpinBoxПродолжим рассмотрение нового подхода разработки (1 часть, 2 часть) и в этой статье рассмотрим использование нативных контролов на базе механизма презентаций для iOS. В качестве примера мы превратим TSpinBox в нативный для iOS.

Код рабочего проекта: Скачать


UIStepper — нативный аналог TSpinBox на iOS

Примечание. Перед тем, как мы рассмотрим создание нативного представления настоятельно рекомендую ознакомиться с концепцией нативных контролов в iOS. А именно View Programming Guide for iOS и iOS Target-Action механизмом.

TSpinBox — это компонент предназначенный для ввода целых или вещественных чисел. Этот компонент использует TEdit для осуществления ввода чисел. А дополнительные кнопки служат для автоматического изменения числа на дельту, указанную в TSpinBox.Increment. Так этот компонент выглядит под Windows.

2015-04-10 17-56-18 Project4 - Unit13

По скольку TSpinBox является наследником TEdit (который поддерживает презентации), то TSpinBox так же поддерживает использование презентаций.

Среди нативных компонентов iOS, такого компонента не существует. Однако есть компонент очень близкий к нашему — это UIStepper. Отличается он от TSpinBox тем, что у него нет отображения вводимого числа и у него нет ввода числа напрямую. Ввод осуществляется только путем нажатия на кнопки «+» и «-«

uistepper_intro_2x

UIStepper так же поддерживает ввод целых и вещественных чисел. Рассмотрим, как в TSpinBox использовать UIStepper для iOS.

Базовое нативное представление для iOS

TiOSNativeView является наследником UIView

Для использования нативных контролов, как нативное представление, FireMonkey содержит базовый класс TiOSNativeView. Это представление по сути является нативным контролом UIView. Это значит, что вы можете напрямую пользоваться всеми методами UIView. UIView является основой всех визуальных нативных компонентов iOS. UIView является аналогом TControl в FireMonkey. UIView обеспечивает:

  1. Вложенность контролов
  2. Позиционирование, повороты, скалирование
  3. Отрисовку
  4. Обработку событий
  5. Возможность использование жестов
  6. Анимация

Заметьте, что список очень похож на функциональность класса TControl. Нативное представление TiOSNativeView обеспечивает:

  1. Позиционирование нативного контрола согласно позиции TPresentedControl. Другими словами при изменении позиции контрола FireMonkey, TiOSNativeView осуществляет расположение нативного контрола в месте расположения FireMonkey контрола
  2. Реализует события работы с мышкой (Touch)
  3. Работа с фокусом
  4. Встраивание нативных контролов друг в друга (Z-Order).
  5. Клиппинг вложенных нативных контролов.

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 и отнаследоваться от него, нам нужно :

  1. Взять в качестве базового класса TOCLocal (в нашем случае это базовый класс для TiOSNativeView)
  2. Создать отдельный Delphi интерфейс с уникальным GUID, отнаследованный от интерфейса расширяемого нативного класса (в нашем случае IFMXUIStepper).
  3. Вернуть указатель на этот интерфейс в перекрытом методе 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, так как мы не реализовали этот функционал:

  1. Value — текущее значение
  2. Increment — шаг изменения значения
  3. Min — нижняя допустимая граница для Value
  4. Max — Верхняя допустимая граница для Value
  5. RepeatClick — изменять значение один раз за клик или при зажатом клике менять каждый определенный интервал времени.
  6. ValueType — тип значения (целое, вещественное)
  7. 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;

При изменении эти значения модель отсылает следующие сообщения:

  1. Min, Max, Value — MM_VALUERANGE_CHANGED
  2. RepeatClick — MM_SPINBOX_REPEATCLICK_CHANGED
  3. ValueType — MM_VALUETYPE_CHANGED
  4. 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 механизм уведомляет о изменении значения. Чтобы подписаться на уведомления нам нужно:

  1. Создать метод класса, который будет вызываться при изменении значения в 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;
  2. Зарегистрировать его в UIStepper, как метод, который должен быть вызыван после изменения значения в UIStepper. Для этого воспользуемся в конструкторе методом RegisterNativeEventHandler.
    constructor TiOSNativeSpinBox.Create;
    begin
      inherited;
      RegisterNativeEventHandler('ValueChanged', UIControlEventValueChanged);
    end;
  3. Добавить этот метод в интерфейс IFMXUIStepper:
    { TiOSNativeSpinBox }
    
      IFMXUIStepper = interface(UIStepper)
      ['{460C2A47-6D76-4945-BCBF-4A14B2147C48}']
        procedure ValueChanged; cdecl;
      end;

Обратите внимание, что все методы работающие с нативным API должны использовать способы вызова языка С cdecl.

UIStepper и фиксированный размер

UIStepper имеет фиксированный размер.  Это значит, что наше нативное представление должно сказать презентационному контролу, что:

  1. У него полностью фиксированная ширина и высота (PM_GET_ADJUST_TYPE)
  2. Вернуть размер нативного контрола (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 использует нативный контрол.

Код рабочего проекта: Скачать

native_iOS_SpinBox

Создание нативных представлений для iOS. TSpinBox и UIStepper. Часть 3: 3 комментария

  1. Уведомление: Fire Monkey - Yaroslav Brovin » Создание нативных представлений для iOS. TSpinBox и UIStepper. Часть 3

  2. Thomas_K

    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;

  3. Thomas_K

    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;

Добавить комментарий для Thomas_K Отменить ответ

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