O implementacji widoków w Androidzie – DSP#06

Android posiada bardzo bogatą kolekcję wszelkiego rodzaju elementów interfejsu użytkownika: przyciski, pola tekstowe, checkboxy, przełączniki, to oczywiście te podstawowe, ale po za nimi występują konkretne widoki całego kalendarza, map, albo np web view – czyli wyświetlenie konkretnego kodu HTML. Wszystkie te widoki mają wspólną nadklasę, z której ja również skorzystam tworząc swoją bibliotekę.

Najkrócej wprowadzając w podstawy, w androidzie rozróżniamy aktywności (dziedziczące po klasie activity i pochodnych), w których znajduje się minimum metoda onCreate(..):

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val toolbar = findViewById(R.id.toolbar) as Toolbar
        setSupportActionBar(toolbar)
    }

To aktywność jest najczęściej uruchamiana po starcie aplikacji, to ona decyduje który widok wyświetlić. Widoki, dziedziczące po klasie View, tutaj musimy po prostu nadpisać konstruktory. To na tym dzisiaj się skupię, ponieważ udało mi się zaimplementować w bibliotece, oraz uruchomić w aplikacji pierwszy, podstawowy widok, który wyświetla aktualny czas w milisekundach.

Widok, to podstawowy „składnik” dla wszystkich elementów interfejsu w Androidzie. Widoki są zbierane przez grupy widoków ViewGroups , które z kolei są bazą dla layout’ów. Ok, łatwiej to wyjaśnić rysunkiem niż słownie.

Tworzony przeze mnie DayView również dziedziczy po klasie View. A żeby cokolwiek się w nim znalazło należy przeciążyć metodę onDraw(..), ale może najpierw o konstruktorach.

    constructor(ctx: Context) : super(ctx)
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)

Tak wyglądają w moim kodzie, pierwszy z nich przyjmuje oczywiście kontekst aplikacji, drugi kontekst i parametry które mogą dotyczyć jego wyglądu i mogą być zdefiniowane w pliku layoutu, jest wywoływany kiedy widok jest łączony ze swoją reprezentacją w xmlu (przyda się to później, kiedy będziemy mieli  kolor tekstu, etc podane w pliku z wyglądem naszego okna).

A teraz co nieco o metodzie onDraw(..), po pierwsze przyjmuje ona parametr Canvas który jest „płótnem” na którym możemy rysować korzystając z metod typu drawOval(..), drawRect(..) i podobnych. Do rysowania potrzebujemy jeszcze obiektu typu Paint który przechowuje nam kolory, rozmiary, kształty, dzięki którym możemy rysować. A teraz kodzik z tego co udało się do tej pory zrobić:

override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        paint.color = Color.WHITE
        paint.style = Paint.Style.FILL
        canvas?.drawPaint(paint)
        drawTextCenter(canvas, paint, System.currentTimeMillis().toString())
    }

    private fun drawTextCenter(canvas: Canvas?, paint: Paint, text: String) {
        canvas?.getClipBounds(rect)
        val cHeight = rect.height()
        val cWidth = rect.width()
        paint.setTextAlign(Paint.Align.LEFT)
        paint.color = Color.BLACK
        paint.textSize = 40f
        paint.getTextBounds(text, 0, text.length, rect)
        val x = cWidth / 2f - rect.width() / 2f - rect.left
        val y = cHeight / 2f + rect.height() / 2f - rect.bottom
        canvas!!.drawText(text, x, y, paint)
    }

Tak naprawdę powyższy kod oznacza rysowanie tekstu na środku płótna, i zostanie pewnie zastąpiony w bardziej logiczny sposób, ale czemu on taki pokręcony? Cóż, android czasem miewa implementacje, w których ot tak wprost nie da się wyśrodkować czegoś na ekranie. Rozwiązanie problemu pochodzi ze stackOverflow gdzie zostało dobrze opisane, ja je tylko przełożyłem na Kotlina.

Pozostałymi rzeczami które zrobiłem w projekcie jest podłączenie biblioteki do aplikacji, ponieważ jeszcze nie została opublikowana, to nie było akurat specjalnie trudne, wymagało jednej linii:

    compile project(path: ':dayview')

W pliku build.gradle w module app. Potem mogłem dodać już mój widok z modułu dayview, wygląda on tak:

<pl.digitalzombielab.dayview.DayView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/dayView"/>

I oczywiście skorzystać z tego identyfikatora w aktywności, tak wygląda aktywność po wywaleniu zbędnych opcji:

package pl.digitalzombielab.dayviewdemo

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.Menu
import android.view.MenuItem
import pl.digitalzombielab.dayview.DayView

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val dayView = findViewById(R.id.dayView) as DayView
        val toolbar = findViewById(R.id.toolbar) as Toolbar
        setSupportActionBar(toolbar)
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        val id = item.itemId


        if (id == R.id.action_settings) {
            return true
        }

        return super.onOptionsItemSelected(item)
    }
}

Całość została zaccomitowana jako „first simple view”.

Kolejnym krokiem będzie stworzenie zdecydowanie ładniejszego wyglądu kartki z kalendarza, a także projekt API z którego będziemy docelowo korzystać.