TListView является одним из ключевых компонентов для построения интерфейса мобильного приложения в FireMonkey. Компонент этот не самый простой в использовании, зачастую предполагает значительный объем кода, зато предоставляет разработчику значительную свободу действий. Конечно, в приложениях можно использовать и TListBox, где все намного проще. Но TListBox, возможно, хорош для отображения фиксированного количества записей, для вывода данных из источников данных, однозначно нужно использовать TListView.
Главные отличия TListView от TListBox в:
- TListBoxItem - контрол, TListViewItem - нет
- В TListBoxItem можно добавлять любые контролы, используя Parent. В TListVIewItem - нет.
- TListVIewItem хранит только данные для отображения
- TListVIewItem сам выполняет отрисовку хранимых данных через метод Render
- За счет собственно ручной отрисовки в TListVIewItem достигается прирост скорости и малое потребление памяти (хранение только актуальных данных)
- Чтобы создать свой вариант TListViewItem, нужно создать свой класс итема, в нем реализовать требуемые данные (например время) и создать in-place редактор для редактирования времени, зарегистрировать его и т. д.
Сам по себе факт повышения производительности и уменьшения потребления памяти – веский аргумент в пользу использования TListView. Но есть и еще кое что.
Во многих Android приложениях мне приходилось наблюдать следующую реализацию списков. При нажатии на элемент списка (Item, если придерживаться выбранной терминологии), производится определенное действие. Обычно вызывается новая форма для редактирования данных. Но при нажатии с удерживанием (Long Tap) производится совершенно другое действие. И эти события не пересекаются. Иными словами Android приложения умеют четко различать “длинное нажатие” от “обычного”. Более того, ни одно из этих событий не срабатывает при скроллинге списка. Наглядный пример – список писем в Яндекс Почта.
При попытке реализовать подобный функционал с помощью TListBox, я получил чехарду из накладывающихся друг на друга событий. Главной проблемой оказалось отделение скроллинга от обычного нажатия. В TListView, как оказалось, с этим проблем нет. Но правильно определить события – тоже задача довольно интересная. Дабы сэкономить время своим читателям, я поделюсь здесь результатами своих экспериментов.
Итак, я заполняю список данными из набора данных следующим образом:
procedure TForm1.InitTiming; var LItem, iHeader: TListViewItem; I: Integer; curDate: TDate; im: TListItemImage; procedure AddHeader(aDate: TDate); begin iHeader := ListView1.Items.Add; iHeader.Text := DateToStr(aDate); iHeader.Purpose := TListItemPurpose.Header; end; begin ListView1.BeginUpdate; ListView1.Items.Clear; try while not UniQTimeSheets.Eof do begin if curDate <> DateOf(UniQTimeSheetsTimeFrom.AsDateTime) then AddHeader(UniQTimeSheetsTimeFrom.AsDateTime); LItem := ListView1.Items.Add; LItem.Text := 'From: ' + FormatDateTime('t', UniQTimeSheetsTimeFrom.AsDateTime) + ' Hours: ' + HoursMinutesFromMin(UniQTimeSheetsHourse.AsInteger, ':'); LItem.Tag := UniQTimeSheetsID.AsInteger; LItem.Data[TMultiDetailAppearanceNames.Detail] := UniQTimeSheetsCompanyName.AsString; curDate := DateOf(UniQTimeSheetsTimeFrom.AsDateTime); UniQTimeSheets.Next; end; finally ListView1.EndUpdate; end; end;
По “обычному” нажатию я хочу отобразить Id записи (Tag элемента списка), по длинному нажатию я хочу перевести режим редактирования (слева будут отображаться CheckBox’ы). Очевидно, что для того, что бы “отловить” длинное нажатие следует обработать событие OnGuesture.
procedure TForm1.ListView1Gesture(Sender: TObject; const EventInfo: TGestureEventInfo; var Handled: Boolean); begin try if EventInfo.GestureID = System.UITypes.igiLongTap then begin if not ListView1.EditMode then begin ListView1.EditMode := True; end; end; except end; end;
Соответственно, на OnClickEx вешаем следующий код:
procedure TForm1.ListView1ItemClickEx(const Sender: TObject; ItemIndex: Integer; const LocalClickPos: TPointF; const ItemObject: TListItemObject); begin Memo1.Lines.Add('ItemClickEx Start'); if (not ListView1.EditMode) and blnCanShowForm then begin ShowMessage(IntToStr(ItemIndex)); end; end;
Если запустить этот код под Windows на ПК с мультитач экраном, он прекрасно отработает. Но вот под Android. увы, при длинном нажатии сначала будет выведен Id элемента, а потом ListView благополучно перейдет в режим редактирования.
Прежде всего, я решил выяснить последовательность вызываемых событий. Для этого я повесил на форму компонент TMemo и в обработчики ключевых событий вписал строку следующего вида
Memo1.Lines.Add(<Название события>);
Сразу оговорюсь, что я знаю о механизмах логирования в Android и о CodeSite Studio. Но, выбрал столь архаичный метод логирования ввиду простоты и универсальности. Впрочем, логирование для Android – совсем другая история.
Что же касается результатов. Под Windows при обычном нажатии я получил следующую последовательность событий:
- Tap
- Click
- ItemClick
- Change
- ItemClickEx
При Long Tap она выглядит так:
- Gesture
- EditModeChanging
- EditModeChange
- Tap
Совершенно очевидно, что предложенная выше обработка событий без проблем будет работать под Windows. При обычном нажатии срабатывает событие OnCahnge, а затем выполняется обработчик ItemClickEx.
В случае Long Tap, ни ItemClick ни ItemClickEx не выполняются, что позволяет обрабатывать длинное нажатие абсолютно независимо. Обращу внимание на то, что здесь идет речь именно о нажатии, а не о клике мышкой.
Как же обстоят дела в мобильном приложении?
При обычном нажатии все точно так же:
- Tap
- Click
- ItemClick
- Change
- ItemClickEx
А вот при длинном нажатии совершенно другая картина:
- ItemClick
- ItemClickEx
- Gesture
- EditModeChanging
- EditModeChange
- Tap
- Click
- Change
Перед обработчиком жеста вызывается OnItemClickEx. таким образом два события. которые должны быть независимы, накладываются друг на друга. Конечно, вполне можно бы было обработать обычное нажатие с помощью события OnTap. Но в этом случае обработчик будет вызван до на новый Item и мы не сможем узнать интересующий нас Id .
Но, как вы понимаете, при наличии наглядной последовательности событий, разрешение данной задачи не является великой алгоритмической проблемой. Самый примитивный вариант – ввести “флажок”, и с его помощью в обработчике события OnTap разрешать выводить Id, а в OnEditModeChange и OnClickEx – сбрасывать флажок.
Скроллинг при этом продолжает работать абсолютно корректно, не пересекаясь с задействованными нами событиями.
procedure TForm1.ListView1Gesture(Sender: TObject; const EventInfo: TGestureEventInfo; var Handled: Boolean); begin try Memo1.Lines.Add('ListView1Gesture'); if EventInfo.GestureID = System.UITypes.igiLongTap then begin if not ListView1.EditMode then begin ListView1.EditMode := True; Memo1.Lines.Add('igiLongTap'); end; end; except end; end; procedure TForm1.ListView1Tap(Sender: TObject; const Point: TPointF); begin Memo1.Lines.Add('Tap'); blnCanShowForm := True; end; procedure TForm1.ListView1EditModeChange(Sender: TObject); begin blnCanShowForm := False; Memo1.Lines.Add('EditModeChange'); end;
procedure TForm1.ListView1ItemClickEx(const Sender: TObject; ItemIndex: Integer; const LocalClickPos: TPointF; const ItemObject: TListItemObject); begin // Memo1.Lines.Add('ItemClickEx Start'); if (not ListView1.EditMode) and blnCanShowForm then begin ShowMessage(IntToStr(ItemIndex)); end; blnCanShowForm := False; Memo1.Lines.Add('ItemClickEx Finish'); end;
Хотите сэкономить? До 30 сентября у вас есть возможность выгодно купить Windows 10.
Хотите выгодно приобрести новую ОС? Подсказываем: в течение первого года после выхода новой ОС, до 29 июля 2016 года, все пользователи Windows 7 и Windows 8.1 могут бесплатно установить Windows 10 на свои ПК. Таким образом, вы можете выгодно приобрести Windows 8.1 и бесплатно перейти на Windows 10:
Microsoft Windows 8.1 Home за 4990 рублей
Microsoft Windows 8.1 Professional за 8990 руб.
Спасибо! Отличная статья, выручила по времени
Ни как не мог разобраться как добраться до хедоров и поменять значения.