Статьи

Создание приложения чата с кодовым названием One Part 4

Этот урок начинает становиться интересным … В этом разделе мы углубимся в анимацию, спецэффекты, стили поиска и извлечение контактов. В предыдущих разделах мы создали первую форму приложения, вошли в систему, и теперь нам нужно показать фактическую форму входа. Еще более важно, что мы должны показать это со стилем, подобным этому:

чат-приложение-учебник-контакты форм-1

(обратите внимание, что вы должны увидеть анимированный GIF выше, естественно, мы уменьшили качество, чтобы сделать его меньше, но загрузка все равно занимает некоторое время).

То, что мы видим здесь, это то, как форма входа в систему оживляется, сдвигая кнопки вправо, затем превращая фон в область заголовка следующей формы. Мы скоро рассмотрим, как это сделать … Но сначала давайте узнаем контактные данные!

API новых контактов

Сначала нам нужно добавить права доступа к контактам в API Google, добавив строку «gc.setScope» (электронная почта профиля https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth /plus.me»);» в первоначальный логин как таковой:

1
2
3
4
5
6
7
8
9
loginWithGoogle.addActionListener((e) -> {
    tokenPrefix = "google";
    Login gc = GoogleConnect.getInstance();
    gc.setClientId("1013232201263-lf4aib14r7g6mln58v1e36ibhktd79db.apps.googleusercontent.com");
    gc.setRedirectURI("https://www.codenameone.com/oauth2callback");
    gc.setClientSecret("uvu03IXOhx9sO8iPcmDfuX3R");
    doLogin(gc, new GoogleData());
});

Мы фактически просим больше разрешений. Обратите внимание, что профиль и электронная почта являются сокращенным синтаксисом для полного URI, принятого Google, и значения в области разделяются пробелом.

Чтобы сделать контакты общедоступными, мы изменили несколько вещей в интерфейсах, определенных на предыдущих шагах:

1
2
3
4
5
static class ContactData {
    public String uniqueId;
    public String name;
    public String imageUrl;
}

Сначала мы добавили простой абстрактный класс данных контакта для представления конкретного контакта. Обычно мы просто использовали бы что-то более простое, но поскольку у Google и Facebook радикально разные источники изображений, нам пришлось создать две разные реализации этого класса.

1
2
3
4
5
6
7
static interface UserData {
    public String getName();
    public String getId();
    public String getImage();
    public void fetchData(String token, Runnable callback);
    public ContactData[] getContacts();
}

Мы добавили только один метод к пользовательским данным, метод getContacts . Для простоты мы выбираем все контакты и делаем это синхронно. В будущей итерации мы могли бы улучшить это, создав реализацию, которая «транслирует» контакты, но сейчас мы хотели что-то простое, что также пригодно для поиска.

Получение контактов в Google

Мы подключаемся к веб-сервису Google и получаем все видимые контакты. Здесь нам не нужно ограничиваться людьми, которые установили приложение, и это дает нам больше информации:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private String token;
 
@Override
public ContactData[] getContacts() {
    ArrayList<ContactData> dat = new ArrayList<>();
    ConnectionRequest req = new ConnectionRequest() {
        @Override
        protected void readResponse(InputStream input) throws IOException {
            JSONParser parser = new JSONParser();
            Map<String, Object> parsed = parser.parseJSON(new InputStreamReader(input, "UTF-8"));
            java.util.List<Object> data = (java.util.List<Object>)parsed.get("items");
            for(Object current : data) {
                Map<String, Object> cMap = (Map<String, Object>)current;
                String name = (String)cMap.get("displayName");
                if(name == null) {
                    continue;
                }
                String type =(String)cMap.get("objectType");
                if(!type.equals("person")) {
                    continue;
                }
                String id = cMap.get("id").toString();
                ContactData cd = new ContactData();
                cd.name = name;
                cd.uniqueId = id;
 
                if(cMap.containsKey("image")) {
                    cd.imageUrl = (String)((Map<String, Object>)cMap.get("image")).get("url");;
                }
 
                dat.add(cd);
            }
        }
    };
    req.setPost(false);
    if(token == null) {
        token = Preferences.get("googletoken", (String)null);
    }
    req.addArgumentNoEncoding("key", token);
    NetworkManager.getInstance().addToQueueAndWait(req);
 
    ContactData[] cd = new ContactData[dat.size()];
    dat.toArray(cd);
    return cd;
}

