Русский
Русский
English
Статистика
Реклама

Дизайн мобильных приложений

Figma делаем дизайн компонентов, пригодный для экспорта в код

15.02.2021 08:08:38 | Автор: admin

В этой статье я расскажу, о том, как сделать в Фигме компонент, который будет пригоден для экспорта в код. В Фигме можно будет выбрать наш компонент и при помощи плагина сгенерировать готовый код.

Начнём с простого

Нарисуем лист вью с иконкой, и сгенерируем вёрстку.

Так выглядит примерная структура нашего элемента списка - слева иконка, и далее текст.

Такую структуру будет иметь наш компонент - вертикальный набор элементов, который мы придумали выше.

Так, со структурой разобрались, поняли что нам примерно нужно сделать, теперь приступаем непосредственно к дизайну. Для этого мы возьмём один элемент, и сделаем его на основе компонентов Фигмы и применим к нему Auto layout. Сначала объединим текст и иконку, добавим отступы, сделаем выравнивание по высоте в середине, и по левому краю. Получится так

Далее нам нужно создать два элемента, расположить их друг под другом по высоте, и объединить их Auto layout. В целом всё кажется готовым, но на самом деле, если вы поменяете длинну текста, то элементы не будут гибко подстраиваться друг под друга.

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

В результате у нас должен получиться такой вот список.

Запускаем генератор кода.

Открывается плагин с генерацией кода. где мы можем выбрать необходимую нам технологию. Я буду использовать Tailwind 2. Далее выберем нужный нам элемент дизайна, и плагин выдаст нам готовую вёрстку.

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

Так, всё работает, кроме иконок, которые нам нужно копировать как SVG и вставить в наш код. Делается это вот так

Заменяем наши иконки в вёрстке (я вставил прям в разметку, но вы можете и так как вам удобно - по url на пример.).

Получаем результат, который идентичен нашему в Фигме.

Подробнее про Auto layout тут.

Результат тут.

Сложнее. Рисуем карточку товара.

Нашей целью будет сделать так, чтобы при генерации кода, наша карточка была выполнена на основе display: flex; - CSS модели, для построения гибких контейнеров.

Я нарисовал макет, как в прошлом примере, сделал дизайн, распределил блоки, и при помощи Auto layout выровнял всё так, как мне нужно. Сгенерировал код, подправил некоторые нюансы с картинками и иконками, в результате получил готовую карточку товара. Подробнее про Flexbox тут.

Моя сгенерированная разметка доступна по ссылке ниже. Вы можете посмотреть и попробовать сами.https://play.tailwindcss.com/2VhmQJIJDl

Надеюсь вам пригодится моя статья, и верстать станет в разы легче. Но если нет, то прошу не минусовать статью, а помочь мне ее доработать до удобоваримого вида - оставляя свои комментарии.

Подробнее..

Зачем дизайнеру юрист или как не получить административный штраф за видео или стоковое фото на своем сайте

24.03.2021 00:15:52 | Автор: admin

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

По закону использовать изображение человека можно только по его письменному согласию и определенным в согласии образом (это написано в статье 152.1 Гражданского кодекса). Фотостудии, например, именно для этого берут у заказчиков согласие в договорах на использование фотографий в портфолио, потому, что без согласия - такое использование будет незаконным.

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

Через некоторое время присылает дизайнер новый макет с изображением уже другой девушки и сообщает, что фото скачано с сайта "стоковых" изображений, но они купили лицензию у сайта и теперь все законно. Очень хорошо, значит учатся. Прошу прислать договор с сайтом. Присылают - читаю, договор на одном листе, в котором никакой конкретики, только сумма за некую "стандартную" лицензию и ссылка на условия использования на отдельном сайте. Они их не читали. Не поленился прочитать условия на сайте, что входит в эту "стандартную" лицензию, читаю, так и есть - лицензия без ограничения для некоммерческого использования или, а для вот коммерческого только в наружной рекламе на аудиторию не более 500 000 человек. Перезваниваю дизайнеру, спрашиваю, где планируете это использовать и узнаю, что реклама будет размещаться на билбордах в Москве и Санкт-Петербурге, а также выйдет рекламой на телевидении. Уточнил у дизайнера читала ли она условия использования лицензии - не читала (а что надо было?). Макет рекламы не согласовал т. к. для такого объема нужно покупать расширенную лицензию и есть риски получить иск о запрете использования изображений и демонтаже уже размещенных рекламных конструкций.

Если человек обнародовал свою фотографию или видео в сети Интернет, то это также не дает право использовать это фото без его согласия.

С фото понятно, а если я хочу видео снять и разместить на сайте или в блоге?

Если Вы решили снять видео (фильм, рекламный ролик,) с музыкальным сопровождением и сюжетом помимо договора с актером(ами), нужно оформить с ними договор о предоставлении Вам прав на результаты творческого труда, которых Вы намерены использовать в фильме - с автором музыки, которая используется в ролике, с автором сценария и режиссером - при создании видео, или как его называет закон - аудиовизуального произведения и т. д. Интересующиеся могут почитать статью 1263 Гражданского кодекса.

Правда у этого правила тоже есть исключение - есть такие произведения, которые считаются "народным достоянием" и их можно использовать без получения согласия автора или его наследников. Но для того, чтобы произведение стало общественным достоянием нужно, чтобы прошло более 70 лет со дня смерти автора.

Однажды коллега попросил прислать форму договора с актером, который будет участвовать в съемках рекламного видеоролика. Договор я прислал и уточнил - нужны ли договоры с другими участниками этого процесса? С композитором? Исполнителем песен? Автором сценария? Где будет использоваться музыка?

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

Наш конкурент нагло копирует разработанные нами образы детских игрушек. Создает похожие и продает. Как его наказать и пресечь незаконное использование?

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

За нарушение этих требований можно получить административный штраф до 40 тыс. руб. с конфискацией контрафактных экземпляров произведений и фонограмм (см. 7.12 КоАП РФ) или лишение свободы сроком до 2 лет (статья 146 Уголовного кодекса РФ).

Практика показывает, что правообладатели весьма успешно защищают свои права и запрещают незаконное использование. ООО Смешарики, например, имеет более 500 судов с коммерсантами о взыскании компенсации за нарушение исключительных авторских прав на произведения изобразительного искусства рисунки: Лосяш, Нюша, Крош, Копатыч с ожидаемым исходом. В этом можно убедиться, если зайти на сайт арбитражного суда https://kad.arbitr.ru/ и вбить в поиск ООО Смешарики. Тем кому лень открывать и что-то искать можно почитать решения судов о том, как правообладатель наказывает рублем кондитеров: раз, два, три.

Кроме государева "ока" за соблюдением авторских прав бдят и видеосервисы и по заявлению правообладателей достаточно оперативно удаляют видео.

А как понять, что тот, с кем я заключаю договор - законный собственник прав на музыку или текст, а не шарлатан?

Если Вы хотите узнать, кто собственник помещения, здания или земельного участка - Вы обращаетесь к единому Государственному реестру недвижимости Росреестра. Если хотите узнать собственника товарного знака - Вы обращаетесь к сайту Роспатента. Какого-то единого реестра прав на музыку или текст песен нет. Поэтому проверку легальности продажи вам прав на произведение придется проводить самому с определенным допуском достоверности.

Для уже опубликованных произведений можно проверить дату когда была первая публикация музыки или текста (если она публиковалась) с указанием автора или текста опубликовать с указанием депонирование. Если произведение не публиковалось рекомендуется пользоваться услугой по "депонированию произведения", то есть фиксацию того, что вы "зафиксировали" в конкретную дату существование и обладание таким объектом. В случае спора это поможет доказать, что Вы первый правообладатель. Депонированием занимаются нотариусы, отдельные некоммерческие организации. Но риск купить контрафакт у мошенника в любом случае имеет место.

Ок, а кто с кем должен заключать договоры?

При создании фильма порядок оформления отличается от оформления прав на фото. Договор с правообладателями музыки, сценария заключает лицо, которое организовывает создание фильма - продюсер. Это лицо занимается организацией создания фильма - приобретением прав на использование музыки в видео, права на использование сценария, на экранизацию(если необходимо), ищет актеров и заключает с ними договоры, заключает договор с режиссером, решает организационные вопросы. Кроме того, продюсер получает права на фильм в целом - это единственный "собственник" фильма, только он, как правило, впоследствии может распоряжаться видео, продавать его или сдавать в прокат. У остальных авторов фильма/видео - композитора, автора сценария, актеров, режиссеров и иных соавторов есть право быть указанными в качестве авторов и право на вознаграждение, но распоряжаться таким видео т. е. продавать права на него другим людям они не могут.

В этом смысле нет существенной разницы между полнометражным художественным фильмом, рекламным роликом и коротким видео в Youtube, Instagram или на ином сервисе. Регулирование рекламного ролика и художественного фильма одинаковое.

Наш класс школьному фотографу предпочел фотосъемку в фотостудии. В предыдущие годы, по разговорам, др. классы поступали аналогично, но сталкивались с тем, что директор отказывалась сниматься у сторонних фотографов. Собственно, вопрос. Наш родительский комитет предложит все же директору посетить выбранную ими фотостудию и сфотографироваться на альбом. Если фотограф из фотостудии возьмет фото директора с официального сайта школы и разместит в школьных альбомах, это будет законно? Приравнивается ли в этом случае директор к публичным людям вроде политиков и т.д.?

Директор школы не государственный служащий. А вот принадлежность его к политической деятельности понятие субъективное и определяется деятельностью самого директора, а не его должностью. Верховный суд по этому поводу сказал следующее: Без согласия гражданина обнародование и использование его изображения допустимо в силу подпункта 1 пункта 1 статьи 152.1 ГК РФ, то есть когда имеет место публичный интерес, в частности если такой гражданин является публичной фигурой (занимает государственную или муниципальную должность, играет существенную роль в общественной жизни в сфере политики, экономики, искусства, спорта или любой иной области), а обнародование и использование изображения осуществляется в связи с политической или общественной дискуссией или интерес к данному лицу является общественно значимым.Вместе с тем согласие необходимо, если единственной целью обнародования и использования изображения лица является удовлетворение обывательского интереса к его частной жизни либо извлечение прибыли.Не требуется согласия на обнародование и использование изображения гражданина, если оно необходимо в целях защиты правопорядка и государственной безопасности (например, в связи с розыском граждан, в том числе пропавших без вести либо являющихся участниками или очевидцами правонарушения).

Если я заключу договор с конкретным человеком или ЮЛ на создание звуковой композиции или изображения, например, для компьютерной игры, которую я делаю, а потом окажется, что они это украли, то нести ответственность мне?

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

Я - человек, который не любит фотографироваться. Я часто замечаю, как граждане снимают видео/фото в общественном месте. Если у меня есть все основания полагать, что я был запечатлен чужой камерой, имею ли я хоть какие-то права? Что реально может сделать человек, которого фотографируют без его на то согласия?

Если вы попали в кадр на улице случайно и вас не снимали специально и видео не о вас - то ничего противозаконного нет. Но если конкретно вас специально снимали на фото или видео - то вы можете требовать удаления этих фото или видео в суде. Если они появятся в соцсетях - требовать удаления таких материалов от самих соцсетей и т.п.

Я могу потребовать если я попал в кадр новостей чтобы меня не показывали?

В законе о СМИ прописаны требования для журналисткой деятельности и они такие же - журналист обязан получить ваше согласие кроме случаев если вас снимали вследствие обсуждения какого-либо вопроса - то есть если вас остановили на улице задали вопрос и вы ответили на него, то вы приняли участие в общественной дискуссии и тем самым согласились на съёмку. В целом у судов неоднозначный подход к этому вопросу. Вот что пишет Конституционный суд, например, "Кроме того, фотографии, на которых А.О. Безрукова позировала фотографу, были сделаны во время проведения публичного мероприятия с участием прессы, что могло свидетельствовать о наличии ее волеизъявления не только на фотосъемку, но и на дальнейшее обнародование и использование этих изображений в средствах массовой информации."

Размещенные изображения и текст на сайте целесообразно "защитить" знаком охраны авторства. (копирайт). Знак охраны авторского права состоит из латинской буквы "С" в окружности, наименования объекта защиты права, имени правообладателя и цифрового обозначения года первого опубликования произведения (без слова "год" или сокращения "г."). От копирования он, сам по себе не спасает, но может быть полезен при доказывании своего авторства. Если кому интересно, то правилам написания копирайта посвящен целый ГОСТ Р 7.0.1-2003.

Спасибо, что дочитали до конца. Если у Вас остались вопросы - Вы можете задавать их в комментариях и прислать по почте по адресу p_slizky@mail.ru .

Подробнее..

Connected! Самое главное о дизайне VPN-приложения

10.02.2021 18:20:17 | Автор: admin

Разбираемся, как сделать удобный и рабочий сервис при минимальных затратах на разработку.

Так получилось, что VPN-клиенты я делал намного чаще, чем пользовался ими по прямому назначению. В 2020 году почти каждый месяц я стабильно получал предложение о создании быстрого, удобного и безопасного. Одни предлагали два сервера, оба из которых платные. Другие давали выбор из 50 стран и возможность подключаться сразу к 4. Несмотря на разный подход, у этих приложений одинаковые паттерны.

Ниже я постарался описать все самое важное, не заостряя внимание на деталях.


Privacy, разрешения

Стандартная процедура, без которой мы не можем предоставить свои услуги. Даем ее сразу после Splash screen. Чем быстрее пользователь разберется с процедурой и забудет о ней, тем раньше он воспользуется приложением. Поэтому мы делаем привычный Agree внизу экрана и нативный Allow, который отпушит в настройки и вернет назад.

Я также слышал, что Privacy принимается пользователем в момент скачивания и все эти экраны не нужны. Тем не менее, VPN-приложений без них я еще не видел.

Кнопка

Большая, заметная. Пользователь пришел к вам, чтобы нажать именно на нее. Креативные решения взамен пользовательского опыта придется окупать очень выгодными условиями встроенной покупки. В остальном, стоит ее разметить не в центре, проигнорировать акценты, не выделить и все человек тормозит, закрывает и переходит в то приложение, которое с ним на Ты. Лучше сделать стандартный круг (часто) или кнопку (редко) снизу, учесть и показать состояния, дать возможность отмены подключения.

Статус подключения. С одной стороны, это элементарно и понятно. Если подключено, напиши подключено, выдели цветом кнопку. Однако, я часто натыкался на приложения, которые отображают статус индикатором сбоку или интересной иллюстрацией, смысл которой известен только дизайнеру и заказчику. Помните, что далеко не все power users.

Одно слово и противоположный цвет. Все довольны, всем понятно.

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

Список серверов

Переходим к нему только сейчас, так как, по большому счету, это уже другой экран. Делаем либо обычный ModalVC, который временами маскируется под вью (поверх кнопки), либо отдельный ViewController (другой экран). Зависит от желания и предпочтений.

Структурируем список серверов по качеству соединения, количеству доступных мегабайт, алфавиту или другому удобному показателю. Если стран 20+, имеет смысл добавить строку поиска. Можно отобразить качество соединения для пользователя, но судя по наблюдениям, это опционально. А вот показать, хотите ли Вы денег конкретно за этот сервер, очень желательно. Людям не нравится, когда их удивляют неожиданный переходом на экран встроенной покупки.

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

Время подключения.Встречал почти так же редко, как и информацию о скорости, однако в отличие от нее, практического применения таймеру я так и не нашел. Ни личный опыт, ни опросы, ни диалог с коллегами не дали мне ответ на вопрос: Эм, я подключен уже 3 минуты. Что дальше?. Жить это не мешает, но и толка от этого нет. Расскажите, что Вы думаете на этот счет, мне интересно.

Карта

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

Встроенная покупка

Этот экран есть везде. Про него нечего сказать в контексте этой статьи. Скорее всего, будет отдельный материал, посвящённый исключительно ему. С более подробной статистикой, исследованием и цифрами.

Другое

Настройки, поддержка, Q&A, восстановление покупок и далее по списку. Все, что сделает жизнь пользователя немного легче. Встречается в 90% VPN-приложений. Остальные 10% вполне обходятся без этого.

Пожалуйста

Не грузите пользователя всеми возможными разрешениями, особенно теми, что вообще не относятся к VPN. Не делайте экран подписки первым, особенно, если его нельзя закрыть. Если можно без обязательной регистрации, сделайте без нее. Старайтесь объяснить пользователю свои функции грамотным дизайном, а не постоянными всплывающими пояснениями. Учитывайте пользовательский опыт платформы.

