Превключвателят на CannyViewAnimator показва красиво

За какво става въпрос

Но първо, за по-голяма яснота, нека си представим ситуация, която е често срещана при разработването на Android. Имате екран и на него има списък, който идва от сървъра. Докато страхотни данни се зареждат от страхотен сървър, вие показвате товарача; веднага след като данните пристигнат, вие ги поглеждате: ако е празно, показвате мъниче, ако не, показвате действителните данни. Как да разрешите тази ситуация в потребителския интерфейс? Преди това ние от Live Typing използвахме следното решение, което веднъж шпионирахме в U2020 и след това прехвърлихме към нашия U2020 MVP - това е BetterViewAnimator, View, което е наследено от ViewAnimator. Единствената, но важна разлика между BetterViewAnimator и неговия предшественик е възможността за работа с идентификатори на ресурси. Но той не е идеален.

ViewAnimator е изглед, който наследява от FrameLayout и има само едно от дъщерните елементи, видими във всеки даден момент. Има набор от методи за превключване на видимото дете.

Важен недостатък на BetterViewAnimator е възможността да работи само с остарялата AnimationFramework. И в тази ситуация CannyViewAnimator идва на помощ. Поддържа Animator и AppCompat Transition. Връзка към проекта в Github

красиво

Как започна всичко

По време на разработването на следващия екран „зареждане на списък-зареждане“ се замислих за факта, че ние, разбира се, използваме BetterViewAnimator, но по някаква причина не използваме почти основната му функция - анимации. Оптимистично, реших да добавя анимация и се натъкнах на нещо, което забравих: ViewAnimator може да работи само с анимация. Търсенето на алтернатива в Github, за съжаление, не беше успешно - нямаше достойни, но имаше само Android View Controller, но той абсолютно не е гъвкав и поддържа самоосем предварително зададени анимации в него. Това означаваше само едно нещо: трябва да напишете всичко сами.

Какво искам да получа

Първото нещо, което реших да направя, е да помисля какво искам да получа в крайна сметка:

  • способността все още да контролира видимостта на детето;
  • възможност за използване на Animator и по-специално CircularRevealAnimator;
  • възможност за стартиране на анимации както последователно, така и паралелно (ViewAnimator може да се изпълнява само последователно);
  • възможност за използване на Transition;
  • направете набор от стандартни анимации с възможност за настройка чрез xml;
  • гъвкавост на работа, възможност да зададете своя собствена анимация за отделно дете.

След като взех решение за желанията, започнах да обмислям "архитектурата" на бъдещия проект. Имаше три части:

  • ViewAnimator- отговаря на превключване на видимостта на детето;
  • TransitionViewAnimator- наследен от ViewAnimator и отговаря за работата с Transition;
  • CannyViewAnimator- наследява от TransitionViewAnimator и отговаря за работата с Animator.

Реших да настроя аниматори и преходи с помощта на интерфейс с два параметъра: дете, което ще се появи, и дете, което ще изчезне. Всеки път, когато видимото дете се промени, необходимата анимация ще бъде взета от изпълнението на интерфейса. Ще има три интерфейса:

  • InAnimator- отговаря за Аниматора на появяващото се дете;
  • OutAnimator- отговаря за Аниматора на изчезващото дете;
  • CannyTransition- отговорен за прехода.

Реших да направя един интерфейс за Transition, тъй като Transition се наслагва незабавно върху всички появяващи се и изчезващи деца. Концепцията бешеобмислих и започнах да се развивам.

viewanimator

С моя базов клас не станах твърде умен и реших да направя план с ViewAnimator от SDK. Просто изхвърлих работата с анимация от него и оптимизирах методите в него, тъй като много от тях ми се сториха излишни. Освен това не забравих да добавя методи от BetterViewAnimator. Окончателният списък с методи, важни за работа с него, се оказа следният:

  • void setDisplayedChildIndex(int ​​​​inChildIndex)- показва дете с дадения индекс;
  • void setDisplayedChildId(@IdRes int id)- показва дете с даден идентификатор;
  • void setDisplayedChild(View view)- показва конкретно дете;
  • int getDisplayedChildIndex()- получаване на индекса на показаното дете;
  • View getDisplayedChild()- получаване на показаното дете;
  • int getDisplayedChildId()- получаване на идентификатора на показаното дете.

След кратък размисъл реших допълнително да запазя позицията на текущото видимо дете в onSaveInstanceState() и да го възстановя onRestoreInstanceState(Parcelable state), показвайки го веднага. Полученият код изглежда така:

TransitionViewAnimator

След като приключих с ViewAnimator, започнах доста проста, но не по-малко интересна задача за това: да направя поддръжка за Transition. Същността на работата е следната: когато се извика замененият метод changeVisibility (View inChild, View outChild), анимацията се подготвя. Преходът се взема от дадения CannyTransition с помощта на интерфейса и се записва в полето клас.

След това, в отделен метод, този преход се стартира. Реших да направя стартирането отделен метод с резерв за бъдещето - факт е, че Transition се стартира с помощта на методаTransitionManager.beginDelayedTransitionи това налага някои ограничения. В края на краищата, преходът ще бъде изпълнен само за онези изгледи, които са променили свойствата си за определен период от време след извикване наTransitionManager.beginDelayedTransition. Тъй като се планира въвеждането на аниматори в бъдеще, което може да отнеме относително дълго време,TransitionManager.beginDelayedTransitionтрябва да се извика непосредствено преди промяна на видимостта. Е, тогава извиквамsuper.changeVisibility(inChild, outChild);, което променя видимостта на желаното дете.

CannyViewAnimator

Така стигнах до основния слой. Първоначално исках да използвам LayoutTransition за управление на аниматорите, но мечтите ми бяха разбити от невъзможността да правя анимации успоредно с него без патерици. Освен това други недостатъци на LayoutTransition създадоха допълнителни проблеми, като необходимостта от задаване на продължителността на AnimatorSet, невъзможността за ръчно прекъсване и т.н. Беше решено да напишем собствена логика на работа. Всичко изглеждаше много просто: стартирайте Animator за изчезващото дете, задайте го наVisibility.GONEв края му и веднага направете появяващото се дете видимо и стартирайте Animator за него.

Тук се натъкнах на първия проблем:не можете да стартирате Animator на неприкачен изглед(това е този, който все още не е завършилonAttachили вече е задействалonDetach). Това ми попречи да променя видимостта на всяко дете в конструктора или всеки друг метод, който се задейства преди onAttach. Предвиждайки куп различни ситуации, в които това може да е необходимо, и еднакво малка група проблеми в Github, реших да опитам да поправя ситуацията. За съжаление, най-простото решение е да се обадитеметодътisAttachedToWindow()се основаваше на невъзможността да се извика преди 19-та версия на API и аз наистина исках да имам поддръжка от 14-та API.

View обаче има OnAttachStateChangeListener и аз не пропуснах да го използвам. Замених методаvoid addView(View child, int index, ViewGroup.LayoutParams params)и окачих този Listener на всеки добавен View. След това поставям в HashMap препратка към самия View и булева променлива, обозначаваща неговото състояние. АкоonViewAttachedToWindow(View v)работи, задавам го на true, а акоonViewDetachedFromWindow(View v), тогава на false. Сега, точно преди да стартирам аниматора, мога да проверя състоянието на изгледа и да реша дали изобщо да стартирам аниматора.

След преодоляването на първата "барикада" направих два интерфейса за получаване на аниматори: InAnimator и OutAnimator.

Всичко вървеше гладко, докато не се сблъсках с нов проблем:след Animator'a трябва да възстановя състоянието на View.

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

Помогна и дори дадох същия отговор на StackOverflow.

Веднага след решаването на този проблем възникна друг:CircularRevealAnimator не изпълнява своята анимация, ако onMeasure все още не е изпълнено на изгледа.

Това беше лоша новина, тъй като невидимите деца на ViewAnimator имат Visibility.GONE. Това означава, че те не се измерват, докато не бъдат зададени на друг вид видимост - ВИДИМА или НЕВИДИМА. Дори ако промених Visibility на INVISIBLE преди стартиране на анимацията, това няма да реши проблема. Тъй като измерването на размерите на изгледа става, когато рамката е изчертана, икадрите се изобразяват асинхронно, тогава няма гаранция, че до момента, в който аниматорът стартира, изгледът ще бъде измерен. Наистина не исках да задавам забавяне или да използвам onPreDrawListener, така че реших да използвам Visibility.INVISIBLE вместо Visibility.GONE по подразбиране.

Тъй като нямах време да се съвзема от предишната "битка", се втурнах към следващата.Тъй като децата са подредени едно върху друго във FrameLayout, когато InAnimator и OutAnimator се изпълняват едновременно, възниква ситуация, когато в зависимост от индекса на детето анимацията изглежда различно.Поради всички проблеми, които възникнаха с внедряването на аниматорите, исках да ги напусна, но усещането за „започни веднъж – завърши“ ме накара да продължа напред. Проблемът възниква, когато се опитам да направя видим изглед, който се намира под текущо показания изглед. Поради това анимацията на изчезването напълно се припокрива с анимацията на външния вид и обратно. В търсене на решение се опитах да използвам друга ViewGroup, играх със свойството Z и опитах куп други неща.

И накрая, идеята се появи в началото на анимацията просто да премахнете желания изглед от контейнера, да го добавите в горната част и в края на анимацията да го премахнете отново и след това да го върнете на първоначалното му място. Идеята проработи, но на слаби устройства анимациите бавят. Зависването се дължи на факта, че когато View се премахва или добавя,requestLayout()се извиква към него и неговия родител, който ги преизчислява и преначертава. Трябваше да се изкача в джунглата на класа ViewGroup. След няколко минути изучаване стигнах до извода, че редът на изгледа вътре в ViewGroup зависи само от един масив и след това наследниците на ViewGroup (например FrameLayout или LinearLayout ) вече решават как да го покажат. Уви, масивът, както и методите за работа с него бяха отбелязани като частни. Но беше и добре.новини: в Java това не е проблем, тъй като има Java Reflection. С помощта на Java Reflection се възползвах от методите за работа с масив и сега можех директно да контролирам позицията на необходимия ми View. Ето как се оказа методът:

Този метод показва позицията, от която се нуждая за изгледа. Няма нужда да извиквате преначертаване в края на тези манипулации - анимацията ще го направи вместо вас. Сега, преди началото на анимацията, мога да сложа изгледа, от който се нуждаех, отгоре и да го върна обратно в края на анимацията. И така, основната част от историята на CannyViewAnimator приключи.

Добавяне на XML поддръжка и помощни класове

Ново предизвикателство: добавете персонализиране чрез XML. Тъй като наистина не обичам да създавам аниматори в XML (изглеждат ми нещо лошо четливо и неочевидно), реших да направя набор от стандартни анимации с възможността да ги задавам чрез флагове. Плюс това, този подход ще улесни задаването на анимации чрез Java код. Тъй като подходът за създаване на CircularRevalAnimator се различава от стандартния, трябваше да напиша два типа помощни класове: един за обикновен Property, а другият за CircularReval. Резултатът е шест класа: