Что такое view android
View-элементы
Первое понятие, необходимое для работы с Android — это представления (или View-элементы).
Базовые элементы Views курс Android Basics
Знакомство и подробное рассмотрение типов элементов Views в Android. Элемент View — первое понятие, которое необходимо для начала работы
Дата загрузки: 2017-06-16T12:04:00
«Верблюжий стиль» (Camel case) — это соглашение, распространенное не только в программировании. Он встречается при использовании FedEx, прослушивании iPod, создании PowerPoint, и даже в McDonalds!
В этом уроке было много новых слов:
Макет | Layout |
Пользовательский интерфейс | User Interface |
Текстовое поле | TextView |
Картинка | ImageView |
Кнопка | Button |
Верблюжий стиль | Camel case |
Не бойтесь, их не нужно запоминать. Их можно отыскать в любой момент, перейдя по этой ссылке. Хотите верьте, хотите нет, но профессиональные разработчики не зубрят все подряд, для них поиск информации — один из ключевых навыков. Эти понятия всегда можно подсмотреть в Словаре терминов.
Свои ответы и вопросы можете оставлять в комментариях.
Основы создания интерфейса
Введение в создание интерфейса
Простые объекты View представляют собой элементы управления и прочие виджеты, например, кнопки, текстовые поля и т.д., через которые пользователь взаимодействует с программой:
Большинство визуальных элементов, наследующихся от класса View, такие как кнопки, текстовые поля и другие, располагаются в пакете android.widget
При определении визуального у нас есть три стратегии:
Создать элементы управления программно в коде java
Объявить элементы интерфейса в XML
Создание интерфейса в коде java
Для работы с визуальными элементами создадим новый проект. В качестве шаблона проекта выберем Empty Activity :
Пусть он будет называться ViewsApp:
Определим в классе MainActivity простейший интерфейс:
Если мы запустим приложение, то получим следующий визуальный интерфейс:
Подобным образом мы можем создавать более сложные интерейсы. Например, TextView, вложенный в ConstraintLayout:
Далее определяется позиционирование. В зависимости от типа контейнера набор устанавливаемых свойств может отличаться. Так, строка кода
указывает, что левая граница элемента будет выравниваться по левой ганице контейнера.
указывает, что верхняя граница элемента будет выравниваться по верхней ганице контейнера. В итоге элемент будет размещен в левом верхнем углу ConstraintLayout.
Для установки всех этих значений для конкретного элемента (TextView) в его метод setLayoutParams() передается объект ViewGroup.LayoutParams (или один из его наследников, например, ConstraintLayout.LayoutParams).
Опять же отмечу, что для конкретного контейнера конкретные действия могут отличаться, но как правило для всех характерно три этапа:
Создание объекта ViewGroup.LayoutParams и установка его свойств
Передача объекта ViewGroup.LayoutParams в метод setLayoutParams() элемента
Передача элемента для добавления в метод addView() объекта контейнера
Хотя мы можем использовать подобный подход, в то же время более оптимально определять визуальный интерейс в файлах xml, а всю связанную логику определять в классе activity. Тем самым мы достигнем разграничения интерфейса и логики приложения, их легче будет разрабатывать и впоследствии модифицировать. И в следующей теме мы это рассмотрим.
Introduction to Android Views and ViewGroups
All the interaction of a user with the Android application is through the user interface(UI), hence it is very important to understand the basics about the User Interface of an android application. Here in this tutorial, we are going to cover about various Views and ViewGroups and will try to explain how they can be used to design the User Interface of an android application.
Views
View can be considered as a rectangle on the screen that shows some type of content. It can be an image, a piece of text, a button or anything that an android application can display. The rectangle here is actually invisible, but every view occupies a rectangle shape.
match_parent means it will occupy the complete space available on the display of the device. Whereas, wrap_content means it will occupy only that much space as required for its content to display.
A View is also known as Widget in Android. Any visual(that we can see on screen) and interactive(with which user can interact with) is called a Widget.
XML syntax for creating a View
Now, as we have explained earlier as well, to draw anything in your android application, you will have to sepcify it in the design XML files. And to add functionality we will create Java files.
Every view in XML has the following format:
So, every View subclass needs to follow this format so that it can appear on the screen of the app. And this format is nothing but default XML style. Right!
These attributes define the size of the invisible rectangle that a view makes. Using these attributes we can easily control the size for every view in our android application.
Most commonly used Android View classes
Here we have some of the most commonly used android View classes:
Programmatic and Declarative Approach
To create/define a View or a ViewGroup in your android application, there are two possible ways:
So addView() is the function used to add any View to the UI and setLayoutParams() function is used to set the various attributes.
Создание собственной View под Android – может ли что-то пойти не так?
«Дело было вечером, делать было нечего» — именно так родилась идея сделать вью с возможностью зума, распределяющую юзеров по рангам в зависимости от кол-ва их очков. Так как до этого я не имел опыта в создании собственных вьюшек такого уровня, задача показалась мне интересной и достаточно простой для начинающего… но, *ох*, как же я ошибался.
В статье я расскажу о том, с какими проблемами мне пришлось столкнутся как со стороны Android SDK, так и со стороны задачи (алгоритма кластеризации). Основная задача статьи – не научить делать так называемыми “custom view”, а показать проблемы, которые могут возникнуть при их создании.
Тема будет интересна тем из вас, кто имеет мало (или не имеет вовсе) опыта в создании чего-то подобного, а также тем, кто хочет словить лулзов с автора в сто первый раз уверовать в «гибкость» Android SDK.
1. Как это работает?
Для начала кратко опишу то, как устроено сделанная вьюшка:
Иерархия (зеленым отмечены собственные вьюшки)
RankingView
RankingView отображает ранг (слева) и UsersView (справа).
GroupView
Пожалуй всё со скучной частью, переходим к проблемам.
p.s. Ссылка на исходники в «Заключении».
2. Android SDK «хочет сыграть с тобой в игру» © Goog… Пила
Начнем с безобидного.
2.1. Inflate разметки внутрь кастомного View с использованием DataBinding
DataBinding с её генерацией кода творит чудеса:
Пару строк и через переменную binding доступны по id все вью, указанные в разметке, какой бы сложной эта разметка не была. Никаких больше:
…И это вполне логично, хоть и не приятно, ведь один из LinearLayout ’ов избыточен. Что же делать? Обойти подобное достаточно просто. Нужно воспользоваться тегом внутри разметки, обернув в него всё, что должно быть внутри вашего вью, как это описано здесь.
Как итог, на данный момент нет способа использовать DataBinding в собственный вьюшках, не наплодив дополнительных Layout ’ов, что не лучшим образом скажется на производительности. ButterKnife по-прежнему наше всё.
2.2. Measure & Layout passes
Несмотря на то, что я не имел опыта в создании вьюшек, я всё же читал изредка попадавшиеся на глаза статьи по этой тематике, а также же видел раздел документации, посвященной теме «как вьюшки отрисовываются». Если верить последней, то всё проще простого:
В моем случае внутри UsersView.onLayout() происходит изменение Y-координат вьюшек, из-за чего некоторые вьюшки становятся видны, а другие прячутся, и это приводит к… (внимание на правый низ):
2.3. ScrollView не разрешает менять размер ребенка
Давайте по порядку. Как вообще можно изменить высоту элемента? Через LayoutParams.height конечно же, больше никак. Проблема решена? Нет. Высота осталась неизменной. Что же произошло? Изучив onMeasure() в дочернем вью, я пришел к выводу, что ScrollView просто игнорирует установленный height в параметрах, отсылая ему сначала onMeasure() с mode равным « UNSPECIFIED », а затем onMeasure() с « EXACTLY » и значением height ’а, равным размеру ScrollView (если установлен fillViewport ). А так как единственный способ изменить height вьюшки – изменить его LayoutParams – то ребенок и не меняется.
Решений я нашел два:
Ах да, проблема с неизменяемым размером ребенка у ScrollView есть на багтрекере, но как это обычно и бывает с Android’ом, оно всё ещё в статусе New с 2014-го года.
2.4. Закругленные углы у background
Сабж (слева снизу и слева сверху):
Как ни странно, у класса ShapeDrawable нет методов для работы с закругленными углами, как можно было бы ожидать. Но к счастью, есть наследник RoundRectShape и PaintDrawable (зачем нужен этот класс – не спрашивайте, сам в шоке), у которых присутствуют недостающие методы. На этом проблему для практически всех приложений можно было бы считать решенной, но не для данной задачи.
Специфика задачи такова, что максимальный zoom in может быть каким угодно, а значит вьюшка с её background ’ом сильно растянутся, что приводет к…
Logcat: W/OpenGLRenderer: Path too large to be rendered into a texture
Выглядит это так, что после превышения некоторого размера, background просто перестает отображаться. Как можно понять из предупреждения, некий Path слишком большой, чтобы можно было его отрисовать в текстуру. Чуток покопавшись в исходниках, я пришел к выводу, что виной всему этот товарищ:
… в mPath которого и помещают закругленный прямоугольник:
Но, к сожалению, у данного подхода есть относительно существенный недостаток: canvas.clipPath() не подвержен antialias. Однако за неимением другого способа сделать подобное, приходится довольствоваться этим.
2.5. Позиционирование View внутри FrameLayout
2.6. Видишь отступы шрифта? Нет? А они тебя видят! © includeFontPadding
Когда юзеры объединяются в группу, добавляется иконка, отображающая кол-во юзеров в группе. Вот как она выглядит сейчас:
А вот как она выглядела раньше:
Замечаете разницу? Есть неприятное чувство, когда смотрите на вторую картинку? Если да, то вы меня понимаете. Я долго не мог сообразить, почему, когда я смотрю на эту иконку, она выглядит не так привлекательно, как предполагалось. Догадавшись взять пэинт в руки и подсчитать, сколько же пикселей слева/справа/сверху/снизу от текста, я понял причину – текст не центрирован до конца. Что-то явно не так гравитацией текста. Да. Нет. Гравитация была установлена верно, другие параметры тоже. Всё выглядело идеально.
2.7. У ViewPropertyAnimator нет метода reverse()
Хотелось мне сделать анимацию сокрытия иконок рангов при определенном размере. Выглядеть это должно было так:
В идеале, здесь должен был бы быть какой-нибудь метод вида setReverseDuration() (без параметров), который бы понял, что, «ага, я проигрывал анимацию fade ’а 500мс, поэтому столько же и будет играть reverse-анимация». Но такого нет, увы. Единственный выход, что мне удалось найти, делать подобное ручками. В моем случае анимация была довольно простая, так что мне хватило этого для скрытия:
… и этого для показа:
Ну а дальше как обычно: view.animate().setDuration((long) realDuration) — и всё в ажуре.
2.8. ScaleGestureDetector (он же «пинч», он же «зум»)
Сам по себе API у ScaleGestureDetector довольно хороший – повесил listener и ждешь себе эвенты, не забыв передавать все onTouchEvent() ‘ы в сам детектор. Однако, не всё так радужно.
2.8.1. Небольшие замечания
И таким его советуют делать все stackoverflow-ответы (пример). Однако такой подход обладает недостатком. Скролл происходит даже тогда, когда вы производите пинч. Может показаться что это пустяк, но на деле очень неприятно случайно листнуть вьюшку, когда пытался её прозумить.
Я был готов долго и нудно рыскать в поисках сложного решения разграничения ответственности между super.onTouchEvent() и scaleDetector.onTouchEvent() … и я и правда искал… Но как оказалось, решение было ужасно простое:
Гениально, да? super.onTouchEvent() не отслеживает id пальца, которым был произведен скролл в первый раз, поэтому даже если вы начали скролл пальцем №1, а закончили пальцем №2 – ему норм, схавает. К сожалению, я так был уверен, что Android SDK в который раз вставит палки в колеса, что удосужился попробовать подобное только после: гуглинга и изучения исходников. Что сказать, Android SDK умеет иногда работать как надо удивлять.
Это совершенно никогда не заметно, но его дочерние вью не всегда могут быть одной высоты, ведь пиксели – атомарные единицы. То есть, если LinearLayout имеет высоту 1001 и у него 2 дочерних элемента, то один из них будет размером 501, а другой 500. Заметить это на глаз практически нереально, но вот косвенные последствия могут быть.
Ситуация редкая и не слишком критичная (было не так уж просто (но и не сложно) двигать мышкой так медленно, чтобы обнаружить не скрытые и скрытые иконки одновременно). Но всё же если вам не нравится такое поведение, исправить это можно только одним способом – не использовать getHeight() для сверки с порогом. В onSizeChanged() внутри LinearLayout ’а находите наименьший размер у всех дочерних элементов и оповещаете всех о том, чтобы они сравнивали порог именно с этим числом. Я назвал это shared height и выглядит у меня это так:
А сама сверка с пороговым значением так:
2.8.2. Проблемы
2.8.2.1. Минимальный пинч
И-и-и-и-и… конечно же у класса нет методов для изменения этого поля, и конечно же com.android.internal.R.dimen.config_minScalingSpan равен магическим 27mm. Для меня вообще существование минимального пинча представляется очень странным явлением. Даже если и есть смысл в подобном, почему не дать возможность его изменить?
Решением проблемы как обычно является рефлексия.
2.8.2.2. Слоп
Для тех, кто не знает, что такое «слоп» (как и я), перевожу:
Slop — чушь, бессмыслица © ABBYY Lingvo
Ладно-ладно, шутки в сторону. «Слоп» это такое состояние, когда считается, что юзер случайно задел экран и на самом деле не хотел ничего двигать/скроллить/зумить/ещё чего. Эдакая «защита от случайного движения». Гифка объяснит:
… на гифке видно, что до начала пинча допускаются минимальные движения, которые не будут считаться пинчем. В принципе, хорошая вещь, плюсую.
Но… слоп то тоже не изменяем! ScaleGestureDetector описывает его так:
… а ViewConfigeuration так:
Откуда именно такое значение? Зачем? Почему? Непонятно… В общем, Android SDK – это лучший учебник по рефлексии.
2.8.2.3. Скачки detector.getScaleFactor() при первом пинче
Выглядело это, мягко говоря, не очень – постоянно дергался размер вьюшки, а уж как анимации при этих скачках выглядели – лучше и вовсе опустить.
Я долго пытался понять, в чем же проблема. Проверял на реальном устройстве (мало ли, вдруг проблема эмулятора), двигал мышкой так, будто за лишний миллиметр движения где-то умирает котик (мало ли, вдруг я дерганный и отсюда коэф. > 1 в логах) – но нет. Ответ найден не был, но мне повезло. Так сказать «от балды», решил попробовал отправить в ScaleGestureDetector эвент MotionEvent.ACTION_CANCEL сразу после инициализации:
… и это помогло О_о… Покопавшись (который уже раз, а, Android SDK?) в исходниках, обнаружилось, что у первого пинча нет слопа (да-да, это того, о котором писалось выше). Почему так – для меня осталось загадкой, ровно как и то, почему этот хак помог. Вероятнее всего где-то они намудрили с инициализацией и часть класса считает, что самый первый пинч уже прошел проверку на слоп, а другая часть считает, что нет, и в итоге в пылу жаркой битвы вида «прошел / не прошел» попеременно побеждает одна из них. ¯\_(ツ)_/¯ © SDK
Возвращаемся к проблеме с игнорированием ScrollView установленных дочерними элементами height ’ов. Ситуация следующая: как только происходит пинч, хотелось бы чтобы текущий фокус (куда ты кликнул мышкой и из какой точки вообще делаешь зум) остался тем же. Почему? Ну просто так зум выглядит более user-friendly, считай, ты не просто изменяешь height дочернего элемента, а именно зумишь к какому-то юзеру, в то время как все остальные разъезжаются от него:
Решение нашлось здесь. Происходит следующее: в момент, когда мы делаем setScroll() происходит проверка того, не выходит ли позиция скролла за размер дочернего элемента, и если выходит, то устанавливаем максимально возможную позицию. А так как setScroll() вызывается при зуме, то и получается следующая последовательность действий:
… а в onLayout() после метода super.onLayout() вызываем этот метод:
p.s. Однако если не использовать хак с «меняем height для child внутри child внутри ScrollView », то такой проблемы не будет. Но тогда возвращаемся к проблеме десятка подклассов.
3. Задача кластеризации юзеров
Теперь время поговорить о самой задаче, её алгоритме и некоторых её особенностях.
3.1. Что делать с граничными юзерами?
Я говорю о юзерах, которые находятся на границе своего ранга:
На картинке граничные юзеры — это юзеры с очками 0%, 30%, 85% (его почти полностью перекрыл юзер с 80%). Самым простым способом было бы рисовать их на своей законной позиции (равной: ), но в таком случае они начнут заезжать в чужие ранги, что выглядело бы мягко говоря «не комильфо». Основная причина отказа от этого – группировка. Представьте себе юзера с 29% очков. Он находится на границу ранга «Newbie». Но вот он вдруг объединяется с юзером с 33% очками и их совместная группа теперь расположена в позиции, соответствующей 31%, т.е., в ранге «Good». Мне не очень понравилась идея, что группировка вьюшек может менять ранги юзеров, поэтому от неё я отказался и решил ограничить юзеров внутри рангов так, как вы видели на картинке выше.
Забегая вперед отмечу, что это добавило очень много мороки алгоритму группировку и логике работы приложения в целом.
3.2. Какое кол-во очков показывать у группы?
Допустим, в группу объединились 2 юзера с очками 40% и 50%. Где расположить их совместную группу и с какими очками? Ответ простой: 45% у группы, позиция соответствует очкам группы.
Усложним задачу. А этих как объединить?
Здесь есть 2 способа решения:
… так как userViewHeight неизменен, а containerHeight меняется, то в разные моменты пинча очки у граничных вью будут разные.
Более того, сама формула:
Учитывая все эти недостатки способа №2, было принято решение использовать способ №1, хоть и групповая вьюшка будет появляться не точно по центру между юзер-вьюшками.
3.3. Алгоритм
Вот уж где можно разгуляться. Что я и сделал. У меня было по крайней мере 5 различных реализаций, которые так или иначе были подвержены «неприятным» эффектам, которые вынуждали меня вновь и вновь решать проблему по-новому.
Я мог бы написать финальную реализацию и закончить на этом, но всё же предпочел описать несколько своих реализаций. Если вам интересна описанная ниже проблема кластеризации, подумайте, как бы вы её решили (какой алгоритм бы придумали/использовали), а уже затем читайте абзац за абзацем, меняя своё решение, если оно, также как было моё, подвержено артефактам.
3.3.1. Задача
Сделать кластеризацию с хорошим UX, а это значит, что:
3.3.2. Пару слов об алгоритмах
По поводу обозначений. Я буду отмечать номер юзера одной цифрой: «7», — а группы – двумя: «78». Цифры в номере группы обозначают номера юзеров в неё входящих, то есть группа 78 состоит из юзеров 7 и 8.
К каждому алгоритму будет приведено как словесное описание, так и псевдокод.
3.3.3. Алгоритм №1
Простой последовательный алгоритм, объединяющий юзеров в порядке следования, если они пересекаются.
1. Проверить, пересекаются ли юзеры с индексом i и i+1.
2.1. Если пересекаются – объединить и сделать ячейкой №1.
2.2. Если не пересекаются, инкрементировать индекс.
3. Повторять с шага 1 пока не достигнут последний индекс.
Но у этого алгоритма есть проблема с UX:
Он объединяет всех юзеров воедино, если те достаточно близки изначально, что приводит к нерациональному использованию пространства, а значит – алгоритм не подходит по UX.
3.3.4. Алгоритм №2
Очевидно, проблема в том, что все юзеры проверяются последовательно. Поэтому я решил чуть доработать алгоритм №1. Теперь будет как минимум пара проходов по списку юзеров: четный и нечетный проходы. Как только в обоих проходах не будет найдено пересечение, алгоритм закончится.
Шаги:
0. i = 0.
1. Проверить, пересекаются юзеры с индексом i и i+1.
2.1. Если пересекаются – объединить, и записать в первую ячейку. Индекс инкрементируется на 1.
2.2. Если не пересекаются, индекс инкрементируется на 2.
3. Если не достигнут последний индекс, повторить шаг 1.
4. Если i == 1 и не нашлось пересекающихся групп, то закончить алгоритм.
5. Повторить с шага 0, инвертировав i (с 0 на 1 или с 1 на 0 – смена на нечетный или четный проходы).
Алгоритм решает предыдущую проблему, но появляется новая, связанная с неправильным разъединением после объединения (из-за иной скорости зума). Такую проблему я называю «проблема 21-12» (название поясню позже):
Объясню, что здесь произошло. При запуске алгоритма, было выявлено, что 1 и 2 юзеры пересекаются и образуют группу. С их группой пересекается 3ий юзер, а посему все они образуют ещё одну группу.
Однако если попробовать их расцепить при более быстром зуме, то произойдет то, что вы видите в нижней части картинки – два юзера будут пересекаться, но не будут образовывать группу.
Даже если после разбиения запускать ещё один проход объединения, мы получим группу 23, которой раньше не существовало. Раньше юзеры объединялись в группы с размером 2 и 1, а теперь, после разъединения, они образуют группы размером 1 и 2. Отсюда и название «проблема 21-12». Это плохо сказывается на UX, когда группу объединяются по-разному в зависимости от скорости зума, а значит алгоритм не подходит.
3.3.5. Алгоритм №3
Тут мне стало ясно, что тупо в лоб задачу не решить и придётся использовать артиллерию потяжелее. Поэтому я решил объединять юзеров на основе расстояния между ними (их вьюшками). Чем меньше расстояние между юзерами – тем первее их объединение. А для того, чтобы разбивать группы, можно просто ввести поле типа Stack groupsStack; и добавлять туда каждую новую группу. Таким образом при разъединении потребуется проверять лишь самый верхний элемент на стэке.
Шаги:
0. Создать массив дистанций между каждой последовательной парой юзеров (массив получится длиной groups.size() – 1, ведь пересечься юзер/группа может только со своими соседями слева и справа). Отсортировать массив дистанций по возрастанию
1. Проверить первую дистанцию между вьюшками. Она меньше либо равно 0?
YES.1. Объединить 2-ух юзеров в группу и добавить группу в стэк групп.
YES.2. Удалить текущую дистанцию. Обновить дистанции, которые зависели от этих 2-ух юзеров, заменив их на дистанцию до их совместной группы.
YES.3. Отсортировать массив дистанций по возрастанию.
YES.4. Перейти к шагу 1.
NO.1. Закончить алгоритм.
Я думал «ну уж этого точно будет достаточно». Но нет. Оказалось, и здесь есть своя проблема: неверный порядок объединения групп. «Как такое может быть?!» — подумали вы? Да очень просто. Что если сделать о-о-о-о-очень резкий зум?
В результате резкого зума у всех юзеров позиция стала одной и той же, ведь они и до зума были достаточно близки друг к другу. Из-за этого sortAsc() не знал, что делать, ведь расстояние между юзерами 1 и 2 равно 0, но и между юзерами 2 и 3 оно также равно 0. Как итог, группы объединились в неверном порядке. Особенно это будет заметно при разъединении. Первыми будут разъединены 12, ведь объединились они последними (стэк — это LIFO. Все помнят?), хотя они и ближе к друг другу.
3.3.6. Алгоритм №3.1
… заменяем этой с доп. сравнением group.scoreAfterMerge ‘ов:
Действительно, user.score и group.scoreAfterMerge всегда будут неравны (а если и равны, то нет разницы, в каком порядке их объединять — всё равно они никогда не расцепятся). Это означает, что доп. сравнения по score должно хватить для решения задачи… но нет, и в этом случае присутствует недочет.
Допустим, высота у вьюшек юзеров 100. Тогда если у юзера позиция 0, то его нижняя координата равна 100. Юзеров я буду отмечать вот так: [0, 100], то есть в виде [начало_вьюшки, конец_вьюшки]. Считайте, что мы работаем с 1D пространством. Для определения пересечения между двумя вьюшками (например, [0, 100] и [60, 160]) нужно вычесть из конца вьюшки №1 начало вьюшки №2, т.е. .
Итак, имеем вьюшку ранга высотой 1000 и 3-ех юзеров:
Юзер №1 с 60%: [600, 700].
Юзер №2 с 79%: [790, 890].
Юзер №3 с 100%: [900, 1000] (до коррекции: [1000, 1100]).
Примечание: у юзера №3 позиция не [1000, 1100] потому, что выход за границы ранга недопустим. Поэтому его вьюшка была перемещена на ближайшую возможную позицию внутри вьюшки ранга.
Казалось бы, расстояние между №1 и №2 равно 90, между №2 и №3 равно 10. Последние пересекутся раньше при зуме, верно. Произведем зум с коэффициентом 0.5 (вьюшка ранга стала размером 500):
Юзер №1 со 60%: [300, 400].
Юзер №2 с 79%: [395, 495].
Юзер №3 с 100%: [400, 500] (до коррекции: [500, 600]).
Юзер №1 со 60%: [100, 200] (до коррекции: [120, 220]).
Юзер №2 с 79%: [100, 200] (до коррекции: [158, 258]).
Юзер №3 с 100%: [100, 200] (до коррекции: [200, 300]).
Расстояние между №1 и №2 равно 0, расстояние между №2 и №3 тоже равно 0. Как гласит алгоритм, сравним тогда вьюшки по user.score и… Совершенно внезапно, №1 и №2 объединились в группу раньше, чем №2 и №3, что в итоге приведет к рывкам при разъединении.
Ситуация может показаться довольно синтетической, однако она происходит не только на границе ранга, но и в центре, если сделать достаточно резкий зум. Просто для примера было проще показать проблему на границе.
3.3.7. Алгоритм №4
Давайте создадим время. Континуум, если быть точнее. Это довольно известная задача в сфере компьютерной физики. Если квантовать время слишком большими отрезками, то объект «A» пересечет объект «B» и это не будет детектировано из-за слишком большой скорости движения. Вот пример:
Даже если мы квантуем время на наносекунды (), то в следующий момент времени позиция «B» будет считаться так:
То есть мы получим:
«B» просто прошел «A» насквозь, и мы этого никак не заметили, ведь их координаты никогда не пересекались и даже не находились вблизи.
Примечание: getNormalizedPos() возвращает нормализованную (т.е., от 0 до 1) позицию внутри ранга.
Этот алгоритм прошел все проверки, что я смог придумать, а также показал довольно резвую скорость работы. В общем, задача решена.
4. Нерешенные проблемы (UPD: уже решенные)
А теперь о плохом. Изначально я планировал написать статью уже после того, как решу все проблемы, но из-за нехватки времени и мысли «а вдруг в комментах помогут – время сэкономлю», я решил выложить так, как есть. Как освободится время, я вернусь к этим проблемам, и, если не забуду, добавлю решение сюда.
4.1. ScrollView.setScroll() внутри ScrollView.onLayout() срабатывает корректно только после вызова super.onLayout() (если изменять размер child’а внутри child’а этого ScrollView )
4.2. View.setVisibility(GONE) вызывает requestLayout()
Текущая реализация алгоритма (которая с континуумом) работает очень быстро, но не реализация отрисовки вьюшек. В идеале, необходимо добавить recycling вьюшек юзеров/групп, да и вообще не пытаться отрисовывать вьюшки, что находятся за пределами экрана.
4.999. Решение описанных проблем
5. Заключение
Задача оказалась куда сложнее и интереснее, чем я себе представлял. Хотел лишь поучиться делать анимацию пересечения, а в итоге поработал с алгоритмами, порылся в исходниках SDK и много чего ещё. С Android’ом скучать не приходится.
Если у вас есть предложения по решению «нерешенных проблем» — пишите в комментариях. Я проверю решение и, если всё хорошо, то обновлю текст.
UPD 1: как предложил мистер Artem_007, добавлены feature-request/bug-report ссылки на упомянутые проблемы.
UPD 2: добавлен пунтк 4.999. с решением «нерешенных» проблем.