Проигнорировав это, Вы рискуете, в лучшем случае, быть удаленным спустя 15 секунд после запуска, а в худшем получить отзывы с оценкой 2.

В заключение

Это основное, что важно знать про дизайн VPN-приложения. Мы не учитываем красивый фирменный стиль, правильные анимации и приветливый UI. Это тоже важно, и для многих пользователей может стать определяющим в выборе. У нас Starter pack. Реализовав этот список, приложение можно отправлять в App Store и Google Play.

Подробнее..

Нужно ли дизайнеру интерфейсов понимать вёрстку?

13.02.2021 10:18:35 | Автор: admin

Вы верно поняли, тут речь пойдёт именно о тех людях, которые делают дизайн интерфейсов. Я порой вижу вопросы на тему: Нужно ли понимать дизайнеру вёрстку? и Почему вы делите дизайнеров на Ui, UX, итд?. В этой статье я отвечу на эти вопросы. Маленькая затравка для продолжения - да, эти два вопроса отвечают друг на друга. О том, кто такие дизайнеры интерфейсов, и что они делают в рамках разработки приложений, мы разбирали в прошлой статье.

UPD: Расшифровка того, кто такой Ui/Ux дизайнер по ссылке выше.

Нужно ли уметь верстать?

Для ответа на этот вопрос мы пойдём от обратного. Возьмём дизайнера, который делает интерфейсы и не понимает, как они работают, разрабатываются и из чего они состоят. Этому дизайнеру на вход поступает задача сделать страницу, которая должна уверенно адаптироваться под различные устройства. Он начинает собирать данные, рисовать макет, адаптировать его под разные размеры - и тут-то проявляется первый звоночек о том, что интерфейс будет непригодным к разработке, по причине того, что дизайнер не понимает принципов адаптивности в вебе, которые строятся на различных подходах к вёрстке. Он на выходе выдаёт макет, который выглядит отлично, но структурно адаптируется неверно. Компоненты принимают вид смешанных элементов - по типу выпадающего списка, с функцией поиска по списку и создания элемента списка прямо в списке. Помимо этого макет оказывается неполным, без содержания активных состояний компонентов, каждый компонент не имеет системной структуры, которая позволяет повторять его во многих местах со смежным контекстом.

Пример:
Строка ввода текста, которая используется в одном месте макета, в другом, при схожем контексте выглядит уже иначе. А если контекст немного отличается - то наша строка ввода текста уже принимает третий вид. В идеале, конечно, строка должна быть спроектирована как разные состояния компонента - строка с плейсхолдером, с введенным текстом, с иконкой слева, с иконкой справа, и так далее

Что делать дизайнеру который не понимает вёрстку? - тут ответ очевиден Нужно разбираться в том, что ты делаешь. Это вопрос компетентности, и ответственности за свои решения, которые должны иметь твердое основание.

Тут кроется маленькая хитрость для людей, которые могут сказать - я не понимаю верстку, но всё равно рисую сайты, и мне нормально. Ответ - люди, которым вы рисуете сайты не знают, каким он должен быть на самом деле, и принимают ваше решение как верное. Я понимаю это как обман клиента, который доверился вам

Почему делят дизайнеров на Ui, Ux, продуктовых, рекламных и так далее ?

Если вы читали ответ на предыдущий вопрос, то вы примерно уже догадываетесь о том, почему дизайнеры интерфейсов, это не тоже самое, что дизайнеры печатной продукции, и другие История IT такова, что постоянно нужно развиваться и учиться новому, без этого есть большой шанс стать неактуальным. На заре зарождения веб интерфейсов дизайном занимались люди, которые сайты разрабатывали, была даже такая профессия Веб мастер. Сейчас, если делать так, как делали в начале нулевых - то дизайнер рискует остаться не у дел. Веб мастера переквалифицировались в фронтендеров, с их большим зоопарком фреймворков, технологий по типу Blazor, и и много чего еще Сейчас веб разработчику, для успешной карьеры, мало знать лишь ванильный JS и JQuery. Так и выводим, что дизайнер, который разрабатывает интерфейсы, знает помимо обязательной для всех видов дизайнеров базы - Теории дизайна, цвета, композиции еще и технические детали в виде Системного подхода, верстки, принципов работы клиент-серверных приложений, принципов работы запросов и ответов, структуры JSON объектов. Справедливо будет уточнить, что не очень нужно знать всё это на уровне профессиональной разработки, а хотя бы на уровне базовых принципов и механизмов работы. Этим собственно и делятся дизайнеры между собой - дополнительными, отраслевыми навыками.

Выводы

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

Обращайтесь только к квалифицированным специалистам.

Подробнее..

UX-исследование какие решения помогают зарабатывать миллионы приложениям для стриминга видео в прямом эфире

25.02.2021 16:13:17 | Автор: admin

Сегодня, я познакомлю вас с несколькими приложениями для стриминга видео в прямом эфире и расскажу, за счет чего им удается удерживать внимание огромной аудитории и успешно монетизировать ее.

Про культурный шок

Привыкший к простым и понятным интерфейсам, я не сразу понял что вообще происходит, стоило только открыть одно из этих приложений. Все свистело, пердело, переливалось цветами радуги, разрывалось салютами, мигало, прыгало, дрожало и что-то от меня хотело. А где-то под всем этим виднелась физиономия славянской девочки, которая несла какой-то бред о личном. Такое впечатление было при первом контакте с Bigo Live самым популярным приложением для онлайн-стримов.

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

Likee, LiveMe, Mico, Hakuna и другие приложения недалеко ушли от Bigo Live, предлагая практически идентичный интерфейс и сценарии взаимодействия, при этом все также, имея огромную аудиторию и внушительные цифры дохода.

Как же так получилось, что эти китайские до мозга костей приложения так привлекательны? И за счет каких решений им удается привлекать, удерживать и, главное, монетизировать огромную аудиторию? Давайте разберемся.

Про аудиторию и контент

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

Так вот, Bigo Live, Likee, LiveMe, Mico, Hakuna и подобные позволяют этим людям не только купаться в лучах славы 24/7, но еще и зарабатывать реальные деньги.

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

Возможность запустить собственную живую трансляцию сразу же после установки приложения и моментально получить сотни, тысячи зрителей привлекает множество новых пользователей.

А зрителей в этих приложениях действительного много: в течение минуты после начала собственной трансляции в Bigo Live, просто транслируя потолок офиса я легко собирал более 100-200 зрителей. И многие из них продолжали смотреть в мой потолок в течение нескольких минут, ожидая что вот-вот что-то произойдет (привет дофамину).

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

Естественно, эти приложения предлагают разные варианты трансляций: с видео или без, одиночные или групповые. Но особенно популярен формат PK (батлов). Это когда в рамках одной трансляции участвуют сразу два стримера, а сам батл сводится к тому, чтобы за ограниченное время получить как можно больше цифровых подарков. Кто получил больше тот и выиграл.

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

Наиболее старательные стримеры могут получать еще и зарплату при выполнении плана по количеству/длительности трансляций и объему собранных подарков.

И как вы уже догадались, основные зрители всех этих трансляций дети и подростки.

Про монетизацию

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

Вот основные из них:

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

  • интерфейсная мотивация приглашение отправить подарок после определенного времени просмотра трансляции.

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

  • мотивация от ведущего трансляции часто, сами стримеры активно просят отправлять им подарки, чтобы увеличить рейтинг трансляции, либо достичь какой-то цели и др.

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

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

  • социальный статус активное дарение подарков растит социальный статус пользователя внутри приложения. Такие пользователи особенно выделяются на фоне обычных.

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

Цифровые подарки основной, но не единственный способ монетизации. Многие приложения также предлагают покупку цифровых товаров (украшения профиля, эффекты входа) и особые VIP-статусы.

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

Про вовлечение

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

Автоматический запуск трансляций

Автоматически запускают случайные (из популярных) трансляции в первых нескольких сессиях пользователей, чтобы приучить взаимодействовать с ними и убрать любые возможные фрустрации выбора (это кстати одна из значительных проблем современного Youtube).

Барьеры при выходе из трансляции

При первых попытках выйти из трансляции не делают этого. Вместо закрытия трансляции, либо объясняют, что для перехода к другой трансляции достаточно свайпнуть вверх/вниз, либо предлагают сперва подписаться на ведущего или просто продолжить просмотр.

Легкий переход к следующей трансляции

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

Покупки без разрыва контекста

Возможность совершения покупок внутри приложения без необходимости прерывать текущее взаимодействие и переходить в раздел пополнения счета. Интерфейс пополнения счета отображается прямо поверх трансляции в ответ на попытку подарить подарок при недостаточном балансе:

Про удержание и возврат пользователей

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

Чекины

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

Ежедневные задания

Разработчики некоторых приложений пошли чуть дальше чекинов и предлагают также выполнение ежедневных заданий в обмен на виртуальную валюту и другие подарки.

Игры

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

Уровень профиля

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

Push-уведомления

Но один из самых главных инструментов возврата пуши. Их, совершенно не стесняясь, отправляют сотнями в день по любому поводу и без. Пуши буквально летят каждую минуту.

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

Про деньги

Если вы все еще думаете, что это какая-то дичь, то я с вами соглашусь. Однако это не просто дичь, это золотая дичь. iOS и Android версии Bigo Live, самого главного игрока на этом рынке, только за январь принесли суммарно 27 млн долларов по данным Sensortower. Это на 12 млн долларов больше чем у Tik-Tok, аудитория которого значительно превосходит Bigo Live.

Остальные игроки только набирают обороты, но уже делают по 1-3 млн долларов ежемесячно.

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

Постскриптум

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

Уже более 10 лет я помогаю разным бизнесам достигать большего за счет удобных пользовательских интерфейсов для любых цифровых продуктов. Если у вас есть какие-либо вопросы по UX напишите мне в ЛС. С удовольствием помогу советом.

Подробнее..

Как я навел порядок страниц вФигме

09.03.2021 08:15:06 | Автор: admin

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

Проблема

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

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

Она гибкая, наглядная и удобная с точки зрения ориентации в файле. И что самое важноеона очевидна для тех, кто заходит в файл впервые!

Решение

Применение системы на примере двух разных проектовПрименение системы на примере двух разных проектов

Система построена на разделении всех страниц на базовые типы. Каждый из типов имеет свой эмоджи для четкого визуального разделения:

  1. Продакшенитоговые макеты, готовые к разработке. Эмоджи:

Подробнее..

Адаптация таблиц под мобильные устройства

15.03.2021 10:06:02 | Автор: admin

Для кого эта статья

Почти все дизайнеры сталкиваются с проблемой адаптации при отрисовке таблиц в интерфейсах - а именно адаптации таблиц под мобильные устройства. Справедливо также будет отметить полезность статьи для Frontend разработчиков, которые эти самые таблицы верстают.

Проблема

Проблема у дизайнера начинается в тот момент, когда таблица из десктопной версии интерфейса адаптируется под мобильные устройства. Как мы знаем таблицы могут иметь много столбцов, что влечёт за собой вылеты талицы за ширину экрана на мобильном устройстве.

Данная проблема встречается повсеместно, и усугубить ее может пожалуй лишь контент ячейки, который не переносится построчно, увеличивая тем самым ширину колонки.

Подопытный набор данных

Для того чтобы понимать лучше суть проблемы, мы придумаем себе таблицу, и будем ее адаптировать.

Колонки:

  1. Номер

  2. Фото

  3. ФИО

  4. Телефон

  5. Email

  6. Дата

  7. Текст описание

  8. Статус

  9. Действия

Результатом станет довольно обширная таблица, которая имеет все нужные нам виды колонок. Признаться даже список сверху вызывает у меня вопросы справа тратится слишком много места.

  1. С фиксированной шириной и переносом строк

  2. С шириной по контенту

Анонс следующей статьи О списках в интерфейсах и как их применять по феншую.

Десктоп

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

Варианты адаптации

Проблема наша талица по ширине не влезает в телефон.

Ошибочные решения

  1. Уменьшать шрифт

  2. Убирать колонки

  3. Делать растровую картинку с таблицей и вставлять ее в макет

Возможные верные решения по убыванию

  1. Каждую строку таблицы делать блоком

  2. Горизонтальный скроллинг

Первый вариант адаптации - сделать каждую строку таблицу отдельным блоком, и вывести ее на экране телефона.

Проблема с данным методом: Удлиняется вертикальный скроллинг, данные повторяются. (частично решается добавлением фильтров для поиска)

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

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

Второй вариант - горизонтальный скролинг таблицы.

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

Субъективно для меня - это наименее подходящий вариант отображения таблиц в мобильных устройствах - так, как он скрывает большую часть контента за пределами окна устройства - что нарушает визуальную целостность данных контекста каждой строки таблицы (мы не видим строку целиком).

Вывод

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

Если вы заметили ошибки, или вам есть что дополнить - дайте мне знать, я обязательно это сделаю.Спасибо за внимание!

Подробнее..

Перевод MotionLayout RecyclerView красивые анимированные списки

05.04.2021 18:06:45 | Автор: admin

В этой статье я расскажу и покажу, как создавать красивые анимированные списки на основе RecyclerView и MotionLayout. Аналогичный метод я использовал в одном из своих проектов.

От переводчика: репозиторий автора статьи - https://github.com/mjmanaog/foodbuddy.
Я его форкнул, чтобы перевести. Возможно, кому-то "русская версия" подойдёт больше.

Что такое MotionLayout?

Если вкратце, то MotionLayout это подкласс ConstraintLayout, который позволяет с помощью XML описывать движения и анимацию расположенных на нём элементов. Подробнее в документации и вот здесь с примерами.

Итак, начнём.

Шаг 1: создадим новый проект

Назовём его, как душе угодно. В качестве активити выберем Empty Activity.

Шаг 2: добавим необходимые зависимости

В gradle-файл приложения добавим:

implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1'

И запустим синхронизацию (Sync Now в правом верхнем углу).

Шаг 3: создадим лэйаут

Наш будущий элемент списка будет выглядеть так:

Элемент списка RecyclerViewЭлемент списка RecyclerView

В папке res/layout создадим файл item_food.

Внутри он выглядит так
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools"    android:id="@+id/clMain"    android:layout_width="match_parent"    android:layout_height="wrap_content"    app:layoutDescription="@xml/item_food_scene">    <ImageView        android:id="@+id/ivFood"        android:layout_width="150dp"        android:layout_height="150dp"        android:layout_marginTop="8dp"        android:elevation="10dp"        android:scaleType="fitXY"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent"        app:srcCompat="@drawable/img_salmon_salad" />    <androidx.cardview.widget.CardView        android:id="@+id/cardView"        android:layout_width="match_parent"        android:layout_height="150dp"        android:layout_marginStart="100dp"        android:layout_marginLeft="100dp"        android:layout_marginTop="16dp"        android:layout_marginEnd="16dp"        android:layout_marginRight="16dp"        android:layout_marginBottom="16dp"        app:cardCornerRadius="20dp"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent">        <androidx.constraintlayout.widget.ConstraintLayout            android:layout_width="match_parent"            android:layout_height="match_parent">            <TextView                android:id="@+id/tvTitle"                android:layout_width="0dp"                android:layout_height="wrap_content"                android:layout_marginStart="60dp"                android:layout_marginLeft="60dp"                android:layout_marginTop="24dp"                android:layout_marginEnd="8dp"                android:layout_marginRight="8dp"                android:textSize="18sp"                android:textStyle="bold"                app:layout_constraintEnd_toEndOf="parent"                app:layout_constraintStart_toStartOf="parent"                app:layout_constraintTop_toTopOf="parent"                tools:text="Салат с лососем" />            <TextView                android:id="@+id/tvDescription"                android:layout_width="0dp"                android:layout_height="wrap_content"                android:layout_marginStart="60dp"                android:layout_marginLeft="60dp"                android:layout_marginEnd="16dp"                android:layout_marginRight="8dp"                android:ellipsize="end"                android:maxLines="3"                app:layout_constraintEnd_toEndOf="parent"                app:layout_constraintStart_toStartOf="parent"                app:layout_constraintTop_toBottomOf="@+id/tvTitle"                tools:text="Лосось с овощами  идеальное сочетание для полноценного питания. Простой рецепт блюда, которое содержит в себе богатый набор питательных веществ: белки, жиры и клетчатку." />            <TextView                android:id="@+id/tvCalories"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_marginStart="8dp"                android:layout_marginLeft="8dp"                android:layout_marginBottom="16dp"                android:textStyle="bold"                app:layout_constraintBottom_toBottomOf="parent"                app:layout_constraintStart_toEndOf="@+id/imageView6"                tools:text="80 ккал" />            <ImageView                android:id="@+id/imageView6"                android:layout_width="24dp"                android:layout_height="24dp"                android:layout_marginStart="60dp"                android:layout_marginLeft="60dp"                android:layout_marginBottom="16dp"                app:layout_constraintBottom_toBottomOf="parent"                app:layout_constraintStart_toStartOf="parent"                app:srcCompat="@drawable/ic_calories" />            <ImageView                android:id="@+id/imageView7"                android:layout_width="24dp"                android:layout_height="24dp"                android:layout_marginStart="24dp"                android:layout_marginLeft="24dp"                android:layout_marginBottom="16dp"                app:layout_constraintBottom_toBottomOf="parent"                app:layout_constraintStart_toEndOf="@+id/tvCalories"                app:srcCompat="@drawable/ic_star" />            <TextView                android:id="@+id/tvRate"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_marginStart="8dp"                android:layout_marginLeft="8dp"                android:layout_marginBottom="16dp"                app:layout_constraintBottom_toBottomOf="parent"                app:layout_constraintStart_toEndOf="@+id/imageView7"                tools:text="4.5" />        </androidx.constraintlayout.widget.ConstraintLayout>    </androidx.cardview.widget.CardView></androidx.constraintlayout.widget.ConstraintLayout>

