Прийшов час осідлати справжнього Буцефала🏇🏻Приборкай норовливого коня разом з Newxel🏇🏻Умови на сайті
×Закрыть

Весела математика у інтерфейсах

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Я початківець у iOS розробці (але не в програмуванні в цілому), і поки моя мрія про власну програму на паузі через затримку у сплаті за акаунт розробника, є час поділитися своїм вирішенням цікавої задачі, яка трапилась під час роботи. Тим паче що цей елемент зустрічається доволі часто. Але я сподіваюсь, що цей метод може бути корисним комусь навіть для задач іншого плану або надихне на вдосконалення, тому я вирішив поділитися саме детальним роз’ясненням того, як метод працює.

В процесі написання свого застосунку для iOS, виникла необхідність написати свій Page Control у вигляді кружечків, що відображають поточну позицію в списку карток.

Для початку я поясню суть задачі. Найпростіший варіант Page Control виглядає як на першій анімації нижче:

Суть проблеми у тому, що ми маємо обмеження по ширині, та на якомусь етапі кількість елементів досягає такого рівня, що загальна ширина page control вилізе за означені межі. Оскільки я не знайшов готового вирішення проблеми, яке б мені подобалось, я намалював декілька варіантів, і обрав ті, які б на мою думку були прийнятними. Відкинувши всі варіанти з пропуском елементів на кшталт трьох крапок всередині, або перетворення у слайдер, щоб користувач чітко і швидко усвідомлював, на якій позиції знаходиться, виділив наступні:

  1. Робити всі елементи меншими, щоб задовольняти обмеження за шириною (але поточний елемент не буде гарно видно при малих розмірах).
  2. Залишати один елемент первісного розміру, і однаково зменшувати всі інші.
  3. Зробити плавний перехід з первісного розміру елементу до меншого через зазначену кількість кроків.

Я обрав варіант #3, тому що він дає найцікавіший і найпривабливіший вигляд, та водночас ми чітко бачимо поточну сторінку, тобто найбільший виділений елемент. До того ж, задля досягнення гармонійного переходу, було вирішено зробити перехід через функцію косинусу.

Результат наочно показано у наступній анімації.

1. Рахуємо кроки

Приймемо базовий розмір елементу за w та позначимо його на графіку. Кінцева ціль розрахунків це знаходження розміру найменшого елементу. Всі елементи за межами однієї хвилі косинусу матимуть такий самий розмір як найменший елемент на кінці хвилі.

Головна умова та суть задачі це зберігання постійної однакової ширини усього Page Control, тобто сума всіх елементів має бути постійною.

Для меншої кількості елементів можна вручну ввести обмеження на розмір елементу, що й було застосовано на анімації вище. Без обмеження бокові елементи ставатимуть навпаки більшими за умови нестачі елементів. Також, за умови дуже малої ширини, розміри можуть приймати від‘ємні значення. В межах цієї статті я не вводитиму ці обмеження щоб не ускладнювати статтю.

Для початку побудуємо бажаний графік:

Підбираємо функцію, яка даватиме нам відповідний графік. Приведена нижче функція дає результат в діапазоні від 0 до 1, що буде в нагоді для обчислення відносного коефіцієнту для обчислення базової величини зменшення w, через який обчислимо розмір найменшого елементу.

0.5cos( p )+0.5 , де p — це кут у радіанах.

Запам’ятаємо цю формулу, вона знадобиться нижче для розрахунків. Графік:

Наступним кроком вводимо величини, необхідні для обчислення розмірів кожного елементу.

l — загальна ширина Page Control (сума всіх елементів)

w — базовий розмір одного елементу

c — загальна кількість елементів у рядку Page Control

s — шуканий розмір найменшого елементу

x — обратно пропорційний коефіцієнт зменшення розміру w, від 0 до 1. Наприклад, при x=0.2 , s=w*0.8

\alpha — прямо пропорційний коефіцієнт зменшення коефіцієнту x

Припустимо, що базовий розмір це w. Оскільки менший розмір буде зменшено на x, то найменший елемент розраховуємо за формулою s=w(1-x). Для розрахунку проміжних розмірів недостатньо використовувати x, тому вводимо додатковий коефіцієнт \alpha , який дозволить брати частину x в діапазоні від 0 до 1. Тож тепер будь-який розмір можна розрахувати за формулою s=w(1-x\alpha). За даною формулою для кожного елементу змінюватиметься тільки \alpha, тому сума всіх елементів розраховуватиметься так:

w(1-x\alpha _{1}) +w(1-x\alpha _{2})+w(1-x\alpha _{3}) \dotsc +w (1-x\alpha _{c})

де c — це кількість елементів (див. останній індекс). Тож для знаходження загальної ширини можемо вивести наступну остаточну формулу:

l=wc( 1-x\overline{\alpha })

де \overline{\alpha } це середнє арифметичне всіх \alpha від 1 до c.

