Как да създадете персонализирани изгледи?

Преди да се потопите в процеса на създаване на персонализиран изглед, струва си да посочите защо може да се наложи да създадем персонализирани изгледи.

  • Уникалност: Създайте нещо, което не може да се направи с обикновени изгледи.
  • Оптимизация: Много пъти сме склонни да добавяме множество изгледи или ограничения, за да създадем желания изглед, който може да бъде оптимизиран драстично по отношение на времето за рисуване, измерване или оформление.

Най-добрият начин да започнете би било да разберете как android управлява групите с изгледи и разпределя гледни точки на екрана. Нека да разгледаме диаграмата по-долу.

Това е основният жизнен цикъл на андроид изгледа

onMeasure (widthMeasureSpec: Int, височинаMeasureSpec: Int)
 Всеки родителски изглед преминава ограничение за височина и ширина спрямо изгледа на детето, въз основа на който изгледът на детето решава колко голям иска да бъде. След това детето извиква setMeasuredDimension (), за да запази измерената му ширина и височина.

Как се преминават тези ограничения?

Android използва 32-битов int, наречен спецификация на измерването, за да опакова измерение и неговия режим. Режимът е ограничение и може да бъде от 3 вида:

  • MeasureSpec.ЕКСПАКТНО: Изгледът трябва да е абсолютно със същия размер като размерите, предавани заедно със спецификацията. Напр. layout_width = „100dp“, layout_width = „match_parent“, layout_weight = „1“.
  • MeasureSpec.AT_MOST: Изгледът може да предава максимална височина / ширина на измерението. Той обаче може да бъде и по-малък, ако желае да бъде. Например android: layout_width = ”wrap_content”
  • MeasureSpec.UNSPECIFIED: Изгледът може да бъде от всякакъв размер. Това се предава, когато използваме ScrollView или ListView като наш родител.

onLayout (променено: Boolean, отляво: Int, отгоре: Int, дясно: Int, отдолу: Int)
Android прилага всякакви компенсации или маржове и призовава този метод, за да информира вашето виждане за това къде точно ще бъде поставен на екрана. За разлика от onMeasure, той се извиква само веднъж по време на преминаването. Така че се препоръчва да се извършват всякакви сложни изчисления в този метод.

onDraw (платно: Платно)
И накрая, Android ви предоставя 2D повърхност за рисуване, т.е. платното, върху което можете да рисувате с помощта на обект на боя.

След това UI нишката преминава към списъци на дисплея, за да визуализира нишката, което прави много оптимизации и накрая GPU обработва данните, предадени на него от рендер нишка.

Как да определим атрибутите за вашия изглед?

Декларирането на XML атрибути е просто. Просто трябва да добавите декларируем стил във вашия attrs.xml и да декларирате формат за всеки атрибут.
Например, ако създавате обикновен изглед, който показва кръг с неговия етикет. Вашите атрибути може да изглеждат така.

<декларирай-styleable name = "CustomView">
    
    
    
    
    
    
        
        
        
        
    

Същото се споменава при създаване на изглед по следния начин.

приложение: circleRadius = "5dp"
приложение: circleDrawable = "@ изтегляемото / ic_bell"
приложение: circleColorType = "fillStroke"
...

Сега трябва да анализираме тези атрибути във вашия клас java или kotlin.

  • Създайте своя клас на изглед, който разширява класа на android.view
  • Получете позоваване на атрибутите, декларирани в XML. Докато attrs се предава в конструктора, вторият параметър е препратка към стилизирания, който току-що декларирахме. Последните две се използват за получаване на атрибути на стил по подразбиране в тема или за предоставяне на атрибути по подразбиране стил.
val a = context.theme.obtainStyledAttributes (attrs, R.styleable.YourCustomViewClass, 0, 0)
  • Разбор на аргументите на атрибута
radi = a.getDimension (R.styleable.CustomView_circleRadius, резервен)
showLabel = a.getDimension (R.styleable.CustomView_showName, резервен)
colorType = a.getInteger (R.styleable.CustomView_colorType, colorType)

Android автоматично обработва процеса на преобразуване на dp или sp в правилното количество пиксели според размера на екрана при анализиране на атрибут на измерение. Но, трябва да гарантирате, че резервната стойност се преобразува в подходяща пикселна стойност, тъй като android връща резервна стойност без никакви преобразувания, ако стойността на атрибут не е дефинирана в XML.

Докато анализа на всички останали атрибути е доста прям. Ще ви информирам как да разберете знамена. Декларирането на атрибути на флагове може да бъде наистина полезно понякога, тъй като можем да проверим за няколко свойства, използвайки един атрибут. По същия начин android обработва знамето за видимост.

colorType тук е цяло число, което представлява flagSet. Сега, тъй като всеки бит в цяло число може да се използва за представяне на индикация. Можем да проверим дали даден флаг съществува и да извършим нашите операции съответно. За да проверим дали ходът на тип флаг излиза, можем просто да извършим операция на flagSet със стойността на удара. Ако резултатът е същият, това означава, че флагът действително съществува в flagSet.

  • И накрая, рециклирайте въведения масив, който да бъде използван от по-късния повикващ.
a.recycle ()

Инициализиране на вашите обекти
Винаги е по-добре да инициализирате вашите бои и обекти на пътя или други променливи в самия конструктор. Тъй като декларирането му, всеки друг метод за преминаване може да доведе до безсмислено създаване на обекти отново и отново.

Изчисляване на размера на изгледа ви

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

  • Изчислете колко ширина и височина изисква изгледът ви. Например, ако рисувате обикновен кръг с неговия етикет под кръга. Предложената ширина ще бъде:
     (диаметър на кръга + всяка допълнителна ширина, ако е заета от етикета).
  • Изчислете желаната ширина, като добавите предложената ширина с paddingStart и paddingEnd. По същия начин, желаната височина ще бъде предложена височина плюс paddingTop & paddingBottom.
  • Изчислете действителния размер, като спазвате ограниченията. За да изчислите това, просто трябва да предадете спецификацията за измерване, която ви е предадена в onMeasure (), и желаното от вас измерение в този метод, наречен resolutionSize (). Този метод ще ви каже най-близкото възможно измерение до желаната ширина или височина, като същевременно спазва ограниченията на родителя му.
  • Най-важното е, че трябва да зададете крайната ширина и височина в метода onMeasure, като извикате setMeasuredDimension (измерена ширина, измерена височина), за да съхранявате измерената ширина и височина на този изглед в противен случай, може да видите, че изгледът ви се срива с IllegalStateException.

Позициониране на вашите мнения

Можем да позиционираме нашите детски изгледи, като използваме onLayoutMethod. Кодът просто може да включва итерация върху всякакви изгледи на деца и приписването им вляво, отгоре, вдясно и отдолу в зависимост от измерените ширини и височини.

Рисуване на вашия изглед

Преди да използваме платното има няколко неща, които трябва да разберем:

  • Paint: Класът Paint съдържа информация за стила и цвета за това как да рисувате геометрии, текст и растерни карти. Ето как създаваме обект на боя.
mPaint = нова боя ();
mPaint.setColor (mDrawColor);
mPaint.setStyle (Paint.Style.STROKE); // по подразбиране: FILL
// Изглажда краищата на това, което е нарисувано, без да засяга вътрешната форма.
mPaint.setAntiAlias ​​(истина);
// Изсъхването влияе върху това, как цветовете с по-голяма точност от устройството се вземат върху пробата.
mPaint.setDither (истина);
mPaint.setStrokeWidth (mStrokeWidth);

Можете да прочетете повече за имотите тук.

  • Рисуване на фигури: Можете директно да рисувате фигури като линия, дъга, кръг и т.н. върху платното. Нека да разгледаме диаграмата по-долу, за да постигнем по-добро разбиране.
Описвайки как на платното са нарисувани овал, дъга и правоъгълник
замени забавлението onDraw (платно: Платно) {

    super.onDraw (платно)
    val cx = canvas.width / 2
    val cy = canvas.height / 2

    ovalRect.left = cx - circleRadius
    ovalRect.top = cy - кръгRadius
    ovalRect.right = cx + circleRadius
    ovalRect.bottom = cy + кръгRadius

    canvas.drawArc (ovalRect, 0F, 90F, true, mCircleFillPaint)
    canvas.drawOval (ovalRect, mCircleStrokePaint)
    canvas.drawRect (ovalRect, mRectStrokePaint)

}