Шаг 4: преобразуем ConstraintLayout в MotionLayout

Чтобы преобразовать ConstraintLayout в MotionLayout:

  • переключитесь в режим Split или Design;

  • в дереве компонентов (Component Tree) щёлкните правой кнопкой мыши на корневой элемент (в данном случае clMain);

  • в появившемся меню выберите Convert to MotionLayout.

Как преобразовать ConstraintLayout в MotionLayoutКак преобразовать ConstraintLayout в MotionLayout

Теперь мы можем работать с MotionLayout.

Содержимое файла item_food поменялось
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools"    android:id="@+id/clMain"    android:layout_width="match_parent"    android:layout_height="wrap_content"    app:layoutDescription="@xml/item_food_scene">    <ImageView        android:id="@+id/ivFood"        android:layout_width="150dp"        android:layout_height="150dp"        android:layout_marginTop="8dp"        android:elevation="10dp"        android:scaleType="fitXY"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent"        app:srcCompat="@drawable/img_salmon_salad" />    <androidx.cardview.widget.CardView        android:id="@+id/cardView"        android:layout_width="match_parent"        android:layout_height="150dp"        android:layout_marginStart="100dp"        android:layout_marginLeft="100dp"        android:layout_marginTop="16dp"        android:layout_marginEnd="16dp"        android:layout_marginRight="16dp"        android:layout_marginBottom="16dp"        app:cardCornerRadius="20dp"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent">        <androidx.constraintlayout.widget.ConstraintLayout            android:layout_width="match_parent"            android:layout_height="match_parent">            <TextView                android:id="@+id/tvTitle"                android:layout_width="0dp"                android:layout_height="wrap_content"                android:layout_marginStart="60dp"                android:layout_marginLeft="60dp"                android:layout_marginTop="24dp"                android:layout_marginEnd="8dp"                android:layout_marginRight="8dp"                android:textSize="18sp"                android:textStyle="bold"                app:layout_constraintEnd_toEndOf="parent"                app:layout_constraintStart_toStartOf="parent"                app:layout_constraintTop_toTopOf="parent"                tools:text="Салат с лососем" />            <TextView                android:id="@+id/tvDescription"                android:layout_width="0dp"                android:layout_height="wrap_content"                android:layout_marginStart="60dp"                android:layout_marginLeft="60dp"                android:layout_marginEnd="16dp"                android:layout_marginRight="8dp"                android:ellipsize="end"                android:maxLines="3"                app:layout_constraintEnd_toEndOf="parent"                app:layout_constraintStart_toStartOf="parent"                app:layout_constraintTop_toBottomOf="@+id/tvTitle"                tools:text="Лосось с овощами  идеальное сочетание для полноценного питания. Простой рецепт блюда, которое содержит в себе богатый набор питательных веществ: белки, жиры и клетчатку." />            <TextView                android:id="@+id/tvCalories"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_marginStart="8dp"                android:layout_marginLeft="8dp"                android:layout_marginBottom="16dp"                android:textStyle="bold"                app:layout_constraintBottom_toBottomOf="parent"                app:layout_constraintStart_toEndOf="@+id/imageView6"                tools:text="80 ккал" />            <ImageView                android:id="@+id/imageView6"                android:layout_width="24dp"                android:layout_height="24dp"                android:layout_marginStart="60dp"                android:layout_marginLeft="60dp"                android:layout_marginBottom="16dp"                app:layout_constraintBottom_toBottomOf="parent"                app:layout_constraintStart_toStartOf="parent"                app:srcCompat="@drawable/ic_calories" />            <ImageView                android:id="@+id/imageView7"                android:layout_width="24dp"                android:layout_height="24dp"                android:layout_marginStart="24dp"                android:layout_marginLeft="24dp"                android:layout_marginBottom="16dp"                app:layout_constraintBottom_toBottomOf="parent"                app:layout_constraintStart_toEndOf="@+id/tvCalories"                app:srcCompat="@drawable/ic_star" />            <TextView                android:id="@+id/tvRate"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_marginStart="8dp"                android:layout_marginLeft="8dp"                android:layout_marginBottom="16dp"                app:layout_constraintBottom_toBottomOf="parent"                app:layout_constraintStart_toEndOf="@+id/imageView7"                tools:text="4.5" />        </androidx.constraintlayout.widget.ConstraintLayout>    </androidx.cardview.widget.CardView></androidx.constraintlayout.motion.widget.MotionLayout>

В папке res Студия создала папку xml и положила в неё файл item_food_scene.xml:

Студия предупреждает (Warnings в нижней части экрана), что у элементов ImageView не заполнен тег contentDescription. Можете проигнорировать эти сообщения, а можете добавить в XML-разметке соответствующие теги (для чего они нужны, читайте здесь).

Шаг 5: добавим анимацию на ImageView

  1. В дереве элементов выберите ivFood (ImageView с основной картинкой);

  2. В редакторе MotionLayout выберите end;

  3. У выделенного элемента ivFood выделите правую (End) опорную точку и перетащите её за правую (End) границу родительского элемента;

  4. Картинка должна встать по центру родительского элемента;

  5. Поменяйте значение атрибутов layout_height и layout_width на 300dp.

От переводчика: начальное состояние ImageView (его положение, ширина и высота) осталось без изменений, а его конечное состояние изменилось: он встанет по центру и увеличится в размере в два раза (с 150dp до 300dp).

Шаг 6: посмотрим, что получилось

Чтобы воспроизвести анимацию, которую мы только что настроили:

  1. В редакторе MotionLayout выделите толстую стрелку, которая соединяет прямоугольники с надписями start и end;

  2. В редакторе ниже станет доступным блок Transition;

  3. Нажмите кнопку Play, чтобы воспроизвести анимацию.

Шаг 7: добавим анимацию на CardView

Порядок действий схож:

  1. В дереве компонентов выделите cardView (constraintView с заголовком, описанием, калорийностью и оценкой);

  2. В редакторе MotionLayout выберите end;

  3. Выделите cardView в появившемся разделе ConstraintSet;

  4. В разделе атрибутов элемента перейдите к группе Transforms;

  5. Поменяйте значение атрибута alpha на 0.

От переводчика: у карточки с описанием блюда конечное состояние (end) от начального (start) отличается только значением параметра alpha. В конечном состоянии она будет скрыта (и скрываться она будет плавно).

Шаг 8: добавим обработчик нажатий

Чтобы анимация включалась, надо настроить обработчик нажатий:

  1. В разделе атрибутов под OnClick добавьте новое поле (кнопка +);

  2. В параметра targetId выберите значение ivFood;

  3. Добавьте ещё одно поле;

  4. Для параметра ClickAction выберите значение toggle.

В результате получится такая анимация:

Шаг 9: добавим RecyclerView в activity_main.xml

<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:layout_height="match_parent">    <androidx.recyclerview.widget.RecyclerView        android:id="@+id/rvMain"        android:layout_width="match_parent"        android:layout_height="match_parent"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout>

Шаг 10: создадим класс и фиктивные данные для примера

package com.mjmanaog.foodbuddy.data.modelimport com.mjmanaog.foodbuddy.Rdata class FoodModel(        val title: String,        val description: String,        val calories: String,        val rate: String,        val imgId: Int)val foodDummyData: ArrayList<FoodModel> = arrayListOf(        FoodModel(                "Салат с лососем",                "Лосось с овощами  идеальное сочетание для полноценного питания. Простой рецепт блюда, которое содержит в себе богатый набор питательных веществ: белки, жиры и клетчатку.",                "80 ккал",                "4.5",                R.drawable.img_salmon_salad        ),        FoodModel(                "Куриная грудка-барбекю",                "От курочки, приготовленной на гриле или запечённой в духовке все всегда в восторге, если только она не сухая или пережаренная.",                "80 ккал",                "4.5",                R.drawable.img_chicken        ),        FoodModel(                "Курица с рисом на пару",                "Приготовление на пару  здоровый метод приготовления пищи. Он сохраняет её аромат, нежность и полезные вещества. К тому же для приготовления блюд не используется масло.",                "80 ккал",                "4.5",                R.drawable.img_chicken_rice        ),        FoodModel(                "Салат Цезарь",                "Зелёный салат из листьев салата ромэн и гренок, заправленный лимонным соком (или соком лайма), оливковым маслом, яйцом, Вустерширским соусом, анчоусами, чесноком, дижонской горчицей, сыром Пармезан и чёрным перцем.",                "80 ккал",                "4.5",                R.drawable.img_salad        ),        FoodModel(                "Просто полезная еда",                "Лосось с овощами  идеальное сочетание для полноценного питания. Простой рецепт блюда, которое содержит в себе богатый набор питательных веществ: белки, жиры и клетчатку.",                "80 ккал",                "4.5",                R.drawable.img_healthy        ))

Шаг 11: создадим адаптер и ViewHolder

Тут ничего экзотического нет. Используем нашу FoodModel

Шаг 12: заполним RecyclerView элементами

class MainActivity : AppCompatActivity() {    private var foodAdapter: FoodAdapter = FoodAdapter()    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)        rvMain.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)        rvMain.adapter = foodAdapter        foodAdapter.addAll(foodDummyData)    }}

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

GIF из статьи не стал добавлять, потому что

Она вести 11 Мб.

Ещё кое-что

В файле item_food_scene.xml содержится описание анимации, которую мы настроили. Никто не мешает вам создавать и редактировать анимации в файлах сцены вручную.

Надеюсь, материал из этой статьи кому-то окажется полезным. Будет круто, если вы узнаете из неё что-то новое.

Спасибо за внимание.

Подробнее..

Дайджест интересных материалов для мобильного разработчика 388 (28 марта 4 апреля)

04.04.2021 12:12:21 | Автор: admin
В новой недельной подборке архитектурные паттерны и новая WWDC21, распознавание карт и 13 подвохов мобильного приложения, траты пользователей, тестирование иконок и многое другое!



Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

Как меня Apple навечно забанил
Архитектурные паттерны в iOS: страх и ненависть в диаграммах. MV(X)
Compositional Layout: стоит ли игра свеч?
Почему мы не обновляли приложение ВКонтакте для iPad пять лет, а теперь обновили
Подключаем нагрудный датчик пульса по Bluetooth на Swift
Настало время офигительных историй[1/2]
Разрабатываем своего первого голосового ассистента на iOS
App Store отклоняет приложения, использующие сторонние SDK, которые собирают пользовательские данные
WWDC21 пройдет онлайн с 7 по 11 июня
Как создавать виджеты с WidgetKit
7 эффективных ключевых слов для оптимизации вашего Swift-кода
Представляем Epoxy для iOS
Синглтон против внедрения зависимостей в Swift
Удаляем фон в изображениях на Swift с помощью Core ML
2 iOS-инструмента для обнаружения мертвого и клонированного кода
Как перенести Луну в вашу комнату с помощью ARKit
Три типа дыр в безопасности, которые я вижу во многих iOS-приложениях
SwiftUI Animations: анимации на SwiftUI
ProgressHUD: анимированные иконки

Android

Доказательное программирование
CameraX+ML Kit для распознавания номера карты в действии
Google ограничивает, какие приложения могут видеть другие установленные приложения
Jetpack Activity Result API. Часть 2. Как работает под капотом
Google выпустил сканер документов Stack
Android Broadcast: как попасть на стажировку в Redmadrobot
Отладка скриптов сборки и плагинов Gradle [IntelliJ/Android Studio]
Самое простое руководство по пониманию Gradle!
Непустые списки в Kotlin
Более безопасный способ сбора потоков из пользовательских интерфейсов Android
Системный сбой в Android WebView: как разработчики могут избежать такой ошибки
Знакомимся с поведением ваших зависимостей
Запускаем ARM-приложения в эмуляторе Android
Реализация Snackbar для отмены действий в Jetpack Compose
Motion Layout: создание простой анимации Recycler View
Десять #AndroidLifeHacks, которые вы можете использовать прямо сейчас
LabeledSeekSlider: настраиваемый слайдер
Flux: погода на Jetpack Compose
KanbanBoard: канбан-доска на Kotlin

Разработка

13 подвохов мобильного приложения, о которых лучше знать до старта разработки
Осмысленные интерфейсы
TestOps: писать автотесты недостаточно
Какие вопросы ожидать на позицию автоматизатора и причем тут сортировка?
Дайджест релизов мобильной разработки Mail.ru Group за время пандемии
Storybook + Flutter = storybook_flutter
Паттерны и Методологии Автоматизации UI: Примеры из жизни
make sense: О карьерном росте до руководителя, необходимых навыках, лидерстве и доверии
Podlodka #208: операционные системы
GitHub обновил уведомления в приложении
Дизайн приложений: примеры для вдохновения #38
Google улучшает установку PWA
20 обязательных навыков для разработчиков 2021
CoScreen создает общую среду для разработки
Опыт 10,000+ экранов: 10 советов от ведущего продуктового дизайнера
Как мы разработали приложение за 300 тысяч и чуть не потеряли 4 млн рублей
Проектирование микро-взаимодействий в Figma с помощью интерактивных компонентов
Это начало конца PWA?
Бесшовная разработка мультиплатформенных приложений с Flutter
4 простых совета, чтобы стать более ценным разработчиком
6 основных различий между Junior и Senior разработчиком
Как мы ускорили нашу систему Continuous Integration на 50%
Как спланировать успех при запуске нового технического проекта
7 уроков моего пути от Junior-разработчика до Senior за 2 года
10 самых популярных вопросов на собеседовании по системному дизайну
ГОНКА к маркетинговому успеху
Инструменты для создания мобильных приложений с дополненной реальностью (AR)
Основы GitHub Actions
4 ошибки, которые я сделал как программист, но мне пришлось стать техническим директором, чтобы увидеть их
Разработка программного обеспечения игра проигравших
Как реализовать покупку подписок в приложении на Flutter
Доставка лучшего программного обеспечения быстрее: как мы сэкономили полмиллиона долларов
Чем мы можем делиться в Kotlin MultiPlatform: модули? данные? экраны?
Создайте свое приложение на Flutter за 5 дней

Аналитика, маркетинг и монетизация

Маркетологи в мобайле: Игорь Посталенко (Тинькофф)
Средний пользователь iPhone в США потратил в 2020 году на приложения $138
Траты пользователей на приложения и игры поставили новый рекорд в 1 квартале 2021
Прекращается работа Facebook Analytics
TechIntern: биржа IT студентов
A/B-тестирование иконок: опыт DEVGAME
Российский игровой рынок в 2020 году вырос на 35%
Lookout for Metrics от Amazon оценивает бизнес с помощью машинного обучения
Доверяете ли вы статистике от Google?
Яндекс попросил Samsung и других производителей не устанавливать неудаляемые приложения компании

AI, Устройства, IoT

Что такое IoT и что о нем следует знать
Microsoft поставит 120,000 HoloLens в армию
Snapchat готовит новые AR-очки Spectacles
IoT-устройства переведут на российский софт

Предыдущий дайджест. Если у вас есть другие интересные материалы или вы нашли ошибку пришлите, пожалуйста, в почту.
Подробнее..

Перевод Реализация Undo в Snackbar на Jetpack Compose