Оскільки наша мета це знаходження розміру елементу, нам потрібно вивести x з цієї формули, що ми і зробимо. Для простоти розрахунку та запобігання помилок я використав сервіс WolframAlpha (нехай пробачить мене шкільна вчителька з математики Тетяна Володимирівна).

x=\frac{cw-l}{\overline{\alpha } cw}

2. Малюємо хвилі

Все що нам залишається це розрахувати всі \alpha для кожного елементу та знайти для них середнє арифметичне. Пам’ятаєте нашу функцію з косинусом?

0.5cos( p )+0.5

Змістимо кут на \pi радіан вліво, щоб інвертувати відносно графіку з розміром елементів, та отримаємо формулу:

\alpha=0.5cos( p - \pi )+0.5 , де p це кут у радіанах (або відстань від центрального елементу).

Побудуємо графік із зазначенням необхідних величин:

Ви помітили? Звичайно! Це ж інвертований графік з попереднього малюнку у діапазоні ще невідомого x :)

Згадаємо правило, розроблене вище, яке визначає вигляд нашого Page Control. Значення p для останнього елементу переходу завжди припадає на \pi радіан. Тож для всіх елементів за межами однієї хвилі визначаємо p=\pi, що даватиме пряму на графіку та однаковий розмір для всіх наступних елементів.

На графіках у прикладі напівхвиля розбита на 4 секції (далі у прикладі в коді це визначатиметься як 5 елементів у напівхвилі, найбільший та найменший включно). Тож для найбільшого елементу розрахунок прийме вигляд:

\alpha =0.5cos( 0-\pi ) +0.5=0 , а отже

s=w( 1-x*0) =w , тобто повний розмір.

А для останнього елементу хвилі розрахунок буде таким:

\alpha =0.5cos( \pi -\pi ) +0.5=1 , а отже

s=w( 1-x*1) =w( 1-x) , тобто базовий розмір зменшений на повний x, вийшов найменший елемент.

Що далі? Далі ми розраховуємо \alpha для кожного елементу, та підставляємо у формулу, виведену у попередньому параграфі.

3. Будуємо результат

Теорія це звісно цікаво, але набагато цікавіше створити щось де вона буде корисна. Тому просто підсумуємо всі підрахунки за допомогою коду. Я використав мову Swift, але я впевнений, що вам не складе труднощів переписати це мовою, якою користуєтесь саме ви, синтаксис простий для розуміння.

Все спрацювало, розрахунки правильні. Перевірка наприкінці тестового коду дала бажану ширину 160 пунктів.

Продублюю варіант побудови реального елементу інтерфейсу, заснованого на вищезазначених розрахунках. Виглядає значно краще анімованим ;)

Тож, що можна винести з цієї статті? Я, власне, зміцнився у переконанні, що набагато веселіше проявляти креативність і вигадувати нестандартні рішення хоча б час від часу. Це не дозволяє засумувати і дає привід отримати задоволення від результату своєї роботи. Та й хто з нас не хоче відчути трішечки естетичної насолоди...

А як вважаєте ви? Додавайте свої цікаві рішення у коментарях.

LinkedIn

Похожие статьи

Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

«Матан нінада нікада» ©

Стаття класна.

Що цікаво, на телефоні приклад виглядає нормально, а на великому екрані через саккаду — ні.

Дякую)))

Це ще й через те що я полінився морочитись з gif файлом, і fps замість оригінальних 60, мабуть 30 вийшло при конвертації. На 60 fps воно інакше виглядає.

Враження двоякі.
З однієї сторони — молодець, що спробував написати щось кастомне і можливо це буде цікаво новачкам. А з іншої — це не практичне рішення, яке забирає час на розробку, тестування та й не універсальне.

Чи є в контрола шанс на reuse? — ні.
Цей контрол треба буде допрацювати для випадків Accessibility, Dark mode, щоб зробити його потенційно можливим для масового використання.
А обмеження по кількості елементів — великий мінус дизайну компонента, через який він так і не буде використаний іншими.

P.S. Keep it simple, не треба робити оver engineering :)

Якщо буде час, то зроблю з годом версію для реюза у маси, просто поки що не на часі (власне я можу реюзати дуже легко, але ж не про це стаття). У мене в додатку причудово працює dark mode доречі, не було нічого складного тут)
Про обмеження елементів — мені саме такий контрол був потрібен, це підходить для проекту на всі сто. Можете загуглити «Page control swift», там обмежена кількість елементів, його розробила Apple, і воно багато де використовується. В моєму контролі набагато більше елементів вміститься, тож тут посперечався би, бо саме для мого випадку це перекрило всі потреби)) Для сотні елементів вже не підійде, про це мови й нема...
Та й для користувача це дуже просто, він же бачить лише результат :))
В будь якому разі, дякую за вашу думку ✌️

Easy! Хлопець мав вільний час, доклав певних зусиль для вирішення задачі і поділився цим з народом. А критика така, ніби він тут щось продає. :)

Ніхто не каже що він щось продає. Статтю треба позначати як pet project, або prototype. В кінці додати висновок що на реальному проекті таке не робити, бо є такі плюси і мінуси :)
DOU читають різні за досвідом люди, тому початківцям бажано зазначити, що так робити на комерційному проекті не бажано, бо це може спричинити додаткові проблеми, які буде в рази складніше вирішити ніж з дефолтним компонентом.
Математика — круто, ініціатива — теж (в конкретному прикладі), але треба розуміти що є тех завдання і його треба робити за принципом KISS (YAGNI).
Без цього висновку стаття мотивуватиме початківців писати складні, в більшості випадків непотрібні рішення замість використання існуючих компонентів, що в свою чергу породжує більшу ймовірність дефектів, а також ускладнює адаптування коду під нову версію операційної системи :)

Навіть більше, я ще й нікого ні до чого не закликаю)) А програмування взагалі складно і мотивує початківців програмувати недосконало, але попереджень через це ще не бачив :)
PS: я зробив це на реальному проекті :)))

Изинги — хорошая вещь, иллюстрация тут:

https://easings.net/

формулы у этих же ребят на гитхабе можно посмотреть

Задача состоит в том, чтобы вычислить ряд размеров, которые удовлетворят данной функции при сохранении их суммы. Сама функция — не суть, подставляем любую. Мне понравилась косинусоида в тот день)
Для других целей я использовал те изинги по вашей ссылке не раз, но не в этом был вопрос в этот раз.
Но все-равно спасибо))

Штуковина симпатичная, но все же возникает вопрос в рациональности решения с точки зрения UX и a11y.

або перетворення у слайдер

Почему нет? Если у меня 100500 страниц (допустим, книга) то слайдер с указанием номера текущей страницы — это же самое то.
|—————-53/205———————————-|
Его можно легко подружить с динамичными шрифтами и voice over — вообще красота.

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

Оскільки я не знайшов готового вирішення проблеми

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

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

Насчет множества страниц, конкретно в моем приложении будет не более 30 элементов, большинство пользователей будут использовать не больше 10. Поэтому ответ здесь прост, дизайн делался под конкретный проект и в нем абсолютно уместен, до 30 элементов выглядит все отлично. Для книги, естественно не подойдет и я делал бы слайдер, ваш вариант из комментария вполне рассмотрел бы)) Это же будет ответом и на последний вопрос, как я уже сказал, мой элемент подходит для охвата до 30 элементов, чтобы это было красиво.

Кстати, Apple вполне себе сделала PageControl, он простой и может держать немного элементов в плане дизайна, что также ответ на последний вопрос насчет готового решения, оно есть и его недостаточно в конкретном проекте (хотя если бы и не было... ;) ))) Посмотрите на стандартный «PageControl» от Apple, чтобы наглядно увидеть, чему именно я искал замену. Мне нужно было больше элементов, лучше наглядность и удобство выбора конкретного элемента. Ну и на 60 fps выглядит реально лучше, gif не совсем передает ощущение.

Спасибо за конкретный вопрос со смыслом)

Нащо воно потрібно? Тупо зробив 3-4 растрові кружечки різного діаметру, ото й всі діла. Ти забагато вигадуєш що до моделі пам′яті людей, що простіше модель, то лояльніший користувач.

«Не змушуйте мене думати» — ось принцип.

smoothstep вместо всей этой простыни

Хотя статья о том, как именно посчитать самому (как это работает внутри), а не о том, как использовать конкретную библиотеку, ваш вариант решения с готовой библиотекой приветствуется, обязательно добавляйте)

это не готовая библиотека а название способа интерполяции, который знает каждый школьник тот кто хоть чуть чуть сталкивался с графикой.
вот пример я накидал за две минуты
www.desmos.com/calculator/cna8ajrn8g

Опять же. Статья о конкретном способе решения, с подробностями, она не перечисляет всевозможные методы, описывая преимущества/недостатки, а подробно описывает суть работы одного конкретного.
Мне правда интересно увидеть ваш вариант решения, тем более если он будет работать быстрее, либо код короче/понятнее. То, что в калькуляторе это не решение задачи из статьи, это просто построение графика функции. У меня полезного кода вышло не более 25 строк. Уверен, вам будет несложно решить задачу вашим методом, раз он намного проще.
PS: На случай недопонимания. Задача состоит в том, чтобы вычислить ряд размеров, которые удовлетворят данной функции при сохранении их суммы. Подставить в мое решение можно любую функцию, включая ту, которую вы написали в калькуляторе (то есть, вообще неважно, какая там функция вместо косинуса, это не является хоть малость важным моментом). Поэтому, если можно то опишите конкретнее суть упрека. Вам лишь не нравится косинус как выбранная функция перехода? Либо вы предлагаете именно более простое нахождение всех значений ряда?

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

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

Подписаться на комментарии