Lekce 10 - Vlastní Android komponenta - Měření a kreslení
V minulé lekci, Vlastní Android komponenta - Kreslený graf, jsme si připravili základ třídy grafu a metody pro obsluhu jeho atributů.
V dnešním Android tutoriálu se budeme věnovat měření a konečně i kreslení
Měření
Začneme přepsáním metody onMeasure()
.
onMeasure()
Tuto metodu přepíšeme, abychom získali rozměry prostoru, ve kterém se bude graf nacházet. Díky tomu pak můžeme i určit velikost grafu. Kód metody bude následující:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); width = getMeasuredWidth(); height = getMeasuredWidth(); initParams(width, height); setMeasuredDimension(width, height); }
Metody getMeasuredWidth()
a getMeasuredHeight()
jsou metody třídy View
, které vrací změřenou šířku a
výšku prostoru určeného pro komponentu. Možnost získat tyto "naměřené"
hodnoty máme díky volání
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
v prvním
řádku metody onMeasure()
.
V předchozí ukázce je dvakrát použita metoda
getMeasuredWidth()
pro získání šířky i výšky prostoru pro
graf. To protože komponenta s grafem má čtvercový tvar a výška komponenty
bude rovna její šířce. Pokud by se jednalo o obdélníkový tvar, byla by
pro výšku použita metoda getMeasuredHeight()
.
Získanou šířku a výšku prostoru pak předáváme metodě
initParams()
(tu si uvedeme níže) a tam vypočítáme rozměry
všech částí grafu. Rozměry, se kterými se pracuje v
onMeasure()
, jsou v pixelech.
Přepisujeme-li onMeasure()
, je
nutné na jejím konci volat
setMeasuredDimension()
. Do parametrů vložíme zjištěné
rozměry. Pokud tak neučiníme, aplikace bude ukončena s výjimkou
IllegalStateException
.
V metodě onMeasure()
jde o sladění prostorového požadavku
grafu s tím, jaké prostorové možnosti mu rodičovský element poskytuje.
Výpočet rozměrů částí grafu
Pojďme se podívat na deklaraci konstant, které slouží k těmto
výpočtům a na deklaraci metody initParams()
, kterou voláme v
metodě onMeasure()
po zjištění rozměrů prostoru:
// Šířka výseče grafu (% z poloměru grafu) final int ARC_WIDTH_PERCENTAGES = 40; // Přesah pozadí grafu přes výseč // 1 = žádný přesah final int ARC_BACKGROUND_WIDTH_PERCENTAGES = 3; // Přepočet procent na úhly (1% => 3,6°) final float PERCENTAGES_TO_ANGLE_CONSTANT = 3.6f; // Poměr výšky textu k poloměru kruhové výseče grafu final float MAX_TEXT_SIZE_RATIO = 0.60f; // Vzdálenost grafu od okolních hran final float PADDING_RATIO = 0.05f; int width; int height; // Maximální poloměr grafu float maxRadius; // Souřadnice středu int centerOfGraphX; int centerOfGraphY; // Šířka výseče int arcWidth; // Přesah pozadí přes výseč grafu int backgroundOverlap = 5; private void initParams(int width, int height) { graphPadding = (int) (height * PADDING_RATIO); maxRadius = (width / 2) - graphPadding; centerOfGraphX = width/2; centerOfGraphY = height/2; arcWidth = (int) (maxRadius / 100 * ARC_WIDTH_PERCENTAGES); backgroundOverlap = (int) (maxRadius / 100 * ARC_BACKGROUND_WIDTH_PERCENTAGES); textSize = (int) (maxRadius * MAX_TEXT_SIZE_RATIO); }
Metoda initParams()
na základě zadané výšky a šířky
nastaví zmíněné proměnné.
Jdeme kreslit
Graf není vykreslován "jedním tahem" a skládá se z více částí:
- Inicializace barev a stylů
- Textu grafu
- Pozadí grafu
- Výseč grafu
- Čáry čtvrtí
Inicializace barev a stylů
Barvy jsme získali v minulé části z XML metodou
applyAttributeSet()
a uložili jsme je do proměnných, též
deklarovaných v předcházející části kurzu. Nyní tedy můžeme založit
jednotlivé části grafu a nastavit jim tyto barvy:
Paint paint = new Paint(); // Výseč Paint paintBackground = new Paint(); // Pozadí Paint paintText = new Paint(); // Text Paint paintLines = new Paint(); // Pomocné čtvrtinové čáry private void setPaints() { paint.setColor(colorResGraph); paint.setAntiAlias(true); paintBackground.setColor(colorResGraphBackground); paintBackground.setAntiAlias(true); paintText.setColor(colorResText); paintText.setAntiAlias(true); paintLines.setColor(colorQuarterLines); paintLines.setAntiAlias(true); }
Text grafu
V následující ukázce je zajímavý způsob, jakým text vycentrujeme
vertikálně. Pro horizontální vycentrování máme parametr
Paint.Align.CENTER
. Pro vertikální zarovnání podpora není,
proto si budeme muset poradit sami.
private void drawGraphText(Canvas canvas) { if (disableText) { // Zobrazení textu není povoleno return; } // Proměnná pro zobrazovaný text String text = ""; if (valueFormat == ValueFormat.PERCENTAGES) { // Zobrazení v % text = "" + valueInPercentages + "%"; } else { // Zobrazování skutečné hodnoty text = "" + valueToDraw; } // Inicializace stylu textu paintText.setTextSize(textSize); paintText.setAntiAlias(true); paintText.setTextAlign(Paint.Align.CENTER); // Horizontální zarovnání textu na střed // Měření šířky textu float textWidth = paintText.measureText(text); // Výpočet šířky prostoru pro text (průměr otvoru ve středu grafu) int maxTextWidth = (int) ((maxRadius - arcWidth - backgroundOverlap) * 2); // Výpočet vertikální pozice textu - vysvětlení v textu pod touto ukázkou int yPos = (int) ((centerOfGraphY) - ((paintText.descent() + paintText.ascent()) / 2)) ; // Test, zda se text vejde do grafu, případně i úprava velikosti textu if (textWidth > maxTextWidth) { int newTextSize = textSize; while (textWidth > maxTextWidth) { // Zmenšení textu o 10% newTextSize = scaleDownTextSize(newTextSize); paintText.setTextSize(newTextSize); // Přeměření šířky textu textWidth = paintText.measureText(text); // Přepočítání vertikální pozice textu yPos = (int) ((height / 2) - ((paintText.descent() + paintText.ascent()) / 2)) ; } } else { paintText.setTextSize(textSize); } canvas.drawText(text, centerOfGraphX, yPos, paintText); } private int scaleDownTextSize(int actualSize) { if (actualSize <= 0) return 0; return (int) (actualSize * 0.90); }
Pojďme si vysvětlit výpočet vertikální pozice textu. Protože graf bude umět plynule reagovat na změnu zobrazené hodnoty, je nutné, aby uměl v průběhu této změny přizpůsobovat velikost textu ve středu grafu. Pokud grafu, jako maximální hodnotu, nastavíme moc vysoké číslo, hodnoty u horní hranice rozsahu nebudou vidět celé - budou částečně překryté pozadím a výsečí grafu. Již víme, že náš graf bude umět v textu zobrazovat aktuální hodnotu ve dvou režimech:
- Přepočet na procenta na základě zadané maximální hodnoty grafu
- Přímé zobrazení hodnoty
V případě procent problém s velikostí nenastane, ale v tom druhém
případě velice snadno. Proto jsme si napsali v metodě
drawGraphText()
kus kódu, který nám bude velikost textu
přizpůsobovat v závislosti na jeho délce. V popsaném kódu si ještě
blíže vysvětlíme tento řádek:
int yPos = (int) ((centerOfGraphY) - ((paintText.descent() + paintText.ascent()) / 2)) ;
Z kódu nemusí každému být jasné, co se zde odehrává. Měl by vám pomoci následující obrázek, kde jsou schématicky naznačeny některé parametry textu:
Vše kolem vertikální polohy textu začíná testem, zda šířka textu,
kterou zjišťujeme v řádku paintText.measureText(text)
, není
větší než vypočítaná maximální možná šířka textu. Maximální
šířka textu se řídí velikostí otvoru ve středu grafu. Pokud je šířka
textu moc velká, vstupuje do hry cyklus, ve kterém postupně po 10ti
procentech velikost textu zmenšujeme a vždy ihned šířku textu znovu
měříme. Ve chvíli, kdy se text svou šířkou vejde do středu grafu, cyklus
končí. Jako poslední krok metody drawGraphText()
je volání
samotné metody drawText()
třídy Canvas
pro
vykreslení textu.
Na následujícím obrázku je vidět text, který se musel pro svou délku zmenšit tak, aby se na šířku vešel do "díry" v grafu. V tomto kroku by ještě nebylo vykreslené pozadí ani samotný graf, ale chci aby bylo vidět, do jakého prostoru se text vmáčkl:
Naše výše deklarované metody přijímají jeden parametr typu
Canvas
. Možná vás napadla otázka, kde tuto instanci vezmeme.
Všechny tyto metody budou později volány v překryté metodě
onDraw()
, která má tento objekt v parametru. K metodě
onDraw()
se brzy prokoušeme Pro dnešek končíme.
V následujícím kvízu, Kvíz - Tvorba vlastní komponenty v Androidu, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.