Код немного большой, но на самом деле он очень прост, мы получаем токен Google, который мы предоставляем API Google для запроса списка контактов. Затем мы делаем синхронный вызов, используя addToQueueAndWait что действительно удобно в этом случае, и добавляем все записи в список массивов.

Обратите внимание, что мы пропускаем типы объектов, которые не являются «персонами», в API Google+ страницы, на которые вы переходите, также возвращаются, поэтому необходимо удалить лишний шум.

Возвращенный JSON сохраняет все контакты, которые связаны под свойством data, поэтому мы анализируем JSON и получаем массив друзей из свойства items . Тогда нужно просто изучить контакты и построить новый контактный объект.

Получение контактов из Facebook

Facebook довольно похож, нам нужно запросить Graph API для пользователей и повторить их. Текущий код получает только первую страницу пользователей, следовательно, он немного ошибочен, но он должен быть легко адаптируемым для разбивки на страницы через полный список результатов. Обратите внимание, что Facebook будет возвращать только тех пользователей, которые вошли в приложение, поэтому в результате в списке могут появиться сотни ваших друзей, но вы все равно можете получить пустой список, если ни один из них не вошел в приложение.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
public ContactData[] getContacts() {
    ArrayList<ContactData> dat = new ArrayList<>();
    ConnectionRequest req = new ConnectionRequest() {
        @Override
        protected void readResponse(InputStream input) throws IOException {
            JSONParser parser = new JSONParser();
            Map<String, Object> parsed = parser.parseJSON(new InputStreamReader(input, "UTF-8"));
            //name = (String) parsed.get("name");
            java.util.List<Object> data = (java.util.List<Object>)parsed.get("data");
            for(Object current : data) {
                Map<String, Object> cMap = (Map<String, Object>)current;
                String name = (String)cMap.get("name");
                if(name == null) {
                    continue;
                }
                String id = cMap.get("id").toString();
                ContactData cd = new ContactData();
                cd.name = name;
                cd.uniqueId = id;
                cd.imageUrl = "http://graph.facebook.com/v2.4/" + id + "/picture";
                dat.add(cd);
            }
        }
    };
    req.setPost(false);
    if(token == null) {
        token = Preferences.get("facebooktoken", (String)null);
    }
    req.addArgumentNoEncoding("access_token", token);
    NetworkManager.getInstance().addToQueueAndWait(req);
 
    ContactData[] cd = new ContactData[dat.size()];
    dat.toArray(cd);
    return cd;
}

Это почти идентично версии Google, единственные различия заключаются в структуре возвращаемого JSON и URL.

Добавление вещей, которые нам нужны к теме

Чтобы следующий раздел работал, нам нужно добавить несколько мультиизображений в тему, используя Quick Add Multi Image в меню изображений в дизайнере, а затем выбрать «Очень высокое» в качестве разрешения исходного изображения. Это автоматически адаптирует изображение ко всем DPI.

Сначала нам нужен округленный-mask.png :

округленный-маска

Это изображение будет использоваться для преобразования квадратных изображений в круглые изображения, как мое изображение в области заголовка, которое вы видите выше. Эффективное маскирование использует черные пиксели для удаления соответствующих пикселей на изображении, оставляя белые пиксели, серые пиксели останутся полупрозрачными.

Тогда нам нужен округленный-border.png :

округлый границы

Мы разместим это изображение под изображением маски, что приведет к эффекту границы / тени. Это важно для внешнего вида, поскольку исходное изображение загружается из Facebook / Google и т. Д. И может иметь цветное столкновение с панелью инструментов. Таким образом, мы гарантируем, что граница будет соответствовать панели инструментов.

Наконец, мы добавляем изображение, которое будет помещено в область панели инструментов social-chat-tutorial-image-top.jpg :

Социально-чат-учебник-образ-топ

Теперь, когда все изображения на месте, нам нужно добавить некоторые UIID-ы в тему для поддержки дополнительных компонентов, которые мы будем использовать, сначала мы начнем с LargeIconFont LargeIconFont, который очень похож на IconFont IconFont, который мы ввели в прошлый раз, но это не перекрывает цвет переднего плана и имеет размер 4 мм. Этот шрифт значка используется в теле приложения, поэтому мы хотим, чтобы он использовал цвета по умолчанию, чтобы он правильно интегрировался, иначе белый передний план стандартного шрифта значка может исчезнуть.

чат-приложение-учебник-контакты форм-2

чат-приложение-учебник-контакты форм-3

Мы также добавим изображение с закругленными границами в качестве UIID, которое мы можем применить к реальному изображению позже, добавив UserImage UserImage и установив фоновое изображение в rounded-border.png и rounded-border.png масштабирование поведения фона. Нам также нужно установить прозрачность на 0, чтобы убедиться, что стиль прозрачен.

чат-приложение-учебник-контакты форм-4

чат-приложение-учебник-контакты форм-5

Мы также хотим сделать TitleArea более управляемым через непрозрачность. В настоящее время он определен как граница изображения, что является проблематичным, поэтому мы изменим границу на «Пусто», определим цвет фона на 5bc8fb и установим прозрачность на 255, что даст тот же эффект, но даст нам больше контроля в коде.

чат-приложение-учебник-контакты форм-6

чат-приложение-учебник-контакты форм-7

И, наконец, MultiButton's don't have a clickable state in the default theme so we need to update their selected state to have 255 transparency (opaque) and have the background color `5bc8fb который обеспечит нам эффект щелчка.

чат-приложение-учебник-контакты форм-8

Отображение контактов

Для отображения контактов нам понадобится несколько новых элементов пользовательского интерфейса и несколько новых переменных класса, поэтому сначала давайте определим новые члены класса, которые необходимы для этого:

1
2
3
4
5
6
7
private String fullName;
private String uniqueId;
private String imageURL;
private static EncodedImage userPlaceholder;
private EncodedImage roundPlaceholder;
private Image mask;
private ContactData[] contacts;

Эти члены определены в основном классе и включают изображения, которые мы будем использовать для отображения отдельных записей и создания маски «округления» для создания эффекта округления изображения в области строки заголовка. Мы также добавили переменную, содержащую массив контактов, который мы можем использовать позже в коде.

Для инициализации этих переменных мы будем использовать метод init(Object context) основного класса сразу после инициализации темы. Этот код происходит только один раз за выполнение приложения и является отличным местом для загрузки / инициализации. Обратите внимание, что этот метод все еще должен быть относительно быстрым, поэтому не выполняйте там интенсивную обработку, иначе запуск приложения может быть замедлен, а ОС может убить ваше приложение, не отвечающее на запросы.

1
2
3
4
5
6
7
8
9
Style iconFontStyle = UIManager.getInstance().getComponentStyle("LargeIconFont");
iconFontStyle.setBgTransparency(255);
FontImage fnt = FontImage.create(" \ue80f ", iconFontStyle);
userPlaceholder = fnt.toEncodedImage();
mask = theme.getImage("rounded-mask.png");
roundPlaceholder = EncodedImage.createFromImage(userPlaceholder.scaled(mask.getWidth(), mask.getHeight()).applyMask(mask.createMask()), false);
fullName = Preferences.get("fullName", null);
uniqueId = Preferences.get("uniqueId", null);
imageURL = Preferences.get("imageURL", null);

Здесь мы делаем несколько интересных вещей, определяем стиль значков для использования LargeIconFont LargeIconFont, который позволяет нам создавать масштабируемое изображение заполнителя. Затем он используется для двух целей в коде: заполнитель для изображений наших контактов (до загрузки изображения с помощью URLImage ) и «моя картинка» (в данный момент зарегистрированный пользователь) в области заголовка. В случае «моей картинки» оно будет округлено с использованием изображения с закругленной маской, упомянутого выше.

Эта строка довольно сложная, поэтому давайте разберем ее немного:

1
roundPlaceholder = EncodedImage.createFromImage(userPlaceholder.scaled(mask.getWidth(), mask.getHeight()).applyMask(mask.createMask()), false);

Здесь мы делаем несколько разных вещей: userPlaceholder.scaled(mask.getWidth(), mask.getHeight()) берет userPlaceholder и следит за тем, чтобы он идеально соответствовал размеру маски. Если этого не произойдет, мы получим исключение. Затем мы берем изображение маски mask.createMask() и конвертируем его во внутреннее представление. Вы можете вспомнить сверху, что изображение маски выглядит так:

округленный-маска

Таким createMask метод createMask извлекает эти пиксели и преобразует их во внутреннее представление, которое мы позже можем применить к произвольному изображению. Это немного дорогая операция, поэтому мы не рекомендуем делать это часто. Затем мы берем масштабированное изображение и применяем вновь созданную маску к масштабированному изображению, которое генерирует круглое изображение с вызовом applyMask .

Однако нам нужен экземпляр EncodedImage, а не просто обычное изображение, поэтому мы используем EncodedImage.createFromImage с аргументом false (указывающим, что изображение не является непрозрачным), чтобы преобразовать результирующий заполнитель в кодированное изображение. Обратите внимание, что если изображение уже закодировано, этот метод ничего не делает …

Нам нужно закодированное изображение, так как позже в коде мы будем использовать URLImage который ожидает EncodedImage , EncodedImage как правило, является более эффективным способом хранения изображений с точки зрения оперативной памяти и позволяет нам более эффективно получать данные изображения. Это означает, что данные PNG / JPEG изображения все еще доступны … Все стандартные / мультиизображения, возвращаемые из файла ресурсов, представляют собой `EncodedImage ‘, который помогает с использованием памяти.

Теперь, когда все готово, мы можем начать с основного метода, который показывает пользовательский интерфейс:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void showContactsForm(UserData data) {
    Form contactsForm = new Form("Contacts");
    contactsForm.setLayout(new BoxLayout(BoxLayout.Y_AXIS));
 
    // the toolbar is created into a layer on the content pane. This allows us to render behind it and leave it semi transparent
    Toolbar tb = new Toolbar(true);
 
    // we want the title area to be transparent so it won't get in the way
    contactsForm.getTitleArea().setUIID("Container");
 
    // folds the toolbar automatically as we scroll down, shows it if we scroll back up
    tb.setScrollOffUponContentPane(true);
    contactsForm.setToolBar(tb);
 
    // we want the image behind the toolbar to stretch and fit the entire screen and leave no margin
    Label titleLabel = new Label(" ");
    Style titleLabelStyle = titleLabel.getUnselectedStyle();
    titleLabelStyle.setBgImage(theme.getImage("social-chat-tutorial-image-top.jpg"));
    titleLabelStyle.setBackgroundType(Style.BACKGROUND_IMAGE_SCALED_FILL);
    titleLabelStyle.setPadding(tb.getPreferredH(), tb.getPreferredH(), tb.getPreferredH(), tb.getPreferredH());
    titleLabelStyle.setPaddingUnit(Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS);
    titleLabelStyle.setMargin(0, 0, 0, 0);
 
    contactsForm.addComponent(titleLabel);
 
    // the behavior of the title is rather complex so we extract it to a separate method
    tb.setTitleComponent(createTitleComponent(contactsForm));
 
    InfiniteProgress ip = new InfiniteProgress();
    contactsForm.addComponent(ip);
 
    loadContacts(data, ip, contactsForm.getContentPane());
 
    // creates the morph and other animations from the main form to the second form of the app
    createMorphEffect(titleLabel);
 
    contactsForm.show();
}

Это относительно большой метод, но он делегирует большую часть тяжелой работы другим методам, поэтому большая часть работы, выполняемой здесь, не очень сложна и даже не интересна. Вот:

1
contacts.getTitleArea().setUIID("Container");

Мы просто делаем область заголовка прозрачной (поскольку UIID контейнера всегда прозрачен), это позволяет нам довольно тщательно createTitleComponent панель инструментов в методе createTitleComponent .

Этот код:

1
2
3
4
5
6
7
Label titleLabel = new Label(" ");
Style titleLabelStyle = titleLabel.getUnselectedStyle();
titleLabelStyle.setBgImage(theme.getImage("social-chat-tutorial-image-top.jpg"));
titleLabelStyle.setBackgroundType(Style.BACKGROUND_IMAGE_SCALED_FILL);
titleLabelStyle.setPadding(tb.getPreferredH(), tb.getPreferredH(), tb.getPreferredH(), tb.getPreferredH());
titleLabelStyle.setPaddingUnit(Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS, Style.UNIT_TYPE_PIXELS);
titleLabelStyle.setMargin(0, 0, 0, 0);

Инициализирует изображение за заголовком:

Социально-чат-учебник-образ-топ

Обратите внимание, что мы стилизуем его в коде, так как мы хотим, чтобы он располагался очень определенным образом и увеличивал размер панели инструментов. Из-за этого мы используем отступ от панели инструментов, чтобы правильно установить размер. Обратите внимание, что мы явно указываем модуль заполнения, в противном случае некоторые платформы, для которых по умолчанию используется заполнение миллиметром, могут оказаться не в порядке.

Мы можем сделать этот вид эффекта несколькими различными способами, но этот специфический подход с опцией «масштабируется до заполнения» позволяет изображению хорошо адаптироваться к ориентации устройства без потери пропорциональности.

В этом блоке используется несколько методов, мы рассмотрим их от самых простых до самых сложных:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
private MultiButton createContactComponent(ContactData d) {
    MultiButton mb = new MultiButton();
    mb.putClientProperty("uid", d.uniqueId);
    mb.setTextLine1(d.name);
    if(d.imageUrl != null) {
        mb.setIcon(URLImage.createToStorage(userPlaceholder, "userPic" + d.uniqueId, d.imageUrl, URLImage.RESIZE_SCALE_TO_FILL));
    } else {
        mb.setIcon(userPlaceholder);
    }
    mb.addActionListener((e) -> {
        showChatForm(d, mb);
    });
    return mb;
}

Запись в списке контактов — просто мультикнопка без особой забавы, мы помещаем uid как свойство клиента, используя вызов putClientProperty . Это эффективно позволяет нам помещать объекты в Map которая существует внутри Component таким образом, позже в коде, когда обрабатывается нажатие кнопки, мы можем вызвать mb.getClientProperty("uid"); и получите уникальный идентификатор, представленный кнопкой. Это отлично подходит для отделения логики приложения от пользовательского интерфейса.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private void createMorphEffect(Label titleLabel) {
    // animate the components out of the previous form if we are coming in from the login form
    Form parent = Display.getInstance().getCurrent();
    if(parent.getUIID().equals("MainForm")) {
        for(Component cmp : parent.getContentPane()) {
            cmp.setX(parent.getWidth());
        }
 
        // moves all the components outside of the content pane to the right while fading them out over 400ms
        parent.getContentPane().animateUnlayoutAndWait(400, 0);
        parent.getContentPane().removeAll();
 
        // we can't mutate a form into a component in another form so we need to convert the background to an image and then morph that
        // this is especially easy since we already removed all the components
        Label dummy = new Label();
        dummy.setShowEvenIfBlank(true);
        dummy.setUIID("Container");
        dummy.setUnselectedStyle(new Style(parent.getUnselectedStyle()));
        parent.setUIID("Form");
 
        // special case to remove status bar on iOS 7
        parent.getTitleArea().removeAll();
        parent.setLayout(new BorderLayout());
        parent.addComponent(BorderLayout.CENTER, dummy);
        parent.revalidate();
 
        // animate the main panel to the new location at the top title area of the screen
        dummy.setName("fullScreen");
        titleLabel.setName("titleImage");
        parent.setTransitionOutAnimator(MorphTransition.create(1100).morph("fullScreen", "titleImage"));
    }
}

Эффект морфа показывает несколько эффектов при выходе из основной формы. Обратите внимание, что он применяется только тогда, когда основной интерфейс является источником перехода, что имеет смысл в большинстве случаев. Например, мы будем иметь другой эффект при переходе в форму контактов из чата …

Здесь мы показываем два разных эффекта: сначала мы анимируем кнопки за пределами экрана, мы делаем это, помещая их в конечное положение (за пределами экрана справа), затем вызывая animateUnlayoutAndWait который является противоположностью animateLayoutAndWait . Он предназначен для показа анимации выхода, подобной этой, и эффективно возвращает компоненты в их правильные позиции макета, а затем анимирует их, а затем, возможно, затемняет их.

Как только компоненты исчезли, мы удалили их, а затем переместили изображение в одну большую метку и превратили UIID формы в стандартную форму. Это необходимо для морфного перехода, который может изменять только элементы, а не саму форму. Обычно этот «трюк» работает без проблем, но в iOS в верхней части формы есть StatusBar StatusBar, который толкает приложение вниз и позволяет нам видеть содержимое строки состояния (батарея и т. Д.). Небольшой взлом позволяет нам временно удалить этот компонент, если он существует, и предотвращает «отскок» при переходе.

Далее мы называем исходный / целевой компоненты для морфного перехода и устанавливаем его в родительскую форму. Вот и все, морфинговый переход выполняет всю работу по анимации компонентов друг в друге.

Область заголовка формы — это стандартная Toolbar , хотя вместо стандартного заголовка мы использовали специальный компонент, состоящий из слоев, для создания специального эффекта. Toolbar прежнему автоматически сворачивается при прокрутке и предоставляет все стандартные эффекты. Он также имеет классную функцию поиска, которую мы обсудим в следующем разделе.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private Component createTitleComponent(Form parent) {
    // we want the toolbar to be completely transparent, since we created it on the layered pane (using the true
    // argument in the constructor) it will flow in the UI
    parent.getToolbar().setUIID("Container");
 
    // we create 3 layers within the title, the region contains all the layers, the encspsulate includes the "me image"
    // which we want to protrude under the title area layer
    Container titleRegion = new Container(new LayeredLayout());
    Container titleEncapsulate = new Container(new BorderLayout());
    Container titleArea = new Container(new BorderLayout());
 
    // since the Toolbar is now transparent we assign the title area UIID to one of the layers within and the look
    // is preserved, we make it translucent though so we can see what's underneath
    titleArea.setUIID("TitleArea");
    titleArea.getUnselectedStyle().setBgTransparency(128);
 
    // We customize the title completely using a component, the "title" is just a label with the Title UIID
    Label title = new Label(parent.getTitle());
    title.setUIID("Title");
    titleArea.addComponent(BorderLayout.CENTER, title);
 
    // the search button allows us to search a large list of contacts rather easily
    Button search = createSearchButton(parent, title, titleArea, titleRegion);
 
    // we package everything in a container so we can replace the title area with a search button as needed
    Container cnt = new Container(new BoxLayout(BoxLayout.X_AXIS));
    titleArea.addComponent(BorderLayout.EAST, cnt);
    cnt.addComponent(search);
 
    // this is the Me picture that protrudes downwards. We use a placeholder which is then replace by the URLImage
    // with the actual image. Notice that createMaskAdapter allows us to not just scale the image but also apply
    // a mask to it...
    Label me = new Label(URLImage.createToStorage(roundPlaceholder, "userImage", imageURL, URLImage.createMaskAdapter(mask)));
    me.setUIID("UserImage");
 
    // the search icon and the "me" image are on two separate layers so we use a "dummy" component that we
    // place in the search container to space it to the side and leave room for the "me" image
    Label spacer = new Label(" ");
    Container.setSameWidth(spacer, me);
    cnt.addComponent(spacer);
 
    Container iconLayer = new Container(new BorderLayout());
    titleEncapsulate.addComponent(BorderLayout.NORTH, titleArea);
 
    titleRegion.addComponent(titleEncapsulate);
    titleRegion.addComponent(iconLayer);
    iconLayer.addComponent(BorderLayout.EAST, me);
 
    return titleRegion;
}

Кнопка поиска включает в себя множество функций, фактически она заменяет заголовок на TextField который позволяет нам вводить имя контакта. Крутая часть — это способ, которым мы фильтруем контакты, чтобы найти правильную запись, это делается с помощью прослушивателя изменения данных и динамического скрытия ненужных записей.

Вы можете увидеть эту анимацию в действии в этом коротком видео:

Все эти функции встроены непосредственно в код, который создает кнопку поиска:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
private Button createSearchButton(Form parent, Label title, Container titleArea, Container titleRegion) {
    // we want the search feature to be based on the title style so it will "fit" but we need it to use the font defined
    // by the icon font UIID so we merge both
    Style s = new Style(title.getUnselectedStyle());
    Style iconFontStyle = UIManager.getInstance().getComponentStyle("IconFont");
    s.setFont(iconFontStyle.getFont().derive(s.getFont().getHeight(), Font.STYLE_PLAIN));
    FontImage searchIcon = FontImage.create(" \ue806 ", s);
    FontImage cancelIcon = FontImage.create(" \ue81e ", s);
 
    // this is the actual search button, but we don't want it to have a border...
    Button search = new Button(searchIcon);
    search.setUIID("Label");
 
    // the search box will be placed in the title area so we can type right into it. We make it look like a title but
    // explicitly align it to the left for cases such as iOS where the title is centered by default
    TextField searchBox = new TextField();
    searchBox.setUIID("Title");
    searchBox.getUnselectedStyle().setAlignment(Component.LEFT);
    searchBox.getSelectedStyle().setAlignment(Component.LEFT);
 
    // the data change listener allows us to animate the data on every key press into the field
    searchBox.addDataChangeListener((type, index) -> {
        String text = searchBox.getText().toLowerCase();
        if(text.length() > 0) {
            Dimension hidden = new Dimension(0, 0);
            // iterates over the components, if a component matches its set to visible and its size is kept as default
            // otherwise the component is hidden and set to occupy no space.
            for(Component cmp : parent.getContentPane()) {
                if(cmp instanceof MultiButton) {
                    String l1 = ((MultiButton)cmp).getTextLine1();
                    if(l1.toLowerCase().indexOf(text) > -1) {
                        cmp.setPreferredSize(null);
                        cmp.setVisible(true);
                    } else {
                        cmp.setPreferredSize(hidden);
                        cmp.setVisible(false);
                    }
                }
            }
        } else {
            // no search string, show all the components by resetting the preferred size to default (thru null) and making them visible
            for(Component cmp : parent.getContentPane()) {
                cmp.setPreferredSize(null);
                cmp.setVisible(true);
            }
        }
 
        // update the UI with an animation effect
        parent.getContentPane().animateLayout(200);
    });
 
    // the action event is invoked when the button is pressed, this can have 2 separate states: during search/before search
    search.addActionListener((e) -> {
        if(search.getIcon() == searchIcon) {
            // Starts the search operation by replacing the title with a text field and launching the native editing
            search.setIcon(cancelIcon);
            titleArea.replaceAndWait(title, searchBox, CommonTransitions.createCover(CommonTransitions.SLIDE_VERTICAL, true, 400));
            titleRegion.revalidate();
            Display.getInstance().editString(searchBox, searchBox.getMaxSize(), searchBox.getConstraint(), "");
        } else {
            // if we are currently searching then cancel the search, return all items to visible and restore everything
            search.setIcon(searchIcon);
            for(Component cmp : parent.getContentPane()) {
                cmp.setPreferredSize(null);
                cmp.setVisible(true);
            }
            parent.getContentPane().animateLayoutAndWait(200);
            search.setEnabled(true);
            search.setVisible(true);
            titleArea.replaceAndWait(searchBox, title, CommonTransitions.createCover(CommonTransitions.SLIDE_VERTICAL, true, 400));
        }
    });
    return search;
}

Это немного большой метод, но его функциональность относительно проста. В следующий раз мы обсудим интерфейс чата.

Другие сообщения в этой серии

Это непрерывная серия постов, включающая следующие части: