Бібліотека для тестів з заповненням пропусків



Не завжди ж тільки користуватися. Саме з такої думки почалося написання бібліотеки під Android і подальше написання цієї статті. Раптом кому стане в нагоді. Під катом те, що в підсумку вийшло.

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

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

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

  • View повинна бути максимально кастомизируема.
p.s.
Тут не буде поетапного розписування як написати бібліотеку і як її опублікувати в інтернеті, і в тому числі на Хабре, вже є вказівки на цей рахунок. Я трохи розповім про свою логіку, про ті проблеми і труднощі, з якими зіткнувся і як їх вирішував. Комусь це здасться банальним, комусь неправильним – не претендую на вірне рішення і всі зауваження врахую. Сподіваюся, що кому-то знадобиться ця стаття або бібліотека. Посилання на github буде в кінці. В описі до сховища є посилання на демо бібліотеки та інструкція до неї.

перший Етап – побудова дерева
До цього етапу у мене була моя первісна демка, вимоги і тверда впевненість, що все вийде. Насамперед було вирішено взятися за малювання, а перетворення рядка в дерево залишити наостанок.

так, логіка дерева така:

Лист – це набір символів. Наприклад, знак множення або якесь число. Лист знає про своїх розмірах, про те, є чи після нього хтось, і як їх розташувати відносно себе. Я виділив кілька видів.

  1. Constable лист. Це +,-,* і числа. Цей лист має одиничну висоту та довжину в залежності від внутрішнього тексту. Все, що за ним, малюється на одній осі з ним. Тобто центр цього листа по осі Y збігається з центром по осі Y, що є за ним.

  2. Changeable лист. Він такий же як і constable, тільки у нього є стани порожній і заповнений. Висота – одиничний блок. Ширина ж береться не з його нутрощів, а по найдовшому тексту з усіх змінних і переміщуються аркушів.

  3. Movable лист. Схожий на changeable, тільки до нього стану бачимо, невидимий, переміщується.

  4. Power лист. Він має ширину по розміру того, що повинно бути в мірі. І особливість його в тому, що всі за ним малює не на одній осі з попереднім листом. Нижній край всієї подальшої області повинен бути зміщений на підлогу блоку щодо попереднього листа. Звучить замудрено, але треба просто приставити число ступеня. (22) Ступінь малюється на підлогу рядки вище. Тільки на відміну від цього ступеня, в моїй число мірою не зменшується за шрифтом. Цей лист знає про те, хто наступний у відображенні ступеня, і хто наступний після відтворення ступеня. Тобто у формулі (2^(1+3)-4) знак ступеня знає про «1» та «-».

  5. Division лист. Як зрозуміло з назви – це лист, що позначає поділ. Він малюється в 4 етапи. Малюється лінія по центру, довжиною як більше з значень довжини чисельника і знаменника. Малюється чисельник так, щоб він був притиснутий до лінії нижнім краєм і був розташований по центру від всієї довжини лінії (на випадок, якщо знаменник довше). Малюється знаменник так, щоб він був притиснутий верхнім краєм до лінії і був розташований по центру від всієї довжини лінії (на випадок, якщо чисельник довше). І в кінці передається відтворення на ті листи, що йдуть після розподілу.


Щоб логіка працювала, кожен аркуш повинен вміти:

  1. Сказати свою довжину і висоту
  2. Сказати довжину і висоту себе і того, що знаходиться за ним. Не просто наступного листа, а всієї гілки листя. Тобто у формулі 2+3-4 знак «+» знає розміри частини «+3-4». Це допомагає визначити розмір всієї формули.
  3. Сказати, наскільки йому треба зрушити від центру по осі Y (правило введено спеціально для Power листа)
  4. Вміти промалювати себе і правильно передати координати для відтворення всім своїм нащадкам.
Етап другий — масштабування блоків і тексту
Щоб максимально заповнити дозволену область, перед відображенням йде етап масштабування. Принцип простий. У мене є одиничний блок, на основі якого йдуть всі розрахунки і змінна, яка відповідає за те, у скільки разів треба збільшити масштаб. Логіка масштабування така: отримуємо розмір формули, порівнюємо з розмірами доступною області, змінюємо коефіцієнт масштабування.

І тут я зіткнувся з невеликою проблемою. Ширина тексту не збільшується пропорційно атрибуту textSize. Тобто, нехай у нас був блок тексту шириною y і textSize x. Якщо ми збільшимо textSize в 2 рази – ширина блоку не буде дорівнює 2*y. У зв'язку з цим, значення масштабування постійно коливалося у близьких значеннях, з-за чого картинка при перемальовуванні «тремтіла». Для вирішення цієї проблеми я бачив 2 способи:

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

  2. Ставити перед кожним кроком масштабування формулу в одні і ті ж умови. Для цього я додавав до коефіцієнту 0.5 і округлял в більшу сторону.
Пішовши по другому методу, я зіткнувся з проблемою в самому першому розрахунку, коли коефіцієнт ще дорівнює 1. Що б вирішити це, спочатку, я робив розрахунок за другим методом 2 рази, але це позначилося на продуктивності. Щоб уникнути другого зайвого розрахунку, було вирішено зберігати попереднє значення і порівнювати його з новим. Якщо вони однакові, то другий раз вважати сенсу немає. Подумавши ще, я виніс перерахунок у метод onSizeChanged, а так само додатково викликав при заданні нової формули/аркушів для руху.

Етап третій — рух
Тут все дуже просто. Була прийнята маленька хитрість. Замість руху аркуша, який ми виділили, ми приховуємо його і заводимо новий, координати якого постійно змінюємо при спрацьовуванні onTouchEvent. Коли спрацьовує MotionEvent.ACTION_DOWN, ми шукаємо, не потрапило наше дотик на двигаемый лист, приховуємо і копіюємо його значення в лист, який рухаємо. Коли спрацьовує MotionEvent.ACTION_UP, ми пробігаємо по дереву і дивимося, не перетинаємося ми з яким-небудь Changeable листом.

Етап четвертий — парсер
Тут в якійсь мірі рішення в лоб. Для початку було визначено 2 спец. символу. Перший вказує на ті блоки, які стануть Changeable. Другий символ — парний, вказує область, яку слід визначити як Constable або Changeable без внутрішніх перетворень (наприклад, символ ділення). Отримуємо рядок, видаляємо всі прогалини, що не входять в область виділення другим спец. символом, перетворимо знаки поділу і починаємо посимвольно зчитувати. Щоб можна було зручніше все перетворювати, знаки ділення з виду 2/3 перетворюються в /(2)(3).

Етап п'ятий – кастомізація
Для простоти кастомізації, майже всі змінні, які фігурують в коді відтворення або парсера, винесені в окремі поля класів. У зв'язку з чим можна успішно змінити майже все, що бачиш.

Висновок
Як виявилося, дана бібліотека цілком підходить під «формули» не тільки математичні, а й, наприклад, на перевірку знань словникових слів. Ще хотілося б додати на майбутнє, для тих хто теж вирішить писати свої бібліотеки – в android studio є створення модуля-бібліотеки, і видаляйте всі з тега application в маніфесті бібліотеки. Якщо є які-небудь питання – з радістю відповім. Всі зауваження та побажання врахую.

Бібліотека на github
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.