Използване на Paths: Рисуването на сложни форми с горните методи може да стане малко сложно, така че android предлага клас Path. С класа Path можете да си представите, че държите химикалка и можете да нарисувате форма, след което може би се преместете в друга позиция и нарисувайте друга форма. И накрая, когато приключите със създаването на пътека. Можете просто да нарисувате пътеката върху платното така. Също така, когато използвате пътища, можете също да използвате различни ефекти на пътя (обсъдени по-подробно по-долу). По-долу е даден пример за формата, създадена с помощта на пътеки.

val cx = canvas.width / 2
val cy = canvas.height / 2
val ъгъл = 2,0 * Math.PI / 5
// преминаване към позиция 1
path.moveTo (
        cx + (радиуси * Math.cos (0.0)). toFloat (),
        cy + (радиуси * Math.sin (0.0)). toFloat ())
// начертайте всички линии до поз. 5
за (I в 1 до 5) {
    path.lineTo (
            cx + (радиуси * Math.cos (ъгъл * i)). toFloat (),
            cy + (радиуси * Math.sin (ъгъл * i)). toFloat ())
}
// присъедини се поз 5 с поз 1
path.close ()
// ако искате да добавите кръг около полигона, използвайки път
// path.addCircle (cx, cy, circleRadius, Path.Direction.CW)
// нарисувайте многоъгълник
canvas.drawPath (път, mShapePaint)
  • Path Effects: Ако също приложите ефект на Corner path към вашия боен обект с определен радиус, многоъгълникът ще изглежда така. Можете също така да използвате други ефекти на пътя като DashPathEffect, DiscretePath и т.н. За да комбинирате два различни ефекта на пътя, можете да използвате ComposePathEffect.
mShapePaint.pathEffect = CornerPathEffect (20f)
Полигон с ефект на ъглов път
  • Рисуване на битови карти: За да рисувате растерни картинки на платното, можете да използвате
canvas.drawBitmap (bitamp, src, dest, paint)

bitmap: Bitmap, която искате да рисувате върху платно
src: Необходим е rect обект, който определя частта от растерната карта, която искате да нарисувате. Това може да е нищожно, ако искате да нарисувате пълната растерна карта.
dest: Рект на обект, който казва колко площ искате да покриете върху платното с растерната карта
paint: Обектът на боя, с който искате да нарисувате растерната карта

Android автоматично прави всички необходими мащабиране или превод, за да пасне на източника в областта на местоназначение.

Можете също така да рисувате рисунки върху платно.

dravable.setBounds (вляво, отгоре, вдясно, отдолу)
drawable.draw (платно)

Преди да начертаете чертеж, трябва да зададете граници на чертожа. Вляво, отгоре, отдясно и отдолу се описва размерът на чертежа и позицията му върху платното. Можете да намерите предпочитания размер за Drawables, като използвате методите getIntrinsicHeight () и getIntrinsicWidth () и решите границите съответно.

Рисуване на текстове: Рисуването на текстове може да бъде мъка. Не самия чертеж, а подравняването или измерването на текст. Това се случва, защото различните символи имат различна височина и за да стане по-лошо може да има и различни шрифтове. Така че за да измерите височината на текста, трябва да изчислите конкретни рамки за текста по този начин.

textPaint.getTextBounds (yourText, startIndex, endIndex, rect)

След това, прехвърленият в края обект на ректа ще съдържа текстовите граници на действителния текст, който трябва да бъде нарисуван. По този начин можете да изчислите действителната височина на текста, който ще бъде начертан, и да зададете правилна базова линия y за вашия текст. За да изчислите ширината на вашия текст, трябва да използвате textPaint.measureText (), тъй като е по-точен от ширината, дадена от текстови граници на боя (поради начина, по който тези методи се прилагат в библиотеката на Skia). Освен това, за да гарантирате, че текстът е центриран хоризонтално върху платното, можете просто да зададете подравняването на боята си към TextAlign.CENTER и да прекарате средната точка на вашето платно в x координата.

canvas.drawText (текст, xPos, yPos, боя)

Изчертаване на многоредов текст: Ако искате да се справите с прекъсвания на линиите (\ n) или Ако имате фиксирана ширина, за да нарисувате текст, можете да използвате статично оформление или динамично оформление. Това автоматично ще обработва всички прекъсвания на думите или прекъсвания на реда и също така ще ви каже колко височина ще е необходима, за да нарисувате текст с дадена ширина.

// изграждане на статично оформление
var builder = StaticLayout.Builder.obtain (текст, 0, text.length (), textPaint, ширина);
StaticLayout myStaticLayout = builder.build ();
var heightRequired = myStaticLayout.height
// чертежната част
canvas.save ()
canvas.translate (xPos, yPos)
myStaticLayout.draw (платно)
canvas.restore ()
  • Запазване и възстановяване на платно: Както може би сте забелязали, трябва да запишем платното и да го преведем, преди да рисуваме върху него и накрая трябва да възстановим платното. Много пъти трябва да нарисуваме нещо с различна настройка, като например завъртане на платното, превеждане или изрязване на определена част от платното, докато рисуваме форма. В този случай можем да се обадим на canvas.save (), което би запазило текущите ни настройки на платно на стек. След това променяме настройките на платното (превод и т.н.) и след това рисуваме всичко, което искаме с тези настройки. И накрая, когато приключим с рисуването, можем да се обадим на canvas.restore (), което ще възстанови платно към предишната конфигурация, която бяхме запазили.
  • Работа с потребителски входове: Накрая сте създали свой собствен потребителски изглед с помощта на XML атрибути, НО какво, ако искате да промените някое свойство по време на изпълнение, като радиуса на кръга, цвета на текста и др. промени. Сега, ако някаква промяна в свойството влияе на размера на изгледа ви, ще зададете променливата и ще извикате requestLayout (), която ще преизчисли размера на изгледа ви и ще го пречертае. Ако обаче свойство като цвят на текст е променено, ще трябва само да го пречертаете с нов цвят на текстовата боя и в този случай би било разумно просто да извикате невалиден ().

Допълнителна забележка: Сега, ако изгледът ви има много атрибути, може да има много пъти, когато трябва да напишете invalidate () / requestLayout след всеки сетер. Този проблем може да бъде решен с помощта на делегати на kotlin. Нека да разгледаме примера по-долу, за да бъдем по-ясни.

var textColor: Int от OnValidateProp (Color.BLACK)
var circleColor: Int от OnValidateProp (Color.CYAN)
клас OnValidateProp  (частно поле на var: T, частен вграден var func: () -> Unit = {}) {
    operator fun setValue (thisRef: Any ?, p: KProperty <*>, v: T) {
        поле = v
        обезсили ()

    }

    оператор забавно getValue (thisRef: Някой ?, p: KProperty <*>): T {
        поле за връщане
    }

}

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

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

В заключение ще спомена някои от най-добрите практики, които винаги трябва да имате предвид, за да оптимизирате ефективността на вашия персонализиран изглед.

  • Невалидна интелигентно: Не се обаждайте на невалиден, освен ако и докато нещо видимо за потребителя не се промени. Второ, когато извиквате невалиден, ако е възможно, прекарайте обект rect в метод на невалиден, за да кажете на GPU каква част от екрана трябва да бъде изтеглена.
  • Рисувайте внимателно: Не рисувайте неща, които не се виждат от потребителя. В крайна сметка неговата 2D повърхност и би било безполезно да нарисувате нещо, което по-късно се припокрива с нещо друго. Можете да постигнете това, като използвате Canvas.clipRect (). Освен това не рисувайте нещо, което е извън границите на екрана. Можете да постигнете това, като използвате canvas.quickReject ().
  • Никога не разпределяйте обекти в onDraw: onDraw () се извиква 60 пъти в секунда. макар че събирачите на боклук са наистина бързи, така че може да няма спад, свързан с GC, но те работят на отделна нишка, което би означавало, че може да ядете много батерия. Освен това, тъй като през повечето време типът обекти, инициализирани в onDraw, рисуват обекти (опаковки около C ++). Това води до извикване на много деструктори и по този начин пускане на финализатори за възстановяване на паметта на обекта, която никога не е добра за ефективност.