13.04.2021 18:11:50 | Автор: admin

Пользовательский опыт (UX User Experience) - это то, как пользователи воспринимают продукт и какие впечатления получают от взаимодействия с ним.

В процессе разработки мобильного приложения важно делать акцент на пользовательском опыте в ситуациях, когда от пользователя требуется подтверждение каких-либо действий (или, наоборот, отмена уже совершённого действия). Если приложение будет выводить AlertDialog по поводу и без, пользователю это вряд ли понравится.

В библиотеке компонентов материального дизайна есть Snackbar виджет, который используется для отображения сообщений в нижней части приложения.

Чем хорош снэкбар:

  • информирует пользователя о статусе процесса, который приложение выполнило или будет выполнять,

  • не прерывает взаимодействие пользователя с приложением,

  • может содержать дополнительную кнопку для программирования различных действий.

Используем Snackbar

В Jetpack Compose уже есть реализация снэкбара. В примере ниже он отображается после нажатия на кнопку:

@Composablefun SnackbarSample() {    val snackbarHostState = remember { SnackbarHostState() }    val coroutineScope = rememberCoroutineScope()    val modifier = Modifier    Box(modifier.fillMaxSize()) {        Button(onClick = {            coroutineScope.launch {                snackbarHostState.showSnackbar(message = "This is a Snackbar")            }        }) {            Text(text = "Click me!")        }        SnackbarHost(            hostState = snackbarHostState,            modifier = Modifier.align(Alignment.BottomCenter)        )    }}

В коде два основных компонента: SnackbarHost и SnackbarHostState. Их использование позволяет правильно отображать, скрывать и закрывать снэкбар в соответствии с гайдлайнами материального дизайна.

Единовременно может отображаться один снэкбар остальные будут ждать в очереди.

Добавляем Undo в Snackbar

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

Упрощённый пример:

@Composablefun HomeList(taskViewModel: ListViewModel = viewModel()) {    Scaffold {        val list by remember(taskViewModel) {            taskViewModel.taskList        }.collectAsState()        LazyColumn {            items(                items = list,                itemContent = { task ->                    ListItem(                        task = task,                        onCheckedChange = taskViewModel::onCheckedChange                    )                }            )        }    }}@Composableprivate fun ListItem(task: Task, onCheckedChange: (Task) -> Unit) {    Row(        modifier = Modifier            .fillMaxWidth()            .height(64.dp)            .padding(8.dp),        verticalAlignment = Alignment.CenterVertically    ) {        Checkbox(checked = task.isCompleted, onCheckedChange = { onCheckedChange(task) })        Spacer(Modifier.width(8.dp))        Text(text = task.title)    }}

Наша composable-функция получает на вход ViewModel, который, в свою очередь, подгружает список задач (viewModel.taskList) и вызывает функцию проверки их статуса (viewModel.onCheckedChange).

Scaffold используется для построения правильной структуры лэйаута. Далее по тексту станет понятно, как она будет связана со снэкбаром. А пока мы имеем такой результат:

Наше приложение должно реализовывать следующее бизнес-правило: в списке отображаются только незавершённые задачи. После нажатия на чекбокс, задача должна сразу же удаляться из списка. За реализацию этой логики отвечает ViewModel.

А теперь давайте реализуем функционал, который позволит пользователю отменять действие. Первым делом создадим лямбда-функцию, которая будет вызываться вместе со снэкбаром, когда пользователь нажимает на чекбокс.

@Composablefun HomeList(taskViewModel: ListViewModel = viewModel()) {    val coroutineScope = rememberCoroutineScope()    val scaffoldState = rememberScaffoldState()   val onShowSnackbar: (Task) -> Unit = { task ->        coroutineScope.launch {            val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(                message = "${task.title} completed",                actionLabel = "Undo"            )            when (snackbarResult) {                SnackbarResult.Dismissed -> Timber.d("Snackbar dismissed")                SnackbarResult.ActionPerformed -> taskViewModel.onCheckedChange(task)            }        }    }  ...}

Разберём код:

  • Строка #3: сохраняем CoroutineScope (потребуется позже при показе снэкбара)

  • Строка #4: сохраняем ScaffoldState, который содержит настроенный SnackbarHostState.

  • Строка #6: вызываем лямбда-функцию, когда пользователь нажимает на чекбокс. В качестве входного параметра она получает задачу, выбранную в списке.

  • Строка #8: вызываем suspend-функцию showSnackbar() и показываем снэкбар. Используя SnackbarResult, понимаем, какие произошли изменения (как изменилось состояние).

  • Строки #12-14: обрабатываем два возможных результата (Dismissed и ActionPerformed). Если кнопку не нажали, пишем сообщение в лог. Если кнопку нажали, ViewModel меняет значение boolean-переменной isCompleted на противоположное (с false на true). Когда значение переменной isCompleted опять будет false, задача вернётся в список.

Описание ViewModel
data class Task(val id: Long, val title: String, var isCompleted: Boolean)class ListViewModel : ViewModel() {    private val list = mutableListOf(        Task(1L, "Buy milk", false),        Task(2L, "Watch 'Call Me By Your Name'", false),        Task(3L, "Listen 'Local Natives'", false),        Task(4L, "Study about 'fakes instead of mocks'", false),        Task(5L, "Congratulate Rafael", false),        Task(6L, "Watch Kotlin YouTube Channel", false)    )    private val _taskList: MutableStateFlow<List<Task>> = MutableStateFlow(list)    val taskList: StateFlow<List<Task>>        get() = _taskList.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), listOf())    fun onCheckedChange(task: Task) {        list.find { it.id == task.id }?.isCompleted = task.isCompleted.not()        _taskList.value = list.filter { it.isCompleted.not() }    }}

Теперь мы можем связать наш снэкбар с Scaffold и вызывать лямбда-функцию в качестве параметра в методе onCheckedChange:

Scaffold(scaffoldState = scaffoldState) {    ...    ListItem(        task = task,        onCheckedChange = { task ->            taskViewModel.onCheckedChange(task)            onShowSnackbar(task)        }    )}

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

И ещё кое-что

В процессе разработки я столкнулся с тем, что после нажатия на чекбокс снэкбар появлялся и тут же исчезал. Решить её мне помог Адам Пауэлл на Kotlinlang в Slack.

Проблема заключалась в следующем: снэкбар удалялся из очереди, когда вызов showSnackbar отменялся. Изначально я объявлял rememberCoroutineScope в compose-функции ListItem, которая формировала элементы списка задач. Проблему решил перенос её объявления выше Scaffold.

Что дальше?

Полный исходный код для это статьи доступен в этом gist-репозитории (и в конце статьи). Не пинайте сильно за реализацию ViewModel. Она примитивна, да, и она здесь лишь для имитации "живых данных". В реальном приложении данные будут поступать, например, из Flow, связанного с Room.

Для своего приложения реализацию Undo в снэкбаре я добавил в этом пул-реквесте. Если есть желание, посмотрите.

Надеюсь, эта статья поможет вам создавать свои реализации отмены действий в снэкбарах. Спасибо за внимание, а Адаму Пауэллу за помощь с корутинами.

Полный исходный код
@Composablefun HomeList(taskViewModel: ListViewModel = viewModel()) {    val coroutineScope = rememberCoroutineScope()    val scaffoldState = rememberScaffoldState()    val onShowSnackbar: (Task) -> Unit = { task ->        coroutineScope.launch {            val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(                message = "${task.title} completed",                actionLabel = "Undo"            )            when (snackbarResult) {                SnackbarResult.Dismissed -> Timber.d("Snackbar dismissed")                SnackbarResult.ActionPerformed -> taskViewModel.onCheckedChange(task)            }        }    }    Scaffold(        scaffoldState = scaffoldState,        topBar = { HomeTopBar() }    ) {        val list by remember(taskViewModel) { taskViewModel.taskList }.collectAsState()        LazyColumn {            items(                items = list,                key = { it.id },                itemContent = { task ->                    ListItem(                        task = task,                        onCheckedChange = { task ->                            taskViewModel.onCheckedChange(task)                            onShowSnackbar(task)                        }                    )                }            )        }    }}@Composableprivate fun ListItem(task: Task, onCheckedChange: (Task) -> Unit) {    Row(        modifier = Modifier            .fillMaxWidth()            .height(64.dp)            .padding(8.dp),        verticalAlignment = Alignment.CenterVertically    ) {        Checkbox(            checked = task.isCompleted,            onCheckedChange = { onCheckedChange(task) }        )        Spacer(Modifier.width(8.dp))        Text(            text = task.title,            style = MaterialTheme.typography.body1,            overflow = TextOverflow.Ellipsis,            maxLines = 1        )    }}@Composableprivate fun HomeTopBar() {    TopAppBar {        Box(modifier = Modifier.fillMaxSize()) {            Text(                modifier = Modifier.align(Alignment.Center),                style = MaterialTheme.typography.h5,                text = "My tasks"            )        }    }}data class Task(val id: Long, val title: String, var isCompleted: Boolean)class ListViewModel : ViewModel() {    private val list = mutableListOf(        Task(1L, "Buy milk", false),        Task(2L, "Watch 'Call Me By Your Name'", false),        Task(3L, "Listen 'Local Natives'", false),        Task(4L, "Study about 'fakes instead of mocks'", false),        Task(5L, "Congratulate Rafael", false),        Task(6L, "Watch Kotlin YouTube Channel", false)    )    private val _taskList: MutableStateFlow<List<Task>> = MutableStateFlow(list)    val taskList: StateFlow<List<Task>>        get() = _taskList.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), listOf())    fun onCheckedChange(task: Task) {        list.find { it.id == task.id }?.isCompleted = task.isCompleted.not()        _taskList.value = list.filter { it.isCompleted.not() }    }}

От переводчика: комментарии и правки приветствуются.

Подробнее..

Обзор мобильного приложения Drive

22.03.2021 14:08:01 | Автор: admin
Ранее в нашем блоге мы рассказывали об on-premise решениях Zextras Team Pro и Zextras Drive, позволяющих создать корпоративное хранилище файлов, а также корпоративный групповой чат и систему для видеоконференций с большим количеством участников на базе Zimbra Open-Source Edition. Оба этих решения, помимо веб-клиента, можно использовать и в разработанных компанией Zextras мобильных приложениях Team и Drive, доступных для Android и iOS. Ранее мы публиковали обзор приложения Team, а в данной статье мы подробно разберем интерфейс и функциональность мобильного приложения Zextras Drive для iOS и Android.

image

Экран входа


Экран входа у обоих приложений полностью идентичен. Осуществить вход в мобильные приложения можно двумя способами. Первый ввести имя учетной записи, пароль, а также адрес сервера для входа, второй войти с помощью специального QR-кода, который можно сгенерировать в веб-клиенте Zimbra OSE.



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



Чтобы сгенерировать QR-код для входа в мобильное приложение, необходимо обновить Zextras Suite до версии не ниже 3.1.8 и развернуть расширение Zextras Auth. Делается это в командной строке при помощи следующих команд:

sudo su - zimbrazxsuite auth doDeployAuthZimletzmprov fc zimlet



Обращаем ваше внимание на то, что для корректной работы Zextras Auth и генерации QR-кодов требуется, чтобы в Zimbra были настроены параметры zimbraPublicServiceHostname, zimbraPublicServicePort и zimbraPublicServiceProtocol. В нашем случае эти настройки будут выглядеть следующим образом:

zmprov mcf zimbraPublicServiceHostnameexample.ruzmprov mcf zimbraPublicServiceProtocol httpszmprov mcf zimbraPublicServicePort 443



После этого в списке зимлетов в веб-клиенте Zimbra OSE появится Zextras Auth. Нажатие на него открывает окно, на котором отображаются пароли учетной записи. Изначально в списке нет паролей, для добавления нового пароля следует нажать на кнопку Новый Пароль. В открывшемся окне можно указать название приложения, в котором будет использоваться пароль, а также его тип: текст или QR-код.



Текстовые пароли используются для подключения приложений, работающих по протоколу Exchange ActiveSync. Возможность создания отдельного пароля для EAS присутствовала и раньше в расширении Zextras Mobile, теперь же эта функциональность полностью перенесена в Zextras Auth. Преимуществом Zextras Auth является то, что данное расширение позволяет пользователю создавать сразу несколько паролей для нескольких приложений без участия администратора.



QR-коды в настоящее время могут использоваться только для подключения мобильных приложений Zextras Drive и Zextras Team. Сгенерированный QR-код сгорает после первого же входа и не может использоваться для повторного входа.

Для того, чтобы войти по QR-коду, достаточно в мобильном приложении Team или Drive нажать на кнопку SCAN QR CODE и навести камеру на экран монитора с QR-кодом.

Мобильное приложение Zextras Drive


Мобильные приложения Drive для iOS и Android отличаются внешне, но при этом имеют абсолютно схожую функциональность. Они позволяют скачивать и загружать файлы в хранилище Zextras Drive и предоставлять доступ к ним другим пользователям. Также мобильные приложения Drive для iOS и Android позволяют управлять данными создавать папки, копировать, перемещать, переименовывать и удалять файлы в хранилище.

Администратор сервера Zimbra OSE, используя консоль администратора, может ограничивать те или иные функции пользователю Zextras Drive. В частности, он может:

  • Включить или выключить возможность для пользователя предоставлять доступ к своим файлам
  • Ограничить максимальный размер загружаемого файла
  • Ограничить время хранения версий файла
  • Ограничить число сохраняемых версий файла



iOS


Интерфейс приложения Zextras Drive на iOS состоит из пяти разделов: Делюсь я, Поделились со мной, Главная, Помечено и Корзина.

  • В разделе Делюсь я отображаются все файлы, к которым пользователь Zextras Drive предоставил доступ своим коллегам
  • В разделе Поделились со мной отображаются все файлы, к которым коллеги пользователя предоставили доступпользователю Zextras Drive
  • В разделе Главная отображаются все файлы, которые находятся в хранилище Drive пользователя
  • В разделе Помечено отображаются все файлы, которые были помечены пользователем
  • В разделе Корзина отображаются удаленные файлы



Долгое нажатие на файл приводит к появлению контекстного меню, в котором доступны следующие разделы:

  • Детали Отображает детали файла, такие как размер, владелец, последнее изменение, описание, публичная ссылка и список сотрудников, имеющих к нему доступ. В этом разделе можно создать публичную ссылку на файл
  • Открыть с помощью Позволяет открыть файл с помощью установленного на мобильном устройстве приложения
  • Переименовать Позволяет изменитьимя файла
  • Перенести Позволяет перенестифайл в указанную папку
  • Копировать Позволяет создать копию файла в указанной папке
  • Отмечено Позволяет пометить файл, чтобы он попал в раздел Помечено
  • Сохранить копию оффлайн Позволяет сохранить файл на устройство или в iCloud Drive
  • Добавить сотрудника Позволяет предоставить сотруднику доступ к файлу
  • Удалить Позволяет переместить файл в корзину
  • Восстановить Позволяет переместить файл из корзины.



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

  • Данные об учетной записи
  • Данные об использовании диска
  • Данные об использовании квоты почтового ящика
  • Свяжитесь с нами -Открывает форму обратной связи для отправки сообщения разработчикам приложения
  • Лицензия -Открываетсоглашение конечного пользователя
  • Сторонние лицензии Открывает список лицензий программных продуктов, использовавшихся при создании приложения
  • Файлы Открывает настройки скачивания файлов. Среди них скачивание файлов только по Wi-Fi, сжатие медиафайлов и очистка локального кэша.
  • Тема Позволяет переключаться между светлой и темной темой
  • Выход Выход из учетной записи
  • Версия Отображает текущую версию приложения.



В разделе Главное присутствуютдве дополнительные кнопки. Первая из них, в форме облака, открывает менеджер загрузок, в котором отображаются загруженныефайлы. Вторая кнопка открывает диалог загрузки новых файлов в Drive и управления хранящимися в нем файлами. В открывающемся диалоге присутствуют следующие разделы:

  • Редактировать Позволяет выбрать несколько файлов, чтобы переносить одновременно перенести или копировать в другую папку, либо одновременно пометить их или удалить
  • Создать папку Позволяет создать новую папку в выбранной локации
  • Включить камеру Запускает камеру, а сделанный снимок или видеоролик сохраняет в Drive
  • Выбрать из галереи Позволяет загружать фотографии и видеоролики из галереи устройства
  • Выбрать из документов -Позволяет загружать фотографии и видеоролики из iCloud Drive
  • Сканирование документа Позволяет при помощи камеры создать скан бумажного документа.



Android


Интерфейс приложения Zextras Drive для Android также состоит из пяти разделов: Все файлы, Делюсь я, Поделились со мной, Помечено и Корзина. Строку с разделами можно прокручивать влево и вправо.

  • В разделе Все файлы отображаются все файлы, которые находятся в хранилище Drive пользователя.В разделе Делюсь я отображаются все файлы, к которым пользователь Zextras Drive предоставил доступ своим коллегам
  • В разделе Поделились со мной отображаются все файлы, к которым коллеги пользователя предоставили доступпользователю Zextras Drive
  • В разделе Помечено отображаются все файлы, которые были помечены пользователем
  • В разделе Корзина отображаются удаленные файлы

В правом верхнем углу каждого раздела находится кнопка поиска, благодаря которой можно искать файлы в каждом из разделов. Кнопка "+" в нижнем правом углу экрана позволяет создать новую папку, либо загрузить новый файл.



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



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



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



В верхнем левом углу окна приложения находится кнопка Параметры, которая вызывает боковое меню с настройками приложения. Здесь также присутствует несколько разделов:

  • Данные об учетной записи
  • Настройки файлов -Открывает настройки скачивания файлов. Среди них скачивание файлов только по Wi-Fi, сжатие медиафайлов и очистка локального кэша
  • Настройки темы -Позволяет переключаться между светлой и темной темой
  • Свяжитесь с нами -Открывает форму обратной связи для отправки сообщения разработчикам приложения
  • Лицензия -Открываетсоглашение конечного пользователя
  • Сторонние лицензии Открывает список лицензий программных продуктов, использовавшихся при создании приложения



По всем вопросам, связанными c Zextras Suite Pro и Team Pro вы можете обратиться к Представителю компании Zextras Technology Екатерине Триандафилиди по электронной почте ekaterina.triandafilidi@zextras.com
Подробнее..

Перевод Этический антидизайн как разработать продукт, не вызывающий привыкания

26.04.2021 14:21:23 | Автор: admin

Почему бы не сделать перерыв?


На столе, напротив открытого окна (смотрите картинку) лежит Wii Remote. Это контроллер для игровой консоли Wii компании Nintendo. Люди, которые выросли c Wii, вспомнят как в игре Wii Sports периодически всплывало окно с сообщением, вежливо напоминающим вам, что стоит сделать перерыв. Возможно, это несколько нелогично, но для такого вторжения в вашу жизнь есть несколько причин. В онлайн-играх, основанных на подписке, компания действительно могла извлечь выгоду, когда вы время от времени выходили из системы (не волнуйтесь, мы и про социальные сети тоже поговорим). Вы ведь не откажетесь от своей подписки, а каждую секунду вашего пребывания в сети вы съедали драгоценную серверную мощность. В других играх такие вещи, как гриндинг, хоть и заманчивы, но могут всё испортить. В Pokmon, например, если вы доведете всю свою команду до 60-го уровня, прежде чем сразитесь с Элитной четвёркой, вы выиграете, но не самым эффектным или честным способом.

С Wii история другая. Кажется, что команда Nintendo знала, что их аудитория в основном состояла из детей. Разработчики чувствовали необходимость вмешаться, в интересах самого игрока (и, возможно, для успокоения родителей), когда ребёнок проводил слишком много времени в игре. Сейчас мы редко встречаем подобные вещи, и я всегда хотел узнать, почему.

Сервера уже не так дороги, как раньше. Механика многих современных видеоигр больше не позволяет заниматься гриндингом. Неужели Nintendo перестала заботиться о наших детях? Что ж, индустрия сильно изменилась. Сегодня многим разработчикам онлайн-игр наоборот выгодно, когда игроки проводят больше времени в сети. В мобильной игре Clash of Clans игроки могут предотвратить нападение на свою базу оставаясь онлайн. Чем быстрее вы закончите игру, тем быстрее вас заставят купить её DLC или, еще лучше, Season Pass.

Чем больше времени вы проводите в игре, тем чаще вы будете сталкиваться с игроками, которые смогли заполучить красивые (и дорогие) скины и другие ценные внутриигровые предметы. Чем больше таких людей вы видите, тем чаще вы будете ощущать давление и захотите купить себе такие же. Это тревожная тенденция, и, похоже, она характерна для большинства AАА-игр.

Но кого ещё нам стоит поблагодарить за то, что наши глаза приклеились к девайсам?

Разработчикам социальных сетей всегда было выгодно удерживать нас онлайн как можно дольше. Всё очень просто: чем дольше вы смотрите на экран, тем больше вероятность, что ваши глаза наткнутся на рекламу. Они вложили миллиарды долларов в усовершенствование технологий для максимального вовлечения аудитории.

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

Instagram тоже идёт в этом направлении: с каждым месяцем он становится всё больше и больше похож на TikTok (особенно с тех пор, как они представили сервис Reels). Неудивительно, что мое поколение тратит так много времени на социальные сети. Но можно ли их обвинить в этом? Проводя исследования по этой теме, я внимательно изучал пользовательский интерфейс Instagram и моей самой большой проблемой было удержаться от того, чтобы погрузится в просмотр бесконечного потока алгоритмически подобранного контента.

Все эти вещи с удержанием пользователей это конечно здорово, но что, если им это не нужно? А что, если мы создадим продукты, которые не будут вызывать привыкание?

Тёмные паттерны и Федиверзум


Я понимаю, что большая часть аддиктивности приложения зависит от его юзабилити, и идея разработки приложения, менее удобного для пользователя, кажется нелепой. Тем не менее, и у такого продукта найдутся свои почитатели. На прошлой неделе я написал статью под названием Fediverse решает только половину проблемы, где я говорил о проблеме тёмных паттернов например таких, как бесконечная прокрутка. Дело в том, что моральный долг технологий, относящихся к категории FLOSS (Free/Libre/Open Source Software, сюда входит свободное и открытое ПО), ставить пользователей на первое место. В случае с социальными сетями большая часть этого процесса заключалась в создании децентрализованной экосистемы (т. е. Fediverse, Федиверзум), где вы сами отвечаете за свои собственные данные. В то время как организация Fediverse стала большим шагом вперёд, нам всё ещё приходится иметь дело с тёмными паттернами.

Большинство open-source разработчиков (включая меня) не имеют большого коммерческого опыта работы в дизайне UI/UX. Вот почему так много программного обеспечения с открытым исходным кодом либо чрезвычайно брутальны (см. Blender), либо очень, очень тесно связаны с дизайном его проприетарного аналога (см. KDE). Fediverse, по-видимому, относится ко второй категории.

Пользовательский интерфейс Mastodon (и всех его клиентов) очень похож на Twitter. В каком-то смысле это было сделано намеренно. Чем больше дизайн пользовательского интерфейса Mastodon походил на Twitter, тем больше новичков могли быстро освоиться с Fediverse при первом знакомстве с ним. Вместе с этим сильнее обостряется проблема тёмных паттернов. Опять же, интерфейс Twitter был преднамеренно спроектирован так, чтобы максимизировать количество времени, которое человек проводит в сети. К сожалению, Федиверзум унаследовал и это. Вот почему я предлагаю пересмотреть принципы проектирования интерфейсов приложений и клиентов для Fediverse. Также предлагаю обсудить, как мы можем наилучшим образом найти баланс между обеспечением положительного опыта использования продукта и психофизическим благополучием пользователей.

Что такое этический антидизайн?


Этический антидизайн строится на двух аспектах. Но об этом чуть позже. Прежде всего, стоит задать более общий вопрос: что же такое антидизайн?


Опасная зона Github, где происходят все рискованные манипуляции с репозиторием

Исторически, антидизайн сформировался в Италии и стал частью эстетики, направленной против культуры потребления. Хотя тут есть что-то общее, это не тот антидизайн, о котором я говорю. Я говорю о гораздо более прагматичном антидизайне, который действительно используют при проектировании интерфейсов. Антидизайн это нечто противоположное дизайну: всё делается не для того, чтобы сделать приложение удобным, а для того, чтобы сделать приложение непригодным для длительного использования.

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

Один из моих любимых примеров то, как Github работает с удалением репозитория. На странице настроек вашего репозитория Github все опасные действия сгруппированы вместе в очень страшном красном поле. В самом низу этого поля находится опция окончательного удаления репозитория. Если вы нажмёте на неё, Github сделает ещё один шаг и попросит вас ввести имя репозитория в диалоговое окно. Всё это заставляет вас от начала до конца осознавать то, что вы делаете, как вы это делаете, и насколько правильно вы это делаете.

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

Этический антидизайн перерабатывает эти идеи и пытается скрестить их с этическим кодексом.

Если бы мы все были, как Марк Аврелий, то, возможно, нам не нужно было бы беспокоиться о зависимости от социальных сетей. Дело в том, что не все являются чистыми существами стоической закалки; большинство людей очень хорошо (или, скорее, очень плохо) реагируют на поведенческий дизайн. Антидизайн предполагает вмешательство в работу пользователя, чтобы он случайно не совершил опасные действия. Этический анти-дизайн спрашивает: Что ещё может считаться опасным, согласно моему этическому кодексу? Как я уже ясно дал понять, одно действие, которое я считаю опасным, это бесконечная прокрутка. Итак, если я хочу опираться на этический антидизайн, то мне нужно, чтобы моё приложение не давало человеку впасть в транс и прокручивать ленты социальных сетей в течение всего дня.

Как выглядит этический антидизайн?


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

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

Другой вариант просто прервать бесконечную прокрутку. Это может быть очень похоже на пагинацию, а может и не быть. Главное заставить пользователя проявить активность, предложить ему сделать паузу и выбрать, хочет он или не хочет продолжать просмотр контента. Представьте себе: вы просматриваете посты в Instagram. Примерно через двадцать постов вы наткнётесь на кнопку с надписью Показать больше. Чтобы увидеть больше контента, вам нужно удерживать кнопку нажатой чуть больше одной секунды. Когда вы начинаете её удерживать, вам показывают анимированный круг, который заполняется с течением времени. Как только время пройдёт, будут показаны еще двадцать сообщений. Так мы получим гарантию, что не только плохое соединение с интернетом может прервать просмотр и заставить осознать, что этот процесс не является бесконечным. Кнопка, опять же, заставляет пользователя физически взаимодействовать с интерфейсом, чтобы увидеть больше контента.

Однако бесконечная прокрутка не единственное, что вызывает зависимость от социальных сетей. Ещё один способ поймать нас на крючок вовлечь в игру с количественными критериями, используя соревновательный элемент. Моя самооценка растёт с количеством подписчиков, лайков и комментариев, которые оставляют под постами. На самом деле, я знаю мало людей, которые искренне осуждали бы подобные вещи и считали их ерундой. Большинство людей включается в игру и относится к своим результатом достаточно самокритично.

Интересно, что Instagram, похоже, действительно знает об этом. Они несколько раз переделывали страницу профиля, чтобы сделать акцент на количестве подписчиков. А совсем недавно они почему-то обновили внешний вид постов, чтобы скрыть количество лайков, которые они получили. Технически всё ещё возможно посмотреть, сколько лайков получил пост, но для этого вы должны перепрыгнуть через достаточное количество обручей. Я бы также рассмотрел этот как пример этического антидизайна: люди испытывают искушение сравнивать себя с другими, и Instagram решил защитить их от этого. Лично я думаю, что этот кейс мог получить развитие. Например, Instagram уже показывает взаимных подписчиков на странице профиля человека; а что, если они вообще не будут показывать количество ваших подписчиков? Если бы мы использовали социальные сети как способ общения с людьми, которых мы знаем, то количество ваших подписчиков не должно было бы иметь большого значения, не так ли?

Куда это всё идёт?


Поведенческий дизайн и порожденный им стандарт для проектирования интерфейса соцсетей принёс миру много проблем. Естественно, эти тёмные паттерны просочились в Fediverse. Теперь нужно приложить большие усилия, чтобы избавиться от них. Это будет нелегко; в частности, мне будет нелегко заставить себя посещать веб-сайты, которые всё ещё используют рудиментарную разбивку на страницы. Но не стоит забывать, что всё это во имя благой цели.

Возможно, вы ещё не догадались, но я разрабатываю новый клиент для Fediverse по концепции этического антидизайна. Возможно, я доведу эту идею до крайности, но моя цель заключается не в том, чтобы создать популярный продукт, а, скорее, в том, чтобы продемонстрировать, как это могло бы выглядеть. Я надеюсь, что в конечном итоге другие разработчики вдохновятся моим проектом и попытаются воплотить эти идеи в своих собственных проектах.

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



Наши серверы можно использовать для разработки приложений.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Исправляем Госуслуги малой кровью добровольный редизайн мобильного приложения

15.03.2021 02:11:17 | Автор: admin

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

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

Дисклеймер о Госуслугах

Я искренне горжусь, что этот сервис есть у нас в стране. Не сосчитать, сколько он уже спас мне часов и недель стояния в очередях, ожидания документов и времени, затрачиваемого на поиск организации, которая имеет право выдать очередную справку для другой организации.

На это дело явно было положено не мало сил - начиная от человека, который решился протолкнуть идею цифровизации, заканчивая всеми дизайнерами, инженерами и разработчиками, которые смогли договориться и выпустить на рынок очень объемный и рабочий продукт.

Вопросов к продукту много, но это все правки к готовому проекту, которые мы все так любим получать.

Дисклеймер об авторе

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

В фигме решил наконец разобраться специально для этой статьи, просто чтобы визуализировать мысли. Так что качество исполнения подстать качеству мыслей)

Сейчас

Если кто-то не заходит в Госуслуги каждый день, то напоминаю, как они выглядят сейчас:

Исправляем главный экран

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

Четыре кнопки оплаты

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

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

  • сделаем так, чтобы кнопки стало видно сразу

  • не будет вводить заблуждение пользователя - заменим иконку штрих-кода на QR-код, ведь именно его надо искать на квитанции

  • статус "Не найдено" - это ошибка, сервер не смог найти информацию, или у меня нет долгов? Заменим на "Отсутствуют"

  • если в какой-то категории у нас есть задолженность, то сразу отобразим количество штрафов и их общую сумму

Уведомления

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

Сделаем колокольчик, отображающих непрочитанные уведомления.

Услуги

Основная часть и смысл приложения - услуги (запись на посещения, получение справок, запрос информации и т.д).

Здесь видим грамотное решение - вынесены три функции, которыми чаще всего будет пользоваться человек, а все остальные спрятаны за кнопкой "все услуги".

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

Также уберем выбор региона на этом этапе. От выбора местоположения зависит функционирование только некоторых услуг - паспорт гражданина РФ он и во Владивостоке паспорт.

FAQ

На скидку в факе 13 разделов в среднем по 10 вопрос-ответ в каждом. И этот раздел действительно старались сделать удобным, когда он начал разрастаться - добавили ссылки, заголовки, свернули ответы.

Но мой жизненный опыт подсказывает мне, что в FAQ можно найти только информацию о пунктах доставки интернет-магазинов - все остальное там искать пустая трата времени. Возможно, Госуслуги исключение, но доверие к этому разделу потеряно безвозвратно.

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

При этом обязательно оставляем более надежные способы найти ответ - кликабельный номер и путь к чату. Надо отметить, что чат у Госуслуг работает хорошо - мне пару раз дали ссылку на раздел Госуслуг, который по-другому найти не получалось.

Новости

Сначала возникает желание выкорчевать этот блок и выругаться: "Кому нужны новости вашей структуры кроме вас самих?". Но через секунду решаешься их прочитать - оказывается, там много чего интересного.

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

Исправляем список услуг

А тут все коротко. На сайте так много услуг, что единственный способ предоставит к ним удобный доступ с мобильного устройства - действительно умный поиск.

Предлагаю выводить группы услуг, а в описание перечислять подгруппы. Сейчас почти так и есть, только не понимаю отрицания существования списков.

Итого

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

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

На правах....

dendolg.ru - сайт-портфолио-визитка, контакты для сотрудничества

t.me/dolgopolovd - личный блог, в котором подмечаю хороший и высказываю идеи по поводу плохого промышленного и цифрового дизайна. а иногда размышляю о программировании и прочей философии

Подробнее..

Мобильное настоящее М.Видео телепортация была стремительной

15.03.2021 18:04:43 | Автор: admin


Говорить о том, что пандемия коронавируса в 2020 году стимулировала онлайн во всех его проявлениях становится уже немодным. Это свершившийся факт. Тем не менее, цифры упрямая вещь: мобильная платформа М.Видео (приложение продавца и покупателя) выросла в 2,5 раза, число уникальных пользователей в месяц клиентского приложения за год увеличилось более чем в 17 раз, при том, что общие онлайн-продажи компании по итогам 2020 года удвоились.

По сути бизнес молниеносно телепортировался из офлайн в digital. Онлайн-продажи в общем обороте компании выросли с 40% до 74%. Все это форсировало внутренние процессы по обновлению существовавших у нас мобильных приложений и интернет-магазинов. Под катом рассказ про эволюцию нашего мобильного приложения.

Стоит напомнить, что сеть М.Видео запустила свой онлайн-магазин еще в 2000 двадцать лет назад, в 2015 мы уже работали как омни-ритейлер, то есть, сайт и магазины были синхронизированы, и мы предлагали единые цены и сервисы во всех каналах.



Нет предела совершенству


Сегодня в компании реализуется концепция OneRetail, предполагающая создание единой универсальной среды для клиентов. Основная фишка бесшовность восприятия покупателями мобильного приложения, интернет-магазина и традиционной розничной сети М.Видео-Эльдорадо. В идеале клиент не должен ощущать разницы при взаимодействии с компанией онлайн или офлайн.

Например, вы вдруг захотели купить стиральную машину. Приходя в магазин бытовой техники отличить один белый квадрат от другого для многих проблематично. Даже если накануне, вы исследовали весь ассортимент КБТиЭ, изучили массу отзывов и рекомендаций. В итоге вы идете за человеческой консультацией в наш каменный супермаркет. Мотив разделить ответственность за выбор дорогостоящей бытовой техники с технологически продвинутым экспертом компании.

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



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

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



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

Рост числа идентифицированных покупателей обеспечивает полноту и качество нашей Big Data. Следовательно, точнее становятся наши промо-кампании и таргетированные предложения, в конечном итоге тем довольней клиент.

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



Творческие планы


Уже сегодня мы стремимся обеспечить 15 минутную готовность товара в выбранном магазине после его оформления на сайте. Для 60% заказов мы планируем доставку день в день. Это накладывается на поиск и расширение оптимальных механик взаимодействия с клиентами, включая: бесконтактную доставку, транспортировку заказов силами служб такси, запуск сервиса видеоконсультаций с продавцами для удалённого ознакомления с товаром и оформления продаж и т. д.

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

Сегодня Группа М.Видео-Эльдорадо остро нуждается в талантливых java и python разработчиках. В компании формируется современный стек на базе собственной микросервисной архитектуры. Для реализации амбициозных планов выделены необходимые ресурсы.

Дело за малым, реализовать все наши наполеоновские планы и осчастливить в конечном итоге существующих и потенциальных клиентов. Приходите, будет интересно.
Подробнее..

Как все-таки экономить на мобильной разработке?

18.03.2021 12:07:58 | Автор: admin

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

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

Экономия на функционале

Есть такая штука, которая называется MVP Minimum Viable Product. То есть, минимально жизнеспособный продукт.

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

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

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

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

Можно начертить роудмап и развивать приложение по нему. Третий квартал 2021 добавляем возможность нанесение кастомного принта на апельсин. Четвертый квартал 2021 GPS треккинг курьера с апельсинами. Первый квартал 2022 создание рекомендательного сервиса для сортов апельсинов

Быстро это медленно, но постоянно. А еще по верному пути. Ограничьтесь MVP для запуска и, если все пройдет хорошо, свистелок накрутить всегда успеете. Если все плохо снизите потери.

Экономия на дизайне

Конечно, хочется, чтобы красиво. А еще хочется, чтоб не как у всех. Хочется поражать, влюблять, вызывать почти что физиологическое привыкание.

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

Максим Никитин уже говорил, что вычурно не значит хорошо. Если у вас не так, как у всех, это совсем не значит, что у вас лучше. А еще, если у вас дороже это тоже не значит, что этим удобнее пользоваться или вообще, что это хоть кто-то заметит.

В iOS и Андроиде есть так называемые гайдлайны объемистые списки рекомендаций, как надо делать дизайн, чтобы вас не прокляли ни разработчики ни юзеры. Если им следовать и размещать все кнопки там, где их ожидают найти, то можно сэкономить кучу сил программистам, денег себе и нервов пользователям.

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

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

Экономия на платформе

Да, надо писать натив. Да, на обе платформы и да, черт бы побрал это всемирное разделение на яблочников и андроидов. Да, это в два раза больше денег. А если нет?

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

Такой джекпот сорвать удается, конечно, не каждому, но в целом схема рабочая. Присмотритесь к своей ЦА, может быть пользователей Apple в ней ничтожно мало? Выпускайтесь тогда на Андроиде, а на iOS анонсируйте через полгода-год. Или наоборот, вам интересная только аудитория iOS, потому что основной способ монетизации внутренние покупки в приложении? Тогда да, яблочники принесут гораздо больше, а Андроид может и подождать.

Экономия ультимативная

Вообще, все вышеперечисленные способы об одном и том же. Не надо делать все и сразу делайте все и постепенно.

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

А знаете что еще можно? Можно вообще не писать приложение.

Да, это забавно, когда сапожник предлагает вам ходить без сапог, но баблишко мы, конечно любим, а истина дороже.

Если у вас есть хороший качественный сайт и мобильная версия этого сайта выполняет весь необходимый на данный момент функционал, бюджет туговат, проект маловат может вам сейчас и не нужно мобильное приложение?

Мобильные маркеты переполнены наспех напиленными, никому ненужными кривыми приложениями. Зачем множить бездну? Зачем сливать время и бюджет в никуда? Может лучше заложить его на следующий, например, год, и сделать потом хорошо? Лучше, чем сейчас и черти как. А за этот год провести хорошую аналитику, выбрать достойную команду, понять дальнейшие точки роста и что именно вам нужно от приложения на начальном этапе.

В конце концов, невозможно сэкономить на проекте сильнее, чем полностью от него отказавшись.

Подробнее..

IOS. UI. Примы. Часть 1

27.03.2021 20:12:03 | Автор: admin

Привет читателям Хабра!

Я iOS-разработчик, и так случилось, что мне приходилось много делать в ui: кастомные view, тени, layout-ы, кнопки и вот это всё. В этой и паре следующих статей хочу поделиться некоторыми приёмами, которые помогали мне добиваться весьма красивых и интересных эффектов в плане рисования компонентов ui. Надеюсь, кому-нибудь это будет полезно. Ну или просто интересно.

Небольшое введение

Не берусь говорить за всех, но, исходя из личного опыта, сложилось впечатление, что для достаточно большого количества разработчиков рисование каких-то "плашек" с нестандартными формой и поведением крайне нежелательная задача. Кто-то больше в архитектуре, кто-то больше про "сделать бизнесу хорошо" с минимальными усилиями (соответственно, просят поумерить пыл дизайнеров) и т.п. И если уж приходится делать что-то из ряда вон, то начинается google, stackoverflow, эксперименты и т.д., что занимает немало времени, и появляется ощущение, что оно того вообще не стоит. Собственно, эту небольшую серию статей я и задумал как некоторую справку, прочтение которой снимет ряд вопросов и позволит быстрее оценивать/реализовывать нетипичные ui-компоненты. На конкретных примерах постараюсь продемонстрировать, как, что и почему можно делать.

Пример 1: view с нестандартными границей и тенью

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

Теперь чуть подробнее. У CALayer есть свойство mask. В документации можно прочитать, что это тот же самый опциональный CALayer, и если он не nil, то его альфа-канал используется как маска для контента исходного layer. То есть если взять png-картинку с котом и прозрачностью и каким-то образом засунуть ее в CALayer (назовем его catLayer), то при присваивании layer.mask = catLayer контент нашего исходного layer будет в виде кота, что бы ни находилось у него внутри. Может, текстовый кот получится, если внутри layer много текста. В нашем же случае нужен layer-маска в виде произвольной фигуры. Тут может помочь CAShapeLayer - наследник CALayer, который, грубо говоря, умеет внутри себя рисовать произвольную форму посредством задания ему проперти path. При использовании shapeLayer в качестве маски, всё, что находится вне формы, описываемой shapeLayer.path, работает как фильтр с alpha = 0.

Саму форму можно задать, используя UIBezierPath: для этого у последнего есть функции
addLine(to:), move(to:), addArc(withCenter:radius:startAngle:endAngle:clockwise) и т.д.
Здесь хотелось бы отметить пару моментов. Итоговый path должен выглядеть так, будто его "нарисовали, не отрывая карандаш от бумаги": стартуем из произвольной точки на границе и постепенно добавляем линии к общему пути так, чтобы конец предыдущей линии был началом следующей линии, и так далее. В конце возвращаемся в исходную точку. Некоторых сбивает с толку функция addArc, потому что в ней есть вроде и startAngle и endAngle, и clockwise. Вот clockwise как раз и нужен для того, чтобы управлять тем, вдоль какой из частей окружности, заданной двумя углами, мы двигаемся. В нашем примере в правом верхнем углу добавляется кусок окружности от -/2 до 0 с clockwise равным именно true, иначе мы бы просто вырезали целую окружность из нашей view:


А зачем здесь вообще дополнительный слой? Почему бы не задать маску у исходного?
Проблема в том, что маска работает так, что отрезает просто всё, что ей попадётся, в том числе и тень слоя. Так что если задавать mask у слоя исходной view, то тени просто не будет видно.

Наконец, чтобы придать нужную форму тени, у CALayer есть свойство shadowPath.

Полный код примера 1
import UIKitfinal class SimpleCustomBorderAndShadowView: UIView {  private let frontLayer = CALayer()  private let inset: CGFloat = 40    // MARK: Override    override init(frame: CGRect) {    super.init(frame: frame)    setup()  }    required init?(coder: NSCoder) {    super.init(coder: coder)    setup()  }    override func layoutSubviews() {    super.layoutSubviews()    frontLayer.frame = bounds        let maskAndShadowPath = UIBezierPath()    maskAndShadowPath.move(to: CGPoint(x: 0, y: inset))    maskAndShadowPath.addLine(to: CGPoint(x: inset, y: 0))    maskAndShadowPath.addLine(to: CGPoint(x: bounds.width - inset, y: 0))    maskAndShadowPath.addArc(withCenter: CGPoint(x: bounds.width - inset, y: inset),                             radius: inset,                             startAngle: -CGFloat.pi / 2,                             endAngle: 0,                             clockwise: true)    maskAndShadowPath.addLine(to: CGPoint(x: bounds.width, y: bounds.height - inset))    maskAndShadowPath.addLine(to: CGPoint(x: bounds.width - inset, y: bounds.height))    maskAndShadowPath.addLine(to: CGPoint(x: inset, y: bounds.height))    maskAndShadowPath.addArc(withCenter: CGPoint(x: inset, y: bounds.height - inset),                             radius: inset,                             startAngle: CGFloat.pi / 2,                             endAngle: CGFloat.pi,                             clockwise: true)    maskAndShadowPath.close()        (frontLayer.mask as? CAShapeLayer)?.frame = bounds    (frontLayer.mask as? CAShapeLayer)?.path = maskAndShadowPath.cgPath    layer.shadowPath = maskAndShadowPath.cgPath   }    // MARK: Setup    private func setup() {    backgroundColor = .clear        layer.shadowColor = UIColor.black.cgColor    layer.shadowOffset = .zero    layer.shadowRadius = 20    layer.shadowOpacity = 1        frontLayer.mask = CAShapeLayer()    frontLayer.backgroundColor = UIColor.white.cgColor    layer.addSublayer(frontLayer)  }}

Пример 2: view с вырезанной кривой произвольного вида

Данный пример выбран, чтобы продемонстрировать два момента: как вырезать что-то внутри слоя и как создать путь, как бы обводящий кривую линию на некотором расстоянии от неё.

Для того, чтобы вырезать что-то внутри слоя, нужно понимать, по какому правилу происходит раскрашивание форм, созданных с помощью UIBezierPath. В принципе, про это довольно внятно написано здесь. Получается, чтобы добиться эффекта как на картинке выше, нужно в итоговый path для маски добавить путь, обходящий внешнюю границу view, что делается с помощью UIBezierPath(roundedRect:cornerRadius:), и после добавить путь, отвечающей вырезу в форме кривой.

Для формы кривой используется функция addQuadCurve(to:controlPoint:). И если взять UIBezierPath, вызывать addQuadCurve, проставить ему ширину с помощью lineWidth, и добавить это в итоговый path для маски то... Ничего не выйдет. Если чуть-чуть задуматься и ещё вспомнить про это, то всё начинает казаться логичным: CoreGraphics нужно как-то сказать о границах, при переходе через которые происходит подсчёт каких-то counter-ов для дальнейшего решения о том, красить данную область или нет. Чтобы построить путь именно вокруг кривой, у CGPath есть функция copy(strokingWithWidth:lineCap:lineJoin:miterLimit:). Сам CGPath, в свою очередь, можно получить из UIBezierPath, обращаясь к свойству cgPath.

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

Полный код примера 2
import UIKitfinal class ErasedPathView: UIView {  private let frontLayer = CAShapeLayer()    // MARK: Override    override init(frame: CGRect) {    super.init(frame: frame)    setup()  }    required init?(coder: NSCoder) {    super.init(coder: coder)    setup()  }    override func layoutSubviews() {    super.layoutSubviews()        frontLayer.frame = bounds        let maskAndShadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 20)        let curvePath = UIBezierPath()    curvePath.move(to: CGPoint(x: bounds.width / 4, y: bounds.height / 4))    curvePath.addQuadCurve(to: CGPoint(x: bounds.width * 3 / 4, y: bounds.height * 3 / 4),                           controlPoint: CGPoint(x: bounds.width, y: 0))        let innerPath =  UIBezierPath(cgPath: curvePath.cgPath.copy(strokingWithWidth: 70, lineCap: .round, lineJoin: .round, miterLimit: 0))    maskAndShadowPath.append(innerPath)        (frontLayer.mask as? CAShapeLayer)?.frame = bounds    (frontLayer.mask as? CAShapeLayer)?.path = maskAndShadowPath.cgPath    layer.shadowPath = maskAndShadowPath.cgPath  }    // MARK: Setup    private func setup() {    backgroundColor = .clear    frontLayer.backgroundColor = UIColor.white.cgColor        layer.addSublayer(frontLayer)    let mask = CAShapeLayer()    mask.fillRule = .evenOdd    frontLayer.mask = mask        layer.shadowColor = UIColor.black.cgColor    layer.shadowOffset = .zero    layer.shadowRadius = 20    layer.shadowOpacity = 1  }}

Пример 3: рисование форм внутри view

Для того, чтобы просто рисовать внутри вашей view всё, что нравится, без создания дополнительных слоёв, можно опять же использовать CAShapeLayer. Нужно сделать override статического свойства layerClass у исходной view, возвращая ShapeLayer.self, и так же как и в Примере 1 задать этому слою path.

Есть один нюанс, не упомянутый ранее. При построении непрерывного пути при рисовании произвольной формы можно случайно перепрыгнуть из конца очередной линии в совершенно другое место. Типичный пример добавление нового куска окружности при непустом path. В таких случаях CoreGraphics просто напросто дорисует за вас недостающую линию, соединяющую последнюю точку пути и новую точку очередной добавляемой линии. В совокупности с fillRule у CAShapeLayer этим можно аккуратно пользоваться. Например, на третьей справа картинке (карта треф) этот подход существенно упрощает рисование: не нужно думать о том, в каких именно местах пересекаются окружности.

Пики
import UIKitfinal class SpadeCardView: UIView {    var selfLayer: CAShapeLayer { layer as! CAShapeLayer }  private let inset: CGFloat = 20    // MARK: Override    static override var layerClass: AnyClass { CAShapeLayer.self }    override init(frame: CGRect) {    super.init(frame: frame)    setup()  }    required init?(coder: NSCoder) {    super.init(coder: coder)    setup()  }    override func layoutSubviews() {    super.layoutSubviews()        let path = UIBezierPath()    let size = bounds.width - 2 * inset    let radius = size / 4    let alpha = atan(2 * radius / size)        path.move(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2))    path.addArc(withCenter: CGPoint(x: inset + radius, y: bounds.height / 2),                radius: radius, startAngle: 0,                endAngle: CGFloat.pi + 2 * alpha,                clockwise: true)    path.addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 - size / 2))    path.addArc(withCenter: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2),                radius: radius,                startAngle: -2 * alpha,                endAngle: CGFloat.pi,                clockwise: true)    path.addQuadCurve(to: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 + size / 2),                      controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))    path.addLine(to: CGPoint(x: bounds.width / 2 - radius, y: bounds.height / 2 + size / 2))    path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2),                      controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))        selfLayer.path = path.cgPath  }    // MARK: Setup    private func setup() {    selfLayer.fillColor = UIColor.black.cgColor    selfLayer.strokeColor = UIColor.black.cgColor    selfLayer.lineWidth = 2        layer.shadowColor = UIColor.black.cgColor    layer.shadowOffset = .zero    layer.shadowRadius = 10    layer.shadowOpacity = 1  }}
Бубны
import UIKitfinal class DiamondCardView: UIView {    var selfLayer: CAShapeLayer { layer as! CAShapeLayer }  private let inset: CGFloat = 20  private let adjustment: CGFloat = 10    // MARK: Override    static override var layerClass: AnyClass { CAShapeLayer.self }    override init(frame: CGRect) {    super.init(frame: frame)    setup()  }    required init?(coder: NSCoder) {    super.init(coder: coder)    setup()  }    override func layoutSubviews() {    super.layoutSubviews()        let path = UIBezierPath()    let size = bounds.width - 2 * inset        path.move(to: CGPoint(x: inset, y: bounds.height / 2))    path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 - size / 2),                      controlPoint: CGPoint(x: bounds.width / 2 - adjustment, y: bounds.height / 2 - adjustment))    path.addQuadCurve(to: CGPoint(x: bounds.width - inset, y: bounds.height / 2),                      controlPoint: CGPoint(x: bounds.width / 2 + adjustment, y: bounds.height / 2 - adjustment))    path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2),                      controlPoint: CGPoint(x: bounds.width / 2 + adjustment, y: bounds.height / 2 + adjustment))    path.addQuadCurve(to: CGPoint(x: inset, y: bounds.height / 2),                      controlPoint: CGPoint(x: bounds.width / 2 - adjustment, y: bounds.height / 2 + adjustment))        selfLayer.path = path.cgPath  }    // MARK: Setup    private func setup() {    selfLayer.fillColor = UIColor.red.cgColor    selfLayer.strokeColor = UIColor.red.cgColor    selfLayer.lineWidth = 2        layer.shadowColor = UIColor.black.cgColor    layer.shadowOffset = .zero    layer.shadowRadius = 20    layer.shadowOpacity = 1  }}
Трефы
import UIKitfinal class ClubCardView: UIView {    var selfLayer: CAShapeLayer { layer as! CAShapeLayer }  private let inset: CGFloat = 20  private let adjustment: CGFloat = 10    // MARK: Override    static override var layerClass: AnyClass { CAShapeLayer.self }    override init(frame: CGRect) {    super.init(frame: frame)    setup()  }    required init?(coder: NSCoder) {    super.init(coder: coder)    setup()  }    override func layoutSubviews() {    super.layoutSubviews()        let path = UIBezierPath()    let size = bounds.width - 2 * inset    let radius = size / 4        path.move(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2))    path.addArc(withCenter: CGPoint(x: bounds.width / 2 - radius, y: bounds.height / 2 + adjustment),                radius: radius,                startAngle: 0,                endAngle: 2 * CGFloat.pi,                clockwise: true)    path.addArc(withCenter: CGPoint(x: bounds.width / 2, y: bounds.height / 2 - radius),                radius: radius,                startAngle: CGFloat.pi / 2,                endAngle: 5 * CGFloat.pi / 2,                clockwise: true)    path.addArc(withCenter: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 + adjustment),                radius: radius,                startAngle: CGFloat.pi,                endAngle: 3 * CGFloat.pi,                clockwise: true)    path.addQuadCurve(to: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 + size / 2),                      controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))    path.addLine(to: CGPoint(x: bounds.width / 2 - radius, y: bounds.height / 2 + size / 2))    path.addQuadCurve(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2),                      controlPoint: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))        selfLayer.path = path.cgPath  }    // MARK: Setup    private func setup() {    selfLayer.fillColor = UIColor.black.cgColor    selfLayer.strokeColor = UIColor.black.cgColor    selfLayer.fillRule = .nonZero    selfLayer.lineWidth = 2        layer.shadowColor = UIColor.black.cgColor    layer.shadowOffset = .zero    layer.shadowRadius = 20    layer.shadowOpacity = 1  }}
Черви
import UIKitfinal class HeartCardView: UIView {    var selfLayer: CAShapeLayer { layer as! CAShapeLayer }  private let inset: CGFloat = 20    // MARK: Override    static override var layerClass: AnyClass { CAShapeLayer.self }    override init(frame: CGRect) {    super.init(frame: frame)    setup()  }    required init?(coder: NSCoder) {    super.init(coder: coder)    setup()  }    override func layoutSubviews() {    super.layoutSubviews()        let path = UIBezierPath()    let size = bounds.width - 2 * inset    let radius = size / 4    let alpha = atan(4 * radius / (3 * size))        path.move(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))    path.addArc(withCenter: CGPoint(x: inset + radius, y: bounds.height / 2 - radius),                radius: radius,                startAngle: CGFloat.pi - 2 * alpha,                endAngle: 0,                clockwise: true)    path.addArc(withCenter: CGPoint(x: bounds.width / 2 + radius, y: bounds.height / 2 - radius),                radius: radius,                startAngle: -CGFloat.pi,                endAngle: 2 * alpha,                clockwise: true)    path.addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height / 2 + size / 2))        selfLayer.path = path.cgPath  }    // MARK: Setup    private func setup() {    selfLayer.fillColor = UIColor.red.cgColor    selfLayer.strokeColor = UIColor.red.cgColor        layer.shadowColor = UIColor.black.cgColor    layer.shadowOffset = .zero    layer.shadowRadius = 20    layer.shadowOpacity = 1  }}

Заключение

Ниже, так сказать, things to remember:

  • +1 CALayer, mask, CAShapeLayer, shadowPath для кастомной границы и тени

  • copy(strokingWithWidth:lineCap:lineJoin:miterLimit:) для объемной обводки path

  • CAShapeLayer, path + fillRule даёт интересные возможности

В следующей статье на эту тему постараюсь рассказать про layout-ы в коллекциях. Всем добра, пишите классные приложения и делайте красоту в интерфейсах!

Подробнее..

Снова про UIUX дизайн в 1С или как ускорить разработку мобильных приложений

09.04.2021 04:13:25 | Автор: admin

Ранее делился тем, как мы решаем проблему отсутствия UI\UX дизайна в 1С с помощью Java Script и React.js. Сегодня обсудим роль и влияние дизайна на скорость разработки и внедрений мобильных приложений на 1С.

Для чего вообще заниматься дизайном, если и так все хорошо на рынке ИТ, спрос велик, работы валом и т.д.?

Три ключевые причины:

  1. Снизить себестоимость, за счет многократных доработок\переделок, когда Заказчик (внутренний\внешний) видит продукт, только после разработки, а не ДО разработки, когда может хоть как-то повлиять.

  2. Повысить человеко-центричность в разрабатываемых бизнес-приложениях, за счет предварительной проработки дизайна (UX и UI), направленного на достижение цели индивидуума, использующего программное обеспечение.

  3. Ускорить внедрение разработанного ПО и снизить нагрузку на техническую поддержку, как свою, так и Заказчика.

Технология разработки, когда до начала программирования, быстро, просто и дешево дизайнишь прототип (MVP), в online обсуждаешь, согласовываешь и сдаешь заказчику, после чего начинаешь кодить зарекомендовала себя на все 100%.

Более того, был опыт, когда задизайнили идею надстройки\расширения к типовой конфигурации, запустили рассылку по внутренней базе и получили тут-же лиды = $!

Как тебе такое Илон Маск? Зерокодинг воплати!

В мобилке тема UI и UX дизайна, прототипирования, MVP стоит ещё острее чем в десктопе. Так, года два назад, обратился в ряд компаний с ТЗ на мобильное приложение и все мне предложили примерно следующий порядок реализации проекта:

Этап 1. Исследование и составление ТЗ

Сбор и формализация требований;

Разработка и проектирование прототипа приложения;

Разработка и проектирование UX/UI интерфейсов ключевых экранов приложения;

Подготовка ТЗ с описанием принципа работы функционала.

Этап 2. Разработка приложения на основании документации, составленной на этапе 1.

2.1. Разработка серверной части:

2.2. Разработка клиентской части:

.

2.3. Тестирование и отладка:

..

2.4. Публикация мобильного приложения в Store:

.

Этап 3. Передача исходных материалов проекта.

Попробовал, и понял на сколько круто, быстро, удобно! Во-первых, был впечатлен конкретным дизайнером в конкретной компании, звали Олеся. Понимала мои запросы с полуслова, сама предлагала кучу классных дизайн-решений, у меня слюнки текли и хотелось уже быстрее дождаться разработки и получить долгожданный прототип. Во-вторых, я участвовал сам в создании своего детища, сам решал, как будут называться кнопки, что будет в результате их нажатия и т.д., конечно 80-90% решений было за Олесей, но те 10% влияния, что я оказывал были ценны для меня. В-третьих, мы были на одной волне с разработчиками, ведь уже не только куча текста было написано, но и был кликабельный прототип, ожидание и реальность сошлись на много быстрее и четче!

Создавая мобильные приложения на платформе 1С, приходится сразу кодить\конфигурировать. Продвинутые компании юзают не привычный 1С-му глазу Axure или Figma, долго настраивают, готовят UI Kitы и только после этого дизайнят интерфейс 1С, как из конфигуратора. Все это долго.. нудно и посильно не всем с коробки, короче "Такси - сел и поехал" не получается ;-)

В итоге, имея положительный опыт и наработки по десктопным форма, начали делать мобильные формы.

Технологии и архитектура уже известные:

Frontend

В основе лежит Single Page Application подход и фреймворк React.

ru.reactjs.org

Для реализации UI конструктора форм возьмем Material UI.

material-ui.com/ru

Сетка для проектирования форм и реализации колонок будет взята тоже из Material, но потребует настройки.

material-ui.com/ru/components/grid

Примеры реализации аналогичной идеи Drag&Drop создания макета из элементов:

github.com/chriskitson/react-drag-drop-layout-builder
github.com/kiho/react-form-builder
github.com/saravananangu/react-drag-drop-form-builde


Backend
На первом этапе достаточно использовать serverless подход и взять Google Firebase за основу.

В дальнейшем начнем разработку собственного backend-приложения на Node.js.

Архитектура:

Что пока получается:

На этот прототип понадобилось 10 минут:

Оказалось, что реализовывать инструмент прототипирования мобильной платформы 1С в разы сложнее десктопа, ведь логика элементов мобильных форм 1С платформы куда сложнее чем десктоп, так же есть особенности растягивания элементов по размеру экрана и т.д. и т.п. Пока тестируем на внутренних проектах и на ряде клиентов, но в целом, технология так же себя оправдывает: разработка и сдача работ заказчику увеличивается минимум на 25-30%, но есть одно НО: нужно выращивать компетенции дизайнера внутри, привлекать консультантов внешних, из мира web и мобильной разработки, в итоге появляются внутренние 1С:Дизайнеры ;-)

Всем успешных проектов, мира и добра!

Подробнее..

То, чего нам так не хватало Render Effect в Android 12

21.05.2021 10:23:30 | Автор: admin

Иногда бывает нужно размыть задний план на экранах мобильного приложения, например в чате. Теперь это можно сделать всего парой строк кода. В Android 12 появился новый API Render Effect, который позволяет накладывать визуальные эффекты на Canvas или View. Этот API радует своей простотой и высокой скоростью отрисовки. Наибольший интерес представляет Render Effect дляразмытия (BlurEffect), но в этой статье мы затронем и остальные виды эффектов. Материал может быть полезен не только андроид-разработчикам, но и дизайнерам мобильных приложений.

Итак, каким образом можно размыть задний план при отображении диалога? Раньше в Андроиде для этого надо было отрисовать все вью с заднего плана на bitmap-е и затем размыть его с помощью RenderScript или OpenGL. Но это означает, что в проекте появится немало запутанного для прочтения кода, либо придется подключить стороннюю библиотеку для размытия. Плюс добавятся обработчики событий отрисовки и код для получения битмапа. К тому же по производительности эти решения могут не давать желаемого результата. При использовании некоторых популярных библиотек для размытия разработчики замечают лаги, например, если есть RecyclerView, который содержит много BlurView.

С помощью Render Effect мы можем всего парой строк кода реализовать блюр без лагов. Render Effect работает эффективно за счет того, что он использует Render Nodes (узлы отрисовки). Они образуют иерархию аппаратно ускоренной отрисовки, и эта отрисовка происходит с помощью GPU (графического процессора). Перерисовываться будут только те части UI, которые оказались в состоянии invalidated. Все вычисления для Render Effect выполняются в Render потоке и не блокируют UI.

У Render Effect очень простой API:

val renderNode = RenderNode("myRenderNode")renderNode.setPosition(0, 0, 50, 50)renderNode.setRenderEffect(renderEffect) canvas.drawRenderNode(renderNode)

Мы добавили эффект для RenderNode методом setRenderEffect(), и его отрисовка происходит в методе Canvas.drawRenderNode().

Можно применить Render Effect и к узлу отрисовки вьюшки:

imageView.setRenderEffect(renderEffect)

Приведем пример создания эффекта:

val blurEffect = RenderEffect.createBlurEffect(       20f, //radiusX       20f, //radiusY              Shader.TileMode.CLAMP)imageView.setRenderEffect(blurEffect)

Здесь мы создаем эффект размытия и применяем его к узлу отрисовки, привязанному к imageView. Задаем интенсивность размытия по оси X и оси Y (Значения 20f) . Последний аргумент Shader.TileMode определяет то, как будет выглядеть эффект на краях отрисовываемой области.

Если применить размытие к корневому layout-у, то будут размыты все вьюшки: и кнопка, и ползунки.

Варианты значений TileMode:

  • MIRROR. Используются зеркальные отражения изображения по вертикали и горизонтали, при этом на границах нет резких переходов.

  • CLAMP. Если shader выходит за пределы границ, используется цвет пикселей на границе. Выглядит практически также как MIRROR, но возможно незначительное искажение формы объектов возле границы.

  • REPEAT. Изображение повторяется горизонтально и вертикально. На изображении заметно, как темная вода из нижней части изображения отразилась на верхней границе изображения.

  • DECAL (появился в API 31). Отрисовка shader-а только в пределах границ. Можно заметить, что на границе изображение стало чуть светлее.

Мы также можем применить размытие при создании тени. Конечно, можно просто создать тень с помощью свойства Elevation:

android:elevation="10dp"

Однако, в этом случае мы не сможем задавать направление, в котором должна отбрасываться тень, или ее цвет. Также тень для Elevation по умолчанию работает только с формами скругленного прямоугольника (круг и прямоугольник частные случаи прямоугольника со скругленными углами). Для тени другой формы нужно реализовать ViewOutlineProvider, чтобы он возвращал Outline с setPath(...), а это может быть весьма трудоемко.

Кастомную тень можно создать и с помощью xml, прописав <shape> с градиентом:

<gradient       android:type="radial"       android:centerColor="#90000000"       android:gradientRadius="70dp"       android:startColor="@android:color/white"       android:endColor="@android:color/transparent"/>

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

Сначала для сравнения покажем, как можно реализовать аналогичную тень круглой формы с помощью RenderEffect. Создаем круг серого цвета:

<shape android:shape="oval">   <solid android:color="#CCCCCC" /></shape>

Затем этот drawable устанавливаем в качестве содержимого ImageView и применяем размытие к ImageView:

imageView.setImageResource(R.drawable.gray_circle)val renderEffect = RenderEffect.createBlurEffect(20f, 20f, Shader.TileMode.CLAMP)imageView.setRenderEffect(renderEffect)

А вот как с помощью RenderEffect можно добиться тени произвольной формы и, если понадобится, произвольного цвета:

<ImageView   android:id="@+id/shadow"   android:layout_width="150dp"   android:layout_height="150dp"   android:src="@drawable/ic_baseline_time_to_leave_24"   android:tint="#444444"/>
val effect = RenderEffect.createBlurEffect(10f, 10f, Shader.TileMode.CLAMP)imageViewfindViewById<ImageView>(R.id.shadow).setRenderEffect(effect)

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

Всего есть семь видов RenderEffect:

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

  • Blur размытие по оси X и Y.

  • ColorFilter применяется цветной фильтр (см. скриншот ниже).

  • Offset сдвиг по осям X, Y (жаль, что нет поворота).

  • Shader отрисовывает шейдер, переданный в аргументах. С помощью шейдеров можно создавать, например, различные градиенты.

  • Chain комбинация 2 эффектов. Результат отрисовки одного эффекта используется как источник для второго эффекта.

  • BlendMode для объединения 2 эффектов в определенном режиме.

Можно добиться красивого эффекта замерзшего стекла (или запотевшего окна): он достигается сочетанием размытия и наложения прозрачно-белого цвета.

Применим такой эффект к панели внизу экрана. Фактически здесь надо сочетать три эффекта: отрисовка битмапа + размытие + цветной фильтр. К сожалению, нельзя сделать так, чтобы размывалась та часть UI, которая отрисована под вьюшкой. Поэтому RenderEffect нужно применить не к самой панели, а к тому, что находится под ней и содержит фоновое изображение: к imageView или к корневому layout-у. Комбинировать эффекты можно 2 способами: передавать один эффект в параметр create-метода другого эффекта, либо использовать метод createChainEffect():

val bmpEffect = RenderEffect.createBitmapEffect(...)val blurBmpEffect = RenderEffect.createBlurEffect(..., bmpEffect, ..)val finalEffect = RenderEffect.createColorFilterEffect(..., blurBitmapEffect)

или

val finalEffect = RenderEffect.createChainEffect(colorEffect,blurEffect)

Приведем пример наложения цветного фильтра:

val argb = Color.valueOf(red, green, blue, transparency).toArgb()val colorEffect = RenderEffect.createColorFilterEffect(       PorterDuffColorFilter(argb, PorterDuff.Mode.SRC_ATOP))

Результат:

Или можно с помощью цветного фильтра поменять насыщенность изображения:

val matrix = ColorMatrix()matrix.setSaturation(0f)imageView.setRenderEffect(RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(matrix)))

Результат для setSaturation(0f) и setSaturation(100f):

BlendMode

С помощью метода RenderEffect.createBlendModeEffect() можно объединить два эффекта в одном из режимов:

  • CLEAR

  • SRC

  • DST

  • SRC_OVER

  • DST_OVER

  • SRC_IN

  • DST_IN

  • SRC_OUT

  • DST_OUT

  • SRC_ATOP

  • DST_ATOP

  • XOR

  • PLUS

  • MODULATE

  • SCREEN

  • OVERLAY

  • DARKEN

  • LIGHTEN

  • COLOR_DODGE

  • COLOR_BURN

  • HARD_LIGHT

  • SOFT_LIGHT

  • DIFFERENCE

  • EXCLUSION

  • MULTIPLY

  • HUE

  • SATURATION

  • COLOR

  • LUMINOSITY

Подробное описание режимов можно посмотреть здесь. Для примера приведем три режима (здесь источник, то есть первый RenderEffect это синий квадрат, а приемник, то есть второй Render Effect красный круг):

DST_ATOP выбрасывает пиксели, которые не накладываются на источник. DST_ATOP выбрасывает пиксели, которые не накладываются на источник. OVERLAY умножает цвета.OVERLAY умножает цвета.COLOR сохраняет оттенок и насыщенность источника, а яркость делает как у приемника.COLOR сохраняет оттенок и насыщенность источника, а яркость делает как у приемника.

Как уже упоминалось выше, RenderEffect работает крайне эффективно. При скролле RecyclerView с большим количеством элементов, у которых размыто по две ImageView (размытие с параметрами radiusX: 20f, radiusY: 20f, Shader.TileMode.CLAMP), никаких подтормаживаний не наблюдается. Размытие вью с анимацией тоже работает без задержек. В настоящий момент работа RenderEffect была проверена автором на эмуляторе Android 12 (API 31).

Что касается поддержки RenderEffect API, можно надеяться, что она будет добавлена в библиотеке AndroidX для Android 10 и 11, так как Render Nodes, лежащие в основе RenderEffect, были добавлены в Android 10 (API level 29). Однако, как пишут в официальной документации, Render Effect могут поддерживать не все устройства: Different Android devices may or may not support the feature due to limited processing power.

Отметим также, что Render Effect является одним из вариантов замены для RenderScript API, который устарел начиная с Android 12.

Заключение

Итак, в Android 12 мы получили то, чего так долго не хватало многим разработчикам простой API для отрисовки визуальных эффектов. Сочетая простые эффекты (размытие, цветные фильтры, шейдеры), мы можем получать интересные графические результаты. При этом больше не нужно возиться с вытаскиванием bitmap-изображения, эффект можно просто повесить на вьюшку. А благодаря использованию RenderNodes, отрисовка RenderEffect работает очень быстро.

Спасибо за внимание! Надеемся, что наш опыт был вам полезен.

Подробнее..

За что банит Apple(и Google)

27.05.2021 02:21:05 | Автор: admin

У платформодержателей мобильных сторов множество причин для блокировок. Если напрямую с этим не сталкиваться, то, кажется, что все просто - не нарушай описанные правила и все будет отлично. На деле не все блокировки легко предугадать простым следованием гайдлайнов.

Рассмотрим некоторые из них.

Покупки не через сервисы Google&Apple

Начнем с одной из самых известных сейчас блокировок - удаление игры Fortnite от Epic Games из мобильных сторов. Издатель решил, что отдавать 30 процентов комиссии с каждой покупки слишком много и сделал оплату в обход стандартного механизма In-app payment. Что, конечно, запрещено. И ни Apple, ни Google не захотели терять свой доход(хотя на некоторые послабления уже пошли Apple Google).

При этом нельзя забывать, что на самом деле оплата сторонними средствами возможна в приложениях, распространяемых в сторах. Например, можно продавать реальные товары(как в Ozon) или услуги(как Uber). Но нельзя продавать то, что потребляет пользователь в самом приложении(игровая валюта, скин на персонажа и т.п.)

COVID-19

Множество блокировок было ровно год назад. Большое количество разработчиков начали выкладывать разные приложения с ковид-тематикой(от агрегаторов статей и карт распространения до фейковых анализаторов на наличие болезни). При этом реальной информацией тогда еще мало кто владел.

Тут Apple и Google решили, что все такие приложения пора либо перестать публиковать, либо заблокировать, если уже прошли. Еще была схема с удалением из поисковой выдачи.

В итоге в мае сторы на поисковые запросы давали такие результатыВ итоге в мае сторы на поисковые запросы давали такие результаты

Apple ссылались на пункт 5.2.1 Apps should be submitted by the person or legal entity that owns or has licensed the intellectual property and other relevant rights.

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

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

Хранение данных

Когда-то я работал в компании, которой прилетел реджект за то, что мы храним данные приложения в iCloud'е пользователя. Делали мы это не специально, а по не знанию) При этом, как оказалось, какие-то данные хранить в iCloud можно, но это должен быть сгенерированный пользователем контент.

Это самый первый пункт из iOS Data Storage Guidelines: "Only documents and other data that is user-generated, or that cannot otherwise be recreated by your application, should be stored in the <Application_Home>/Documents directory and will be automatically backed up by iCloud. "

Убрали хранение данных в локальный кеш и смогли пройти ревью.

Пермишены

В iOS 14.5 стал обязательным запрос нового пермишена - про трекинг данных пользователя. Компании принялись рассказывать юзерам на предварительных экранах почему же надо разрешить трекинг и... некоторые столкнулись с блокировками как раз из-за онбординг экрана для пермишена. Дело было в добавлении двух кнопок на этот экран. По нажатию одной - запрашивалось разрешение, второй - нет. В гайдланах это строчка "If you display a custom screen that precedes a privacy-related permission request, it must offer onlyoneaction, which must display the system alert."

При этом, например, у facebook'а получалось проходить ревью с двумя кнопками При этом, например, у facebook'а получалось проходить ревью с двумя кнопками Но позже и facebook, и instagram заменили эти экраны на однокнопочныеНо Но позже и facebook, и instagram заменили эти экраны на однокнопочныеНо

Ссылки на другие приложения

Когда я начинал разрабатывать свои приложения, то пользовался кросслинками из одного в другое. И так мои новые игры набирали лояльную аудиторию из старых. Довольно неплохо работало. Делал я это максимально примитивно - ставил иконку нового приложения в угол экрана меню старых. А еще на экране подтверждения закрытия приложения третьей кнопкой был переход в новую игру. Но через какое-то время Google начал поочередно блокировать одно приложение за другим, т.к. "Ads must not simulate the user interface of any app". Оказалось, что к подобным переходам надо явно писать, что они являются рекламой.

Слишком взрослый рейтинг

Напоследок совсем забавная для меня формулировка. Первая от Google. Приложения не пропускали в стор из-за того, что google play решил, что поставлен слишком высокий возрастной рейтинг. Сделано это, чтобы не заморачиваться с контентом, который мог где-нибудь(например, в рекламе) появиться, а детям его показывать не стоит. Но google сказал, что "We determine that some elements of your store listing may appeal to children under 13: Animated characters in app icon, young characters". Пришлось понижать возрастной рейтинг и фильтровать "опасные" категории рекламы.

А с какими блокировками приходилось сталкиваться вам?

Подробнее..

Как сделать экран подтверждения СМС-кода на iOS

03.06.2021 16:16:28 | Автор: admin

Привет, Хабр!

Меня зовут Игорь, я Head of Mobile в компании AGIMA.

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

Важно: в примере кода на github будет полноценный пример с вводом номера телефона и кодом, но экран ввода номера телефона совсем скучный, поэтому сегодня мы вводим код :)

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

  • отправить код на сервер;

  • включить таймер повторной отправки + отобразить визуально;

  • после завершения таймера показать кнопку отправить еще раз;

  • отправить повторный запрос на получение кода;

  • отобразить все ошибки;

  • обработать успешное подтверждение кода.

Если попробовать разделить экран на UI и логику, получается примерно такое взаимодействие между логикой и интерфейсом.

Можно, конечно, отправить всю логику про таймеры и isLoading на View слой, но мне больше нравится относить это к логике. Особенно учитывая то, что я большой поклонник MVVM+Rx (и буду это использовать в статье), это более чем уместно смотрится. Ну да ладно.

ViewModel в этом случае играет роль некоего преобразователя пользовательских действий: у нее есть input и output (видно на картинке выше). За навигацию будет отвечать кто-то еще, например, координатор.

Со стороны UI нам будут интересны следующие компоненты:

final class ConfirmCodeViewController: BaseViewController {  /// поле ввода кода  private lazy var codeTextField = CodeTextField()  /// лейбл для отображения ошибок   private lazy var errorLabel = UILabel()  /// один лоадер для запросов на отправку кода и на повторный запрос кода  private lazy var loader = UIActivityIndicatorView()  /// лейбл с обратным отсчетом для повторной отправки кода  private lazy var timerLabel = UILabel()  /// кнопка повторной отправки кода  private lazy var retryButton = UIButton(type: .system)  /// это все будет в стеквью  private lazy var stackView = UIStackView()}

ViewModel будет выглядеть так:

/// Например, после успешного подтверждения кода нам могут предложить ввести перс. данныеenum AuthResult {case successcase needPersonalData}protocol ConfirmCodeViewModelProtocol {    /// Введенный пользователем код для подтверждения    var code: AnyObserver<String> { get }        /// Пользователь нажал на отправить повторно    var getNewCode: AnyObserver<Void> { get }        /// Результат подтверждения кода    var didAuthorize: Driver<AuthResult> { get }        /// Один индикатор на все запросы на этом экране    var isLoading: Driver<Bool> { get }        /// Ошибки из всех запросов на этом экране    var errors: Driver<String> { get }        /// Таймер отправки нового кода    var newCodeTimer: Driver<Int> { get }        /// Запросили новый код при нажатии на отправить заново    var didRequestNewCode: Driver<Void> { get }      /// Таймер отправки нового кода запущен    var codeTimerIsActive: Driver<Bool> { get }}

Обратите внимание, что при таком подходе мы стараемся не использовать PublishSubject, BehaviourRelay итп, чтобы четко разделить input и output у ViewModel. Теперь давайте это все свяжем.

View отдает следующие потоки данных:

let codeText = codeTextField.rx.text.share()codeText    .bind(to: viewModel.code)    .disposed(by: disposeBag)retryButton.rx.tap    .bind(to: viewModel.getNewCode)    .disposed(by: disposeBag)

ViewModel будет как-то (покажу ниже) обрабатывать ввод кода пользователя, а также делать запрос на повторную отправку кода, если мы нажмем на кнопку.

Сначала давайте посмотрим ViewModel целиком, далее разберем ее более подробно.

ViewModel рассмотрим по кусочкам:

let _codeSubject = PublishSubject<String>()self.code = _codeSubject.asObserver()let codeObservable = _codeSubject.asObservable()let validCodeObservable = codeObservable.filter { $0.count == codeLength }

_codeSubject это поток данных из textfield ввода кода.

validCodeObservable отфильтровывает значения нужной длины, которые мы будем отправлять на сервер.

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

let codeEvents: Observable<Result<Void, Error>> = validCodeObservable    .flatMap { (code) in        authService.confirmCode(code: code, token: token).materialize()    }.share()

Собственно, отправка кода на сервер :) Обращаем внимание на .materialize(). Поскольку мы планируем использовать этот Observable в реактивных цепочках, мы не хотим получить ошибку и прерывать их. materialize позволяет завернуть все значения и ошибки в Result<Value, Error> и тем самым мы никогда не прервем реактивную цепочку из-за ошибки.

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

Состояние загрузки

Здесь довольно интересный момент. Если мы получили валидный код, готовый к отправке, то мы отображаем интерфейс загрузки. Если мы получили ответ от сервера, это означает, что нам надо скрыть состояние загрузки. Таким образом, мы можем взять эти потоки данных (на примерах выше), смаппить их в true или false и забиндить в isLoading.

didAuthorize = codeEvents.elements()...

.elements() работает как фильтр и пропускает только значения из codeEvents и игнорирует ошибки. Напомню, что тип значений у codeEvents это Result<Void, Error> , что является частью RxSwiftExt.

Таймер повторной отправки кода

Таймер включается при следующих событиях:

  • мы отправили код на подтверждение (validCodeObservable.mapTo(Void()));

  • мы перезапросили код (didRequestNewCode);

  • сразу же при заходе на экран (.startWith(Void())).

Именно это описано в строчке Observable.merge... Сам таймер делается стандартными средствами RxSwift. Останавливаем таймер с помощью оператора take(while:), пока значение таймера не станет равно 0.

Лейбл с таймером и кнопка переотправить должны скрываться/показываться в зависимости от того, активен ли таймер:

viewModel.codeTimerIsActive    .drive(retryButton.rx.isHidden)    .disposed(by: disposeBag)        viewModel.codeTimerIsActive    .not()    .drive(timerLabel.rx.isHidden)    .disposed(by: disposeBag)

За ошибки отправки и запроса нового кода у нас будет отвечать один поток данных errors.

errors = codeEvents.errors().merge(with: fetchNewCode.errors())            .compactMap { ($0 as? ErrorType)?.localizedDescription }            .asDriver(onErrorJustReturn: "")

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

viewModel.isLoading    .not()    .drive(codeTextField.rx.isEnabled)    .disposed(by: disposeBag)

ViewModel получилась довольно-таки тестируемая, поэтому давайте напишем тесты! Я приведу примеры тестов, которые будут показывать, как ViewModel реагирует на пользовательский ввод. Создадим вспомогательный метод, который будет создавать поток событий ввода кода. Внимание, используется RxTest!

class ConfirmCodeViewModelTests: XCTestCase {    // properties// methods     //MARK:- Helpers    private func bindCodeInputEvents(        _ events: [Recorded<Event<String>>] = [.next(100, "1"), .next(200, "11"), .next(300, "111"), .next(400, "1111")])    {        codeInputEvents = scheduler.createHotObservable(events)        codeInputEvents.bind(to: viewModel.code).disposed(by: disposeBag)    }}

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

   func test_timerInvokedAutomatically() {        let sut = scheduler.start(created: 0, subscribed: 0, disposed: 1000) { self.viewModel.newCodeTimer }        XCTAssertEqual(sut.events, [.next(1, 2), .next(2, 1), .next(3, 0)])    }

Или вот такой: проверим, что у нас передается на UI событие об ошибках

 func test_errorEmmitedValueAtFailure() throws {        bindCodeInputEvents()        setConfirmCodeResult(.error(0, MockError.confirmFailure))         let sut = scheduler.start { self.viewModel.errors }        XCTAssertEqual(sut.events, [.next(400, "confirmFailure")])    }

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

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru