#19 Szybkie rozszerzanie funkcjonalności

Zbliżamy się do końca konkursu DSP, to już przedostatni wpis, noo i trzeba pomyśleć o jakiejś stabilizacji. Aplikacja nie będzie ukończona do dwudziestego wpisu, ale w jakiś sposób musi działać. Finalnie pewnie będzie ukończona we wrześniu, nie będę nad nią działał sam 😉 Dzisiaj dodałem kod odpowiedzialny za uruchamianie zadań praktycznych, testów i customowych prób. Kod dla funkcjonalności porównywania i dodawania. Najwięcej czasu zajęły poprawki w wyglądzie i obsługa nowych przycisków, reszta poszła błyskawicznie, dzięki zaimplementowaniu wielu rzeczy w nadklasie. Szybki rzut oka na aplikację:

Więcej funkcji się już niestety nie pojawi, następny wpis będzie o drobnych poprawkach w wyglądzie, etc. Ale skupmy się na tym co dodałem, czyli np funkcji porównywania z opcją ustawienia sobie czasu i ilości odpowiedzi:

Kodzik głównie dziedziczy, te funkcjonalności się powtarzają, więc w sumie czemu nie 😉 Spójrzmy np na to:

@Override
@OnClick(R.id.successView)
public void onClickSuccessView() {
    if (operation.nextPoint(true))
        nextActivity(true, CompareCheckActivity.this);
    else
        onClickCommon();
}

@Override
@OnClick(R.id.failureView)
public void onClickFailureView() {
    if (operation.nextPoint(false))
        nextActivity(false, CompareCheckActivity.this);
    else
        onClickCommon();
}

@Override
@OnClick(R.id.fab)
public void onClickFab() {
    if (!isScreenFiled()) {
        stopTime();
        fillScreen(counterRight == counterLeft, true);
    } else {
        if (counterRight == counterLeft)
            onClickSuccessView();
        else
            onClickFailureView();
    }
}

@Override
public void onClickCommon() {
    stackBricksElementsOperation.hideStack(bricksLeftArray);
    stackBricksElementsOperation.hideStack(bricksRightArray);
    unFillScreen(counterRight == counterLeft, true);
    createReadyView();
}

Jak już pisałem wcześniej, mamy tutaj interfejs operation który decyduje którą implementację wybrać. Metoda nextPoint odpowiada true, jeśli mamy wystartować następną aktywność, lub false jeśli wyczyścić ekran i wykonać inne operacje, które mamy opisane w jednej metodzie: onClickCommon. Ważne aby wyczyścić ekran, zanim wylosujemy następne numery. Animacja się rozpocznie w osobnym wątku, i wszystko zdąży się wykonać, zanim zauważymy zmiany. To bardzo fajna cecha, nie trzeba o tym myśleć i przydatna w większości przypadków. Ale niestety nie we wszystkich, np trzeba pamiętać, aby wyłączać parametr clickable na elementach, kiedy uruchamiamy jakąś długo trwającą animację i nic ma jej nie przerywać.

Jak widać czasem przygotowanie sobie dobrego API, i wydzielenie kodu który się powtarza niejednokrotnie trwa dłużej, niż faktyczne korzystanie z niego. I wydaje mi się, że to dobra droga, w przypadku rozrastania się programu, o wiele łatwiej można dodawać nowe funkcje.

Projekt na Github.

Pozdrawiam!

#18 Tworzenie własnych zadań

Poprzednio pokazałem jak osiągnąłem aktywności obsługujące testy, a dzisiaj dzięki kilku zmianom w kodzie udało się zrobić tak, aby użytkownik mógł sam wyznaczyć sobie czas, ilość odpowiedzi i maksymalną ilość pomyłek. Było to naprawdę proste, a spójrzmy jak fajnie działa:

A teraz wszystkie zmiany, które poczyniłem w kodzie wyglądają tak:
1) w klasie MenuActivity obsługa następnego activity -> Custom

2) stworzenie CustomActivity w którym wpisujemy ile ma być zadań etc..
Intent intent = new Intent(NumbersCustomActivity.this, ChoiceCheckActivity.class);
intent.putExtra(Const.CUSTOM, true);
intent.putExtra(Const.CUSTOM_TIME,
     Integer.parseInt(timeAmount.getText().toString()));
intent.putExtra(Const.CUSTOM_GOOD_ANSWER,
    Integer.parseInt(goodAnswerAmount.getText().toString()));
intent.putExtra(Const.CUSTOM_BAD_ANSWER,
    Integer.parseInt(badAnswerAmount.getText().toString()));
startActivity(intent);

A także przekazujemy dane które zostały wpisane w pola. Od razu parsujemy na inty, żeby nie było problemu 😉 Formatki EditText po stronie xml są tak sformatowane, żeby przyjmować tylko cyfry 😉

Edycji w istniejącym kodzie też nie było zbyt wiele 😉

3) zmieniła się klasa CounterOperation, metoda getData()

private void getData(Intent intent) {
    boolean test = intent.getBooleanExtra(Const.TEST, false);
    if(test) {
        int c = intent.getIntExtra(Const.CLASS, 0);
        if (c == 1) {
            time = Const.I_CLASS_TIME;
            goodAnswer = Const.I_CLASS_GOOD_ANSWER;
            badAnswer = Const.I_CLASS_BAD_ANSWER;
        } else if (c == 2) {
            time = Const.II_CLASS_TIME;
            goodAnswer = Const.II_CLASS_GOOD_ANSWER;
            badAnswer = Const.II_CLASS_BAD_ANSWER;
        } else {
            time = Const.III_CLASS_TIME;
            goodAnswer = Const.III_CLASS_GOOD_ANSWER;
            badAnswer = Const.III_CLASS_BAD_ANSWER;
        }
    }
    else {
        time = intent.getIntExtra(Const.CUSTOM_TIME, 0);
        goodAnswer = intent.getIntExtra(Const.CUSTOM_GOOD_ANSWER, 0);
        badAnswer = intent.getIntExtra(Const.CUSTOM_BAD_ANSWER, 0);
    }
}

Dzięki temu obsługuje dane przesłane przez użytkownika (obiekt Intent może być naprawdę bardzo przydatny 😉

4) doszła zmienna boolean o nazwie custom, która ma zastosowanie w tych samych przypadkach co zmienna test, czyli zasady są takie same, ale wartości może podać użytkownik.

W ten oto sposób w kilka chwil doszła kolejna logika do zadań. Także możliwości mamy już całkiem sporo i praktycznie zostało to wszystko osiągnięte dzięki kilku liniom kodu, dzięki wcześniejszemu przygotowaniu sensownych nadklas.

Ten post ma numer 18 w konkursie DSP, cieszę się, że jest tyle osób które przegląda mojego bloga (dzięki! :). Pozostały jeszcze dwa DSP i zastanawiam się, czy pisanie dalej, tzn po zakończeniu konkursu na temat tego projektu z tak dużą intensywnością ma sens? Bo mam w głowie kilka pomysłów na to co może się tutaj dziać, ale o tym za kilka wpisów 😉 Będzie fajnie 🙂

Projekt na github.

Pozdrawiam!

#17 Grywalizacja, czyli dodanie kolejnej logiki to zadań

Kolejnym krokiem w rozwoju naszej aplikacji jest napisanie następnej logiki 😉 Ta jednak będzie nieco bardziej będzie opierała się o zasady grywalizacji.

Przypomnę dla porządku czym jest grywalizacja, chodzi o zastosowanie zdobywania punktów, odznak, poziomów w (naszym przypadku) procesie uczenia. Pomysł jest taki, aby użytkownicy mogli sobie wybrać zestaw zadań do rozwiązania na 3 poziomach. Najwyższy poziom daje najmniej czasu na odpowiedź, oraz nie pozwala na pomyłki. Wyższe pozwalają na dwie lub jedną pomyłkę. Pokażę może teraz jak to działa:

Jak widać ekran po wykonaniu zadania nie jest jeszcze zaprojektowany. To tam będzie informacja, czy użytkownik dostał daną odznakę, czy mu się nie udało. Na ekranie widzimy wyświetlone zadanie, oraz pomniejszającą się ikonkę zegarka (taki na ten moment miałem pomysł na pokazanie uciekającego czasu 😉 oraz liczbę punktów. Brakuje jeszcze sporo rzeczy (między innymi nie wyświetlam jeszcze opisu zadań, czyli co trzeba zrobić i w jakim czasie na danym poziomie).
Parę słów o implementacji, po pierwsze, niech żyją interfejsy! Doszedłem do wniosku, że jedyną informacją jaką powinienem przekazywać jest informacja, czy użytkownik dał dobrą czy złą odpowiedź, oczekując w odpowiedzi czy w związku z tym uruchomić następną aktywność, czy niech odpowiada dalej. Stworzyłem więc sobie do tego interfejs:
public interface Operation {
    boolean nextPoint(boolean b);
}

Następnie stworzyłem klasę która dziedziczyła z klasy głównej i którą będą rozszerzały wszystkie zadania. Klasa ta zawiera np taki fragment:

Intent intent = getIntent();
setPractice(intent.getBooleanExtra(Const.PRACTICE, false));
setTest(intent.getBooleanExtra(Const.TEST, false));
if (isPractice())
    operation = new CounterOperation(this);
else if (isTest())
    operation = new CounterOperation(this, getIntent());
else
    operation = new StarsOperation(this);

Jak widzimy nadklasa decyduje której implementacji użyć, W klasie właściwej używam tylko:

@Override
@OnClick(R.id.successView)
public void onClickSuccessView() {
    if(operation.nextPoint(true))
        nextActivity(true, ChoiceCheckActivity.this);
    else {
        onClickCommon();
        unFillScreen(true, true);
    }
}

@Override
@OnClick(R.id.failureView)
public void onClickFailureView() {
    if(operation.nextPoint(false))
        nextActivity(false, ChoiceCheckActivity.this);
    else {
        onClickCommon();
        unFillScreen(false, true);
    }
}

Sprawdzam co zwraca implementacja i decyduję, czy startuję z aktywnością, czy czyszczę ekran i użytkownik ma następną szansę. Ta nadklasa decyduje również o wystartowaniu animacji (konstruktor przyjmujący obiekt Intent ją implementuje). Ma też metody start i stop time:

public void startTime() {
    if(isTest()) {
        filledScreen= false;
        counterOperation.startTime().setAnimationListener
                 (new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {  }
            @Override
            public void onAnimationEnd(Animation animation) {
                if(!filledScreen)
                    fillScreen(false, true);
            }
            @Override
            public void onAnimationRepeat(Animation animation) {  }
        });
    }
}

public void stopTime() {
    if(isTest())
        counterOperation.stopTime();
    filledScreen = true;
}

Niestety listener obiektu Animation wymusza na mnie implementację 3 metod, chociaż korzystam tylko z jednej.

To by było chyba na tyle jeśli chodzi o postępy w projekcie. Wkrótce następne wpisy, stay tuned! 😉

Pozdrawiam!

#16 Praktyka czyni mistrza, czyli dopasowujemy aktywności do nowej logiki

Wspominałem w którymś wcześniejszym wpisie, że te ekrany, na których aktualnie mamy 5 prób zostaną przerobione, na takie bardziej arcadowe. O tym chciałem też dzisiaj napisać, udało mi się całkiem bezboleśnie przeprowadzić taką operację. Oto jak to wyglądało.

Po pierwsze przebudowałem trochę ekran główny dodając przycisk „Practice” i pod tym przyciskiem ukryte są kolejne aktywności:

Po drugie potrzebowałem w jakiś sposób odróżnić kiedy aktywność ma zachowywać się normalnie, a kiedy jest to ekran praktyk. Osiągnąłem to korzystając z możliwości przekazania danych w intencji. Przekazanie wygląda tak:
intent.putExtra(Const.PRACTICE, true);
a odczyt:
Intent intent = getIntent();
practice = intent.getBooleanExtra(Const.PRACTICE, false);
Czyli przekazujemy wartość boolean, przy odczycie z kolei drugi parametr oznacza wartość domyślną, czyli co tam przypisać jeśli nic nie znajdziemy.
Po trzecie, potrzebny był widok w którym mógłbym zapisywać zdobyte punkty i popełnione pomyłki, wykorzystałem istniejące już obiekty i oto jest:
Ikona zegarka w bieżącej implementacji jest ukryta, ale przyda się w przyszłości 😉 Reszta mam nadzieję jest zrozumiała i czytelna.
Po czwarte i ostatnie, implementacja. Tutaj trzeba było dodać warunek, jak aplikacja ma się zachowywać kiedy przyszła z ekranu praktyk, czyli powyżej odczytany intent Extra ma wartość true. Wklejam poniżej przykładowy kod jak to wygląda:
@Override
@OnClick(R.id.successView)
public void onClickSuccessView() {
    if (isPractice()) {
        stackBricksElementsOperation.hideStack(bricksArray);
        createReadyView();
        counterOperation.count(true);
        unFillScreen(true, true);
        screenFiled = false;
    } else {
        starsOperation.nextStar(true);
        gameCounter++;
        if (gameCounter == 5)
            nextActivity(true, ChoiceCheckActivity.this);
        else {
            stackBricksElementsOperation.hideStack(bricksArray);
            createReadyView();
            unFillScreen(true, true);
            screenFiled = false;
        }
    }
}

 

Te warunki pewnie można napisać lepiej, także pomyślę nad nimi i je poprawię, jeśli myślenie poskutkuje 😉 A na sam koniec oto jak to wygląda na jednym przykładzie:
Jak widać poszło całkiem sprawnie i bezboleśnie. Następnym krokiem będzie wykorzystanie pokazanego już tutaj zegarka, ale o tym innym razem.
Pozdrawiam!

#15 Aktywność wskazania ilości i kolejne poprawki

Ale tytuł posta 😉 Tak naprawdę to tylko tak zawile brzmi sprawa jest naprawdę prosta, mamy narysowany stos (a jakże) i wybieramy ile elementów się na nim znajduje 😉 Noo i oczywiście trzymając się konwencji mamy najpierw wprowadzenie, a następnie 5 prób na punkty. Oto jak to wygląda.

Sam widok tablicy numerycznej można by jeszcze poprawić, ale nie będzie to trudne 😉 Dzięki ostatnim poprawkom dodawanie tego typu zadanek to już naprawdę chwilka, wydaje mi się, że nie wiele więcej da się jeszcze tutaj zrobić. Ale wprowadziłem jeszcze dwie istotne poprawki, mianowicie, kiedy dodałem zadanka i chciałem je umieścić w innym miejscu na tej liście:

To okazało się, że musiałem edytować kilka klas, aby wiedziały jak się prawidłowo przełączać. Dlatego wyciągnąłem to wszystko w jedno miejsce, usunąłem z funkcji jeden argument, tak to wygląda:

Intent intent = new Intent(packageContext,
    getNextActivity(packageContext));
startActivity(intent);

...

private Class getNextActivity(Context context)
{
    if(context.getClass().equals(BricksActivity.class))
        return BricksOrderActivity.class;
    else if(context.getClass().equals(BricksOrderActivity.class))
        return ChoiceActivity.class;
    else if(context.getClass().equals(ChoiceActivity.class))
        return ChoiceCheckActivity.class;
    else if(context.getClass().equals(ChoiceCheckActivity.class))
        return CompareActivity.class;
    else if(context.getClass().equals(CompareActivity.class))
        return CompareCheckActivity.class;
    else if(context.getClass().equals(CompareCheckActivity.class))
        return AddingActivity.class;
    else if(context.getClass().equals(AddingActivity.class))
        return AddingCheckActivity.class;
    else
        return MenuActivity.class;
}

I teraz już kolejne elementy mam w jednym miejscu 😉 Dodanie następnego zadanka nie będzie wymagało edycji w innych aktywnościach.

Kolejną rzeczą którą poprawiłem jest niepowtarzanie się animacji fab przeniesione do samej obsługi animacji, dzięki temu nie musimy o to dbać w każdej aktywności osobno.

private boolean currentShapeIsDifferent(int resId)
{
    if(resId==previousAnim)
        return false;
    else
    {
        int pAnim = previousAnim;
        previousAnim = resId;
        if(resId==R.drawable.ic_line_to_undone&&
      pAnim==R.drawable.ic_done_to_undone)
            return false;
        else if(pAnim==R.drawable.ic_line_to_undone&&
      resId==R.drawable.ic_done_to_undone)
            return false;
        else if(resId==R.drawable.ic_line_to_done&&
      pAnim==R.drawable.ic_undone_to_done)
            return false;
        else if(pAnim==R.drawable.ic_line_to_done&&
      resId==R.drawable.ic_undone_to_done)
            return false;
        else
            return true;
    }
}

Jak widać logika nie jest taka całkiem prosta, ale ogólnie chodzi o to, że jeśli ostatnia figura w animacji jest taka sama, to nie trzeba animować, wizualnie wygląda to dobrze 😉 Przy okazji, Android Studio, czasem podpowiada jak można rozwiązać to w kodzie krócej, opcją simplify. Tym razem też podpowiedział:

return !(resId == R.drawable.ic_line_to_undone
&& pAnim == R.drawable.ic_done_to_undone)
&& !(pAnim == R.drawable.ic_line_to_undone
&& resId == R.drawable.ic_done_to_undone)
&& !(resId == R.drawable.ic_line_to_done
&& pAnim == R.drawable.ic_undone_to_done)
&& !(pAnim == R.drawable.ic_line_to_done
&& resId == R.drawable.ic_undone_to_done);
}

Sam dodałem entery, żeby nie był to jeden ciąg 😛 Simplify, że hej, oczywiście nie zostawiłem w tej wersji 😉

Ps. Aktualna liczba wyświetleń to: 4993, także wkrótce pyknie okrągłe 5000 odsłon bloga 😉 Dzięki wielkie 🙂 A po zakończeniu konkursu blog na pewno nie umrze, mam już kilka pomysłów na niego 😉 Ale o tym za jakiś czas, na razie mam 75% DSP wpisów 🙂

Projekt na github.

Pozdrawiam!

#14 Porządki są dobre – 2 nowe aktywności – porównywanie i dodawanie

Wczoraj pisałem o refactoringu, a dzisiaj udało mi się dodać dwie nowe funkcjonalności, co pozwoliło na to, że projekt trochę bardziej przypomina coś, co kiedyś mogło by działać 😉 Nie są to zupełne nowości, ale takie, które były potrzebne, aby utrzymać logikę, jest nowa aktywność dodawania i porównywania. Ogólnie wygląda to w ten sposób, że najpierw mamy etap prezentacji, w którym jest pokazywane po reakcji fab jak to ma działać, a potem rozwiązujemy zadanka. Na razie tylko 5 prób, pewnie finalnie będzie opcja odpalenia ich również na punkty 😉
To ja może na początek zademonstruję:
Jak widzimy, najpierw mamy pokazane, kiedy jest ok (belki są w równowadze) a po kliknięciu przechodzimy do testów, gdzie kliknięcie sprawdza, czy się nie pomyliliśmy 😉 Mamy gwiazdki informujące o tym, czy wszystko się udało.
Kodziku za bardzo nie mam co pokazywać, ponieważ z powodu refaktoryzacji, jest bardzo podobny do tego co już pokazywałem.
Kolejne dwie funkcjonalności to podanie wyniku dodawania, tutaj również na początku jest pokazane jak to działa, a później sprawdzenie z gwiazdkami 😉
Tutaj jeśli chodzi o kodzik, to mogę pokazać metodę która odpowiada za zmiany FAB:
private void changeFab() {
    if (counter != result && firstChange) {
        morphingAnimation.animFab(
  R.drawable.ic_done_to_undone, R.color.colorFailureRed);
        firstChange = false;
    }
    if (counter == result) {
        morphingAnimation.animFab(
  R.drawable.ic_undone_to_done, R.color.colorSuccessGreen);
        firstChange = true;
    }
}
I jeszcze na koniec, znalazłem dzisiaj niezłego buga w mojej aplikacji, mianowicie kod .setVisibility(View.VISIBLE) / .setVisibility(View.INVISIBLE) nie reagował. Tzn, element ciągle był niewidoczny. Wymieniłem implementację na .setAlpha (dokonywałem zmian tylko w dwóch klasach, czyli chyba refactoring się powiódł), ale błąd dalej występował. Okazało się, że obiekty zapamiętują jeśli były animowane i wtedy, jeśli animacja kończyła się zniknięciem, to można sobie było próbować. Naprawa była trywialna, ale nim do tego doszedłem…. Oto jak wygląda kod który naprawia tego buga:
public void hideStack(View bricks[]) {
    for (View brick : bricks) {
        brick.setAlpha(0);
        brick.clearAnimation();
    }
}
A właściwie za naprawdę odpowiada dyrektywa .clearAnimation(). Cóż, jestem z siebie naprawdę dumny, że to odkryłem 😉
Projekt na github, a także coś extra, tutaj znajduje się plik apk, który można sobie bezpośrednio zainstalować na telefonie. Link jest aktywny, tzn aplikacja będzie się tam co jakiś czas podmieniać 😉
Pozdrawiam!

#13 Refactoring, czy na pewno zrobiłem go właściwie?

Dawno nic nowego nie napisałem, a to dlatego, że następny wpis miał być o refactoringu. Skoro ma być o refactoringu, to trzeba go zrobić. Noo i się zaczęło 😉 Ale po kolei.

Założenia wstępne były takie, że chcę dodać nadklasę, w której będą metody odpowiedzialne za rzeczy wspólne dla wszystkich aktywności. Noo właśnie, ale co to tak właściwie znaczy rzeczy wspólne? Wymyśliłem, że każda aktywność kończy się wypełnieniem ekranu i po kliknięciu uruchamia nową aktywność. Noo dobrze, ale ekran możemy wypełnić jednym lub drugim widokiem w zależności od tego, czy poprzednie zadanie zakończyło się sukcesem, czy porażką. I co wtedy, oddzielne metody, czy zmienna typu boolen w parametrze wywołania? Takich pytań nasuwa się setki, a nad odpowiedziami na niektóre naprawdę długo myślałem. Nawet takie rzeczy jak decyzja o nazwie metody, to wszystko dosyć mocno mnie przytłoczyło, dlatego też długo nie mogłem skończyć ani poprawek, ani tego wpisu. Ostatecznie jednak sądzę, że mi się udało i staram się nie dopuszczać do siebie myśli, że to mogło być zrobione inaczej 😉

Klasa nadrzędna przejęła całkiem sporo funkcji, np przy pomocy ButterKnife podpinam kilka rzeczy właśnie w klasie nadrzędnej:

@Bind(R.id.fab)
FloatingActionButton fab;
@Bind(R.id.layoutSuccess)
ViewGroup layoutSuccess;
@Bind(R.id.successView)
ImageView successView;
@Bind(R.id.layoutFailure)
ViewGroup layoutFailure;
@Bind(R.id.failureView)
ImageView failureView;

Wymuszam też przeciążenie metody createViews(), aby w aktywności docelowej wywołać ją w metodzie onCreate. Dzięki temu nie muszę tych samych kawałków kodu pisać wszędzie, i mam kilka rzeczy załatwionych zaraz na starcie (np metoda calculateValues() ustawia wartości dla animacji circularReveal).

public void createViews() {
 setNaviBarColor();
 calculateValues();
 ButterKnife.bind(this);
 Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
 setSupportActionBar(toolbar);
 assert getSupportActionBar() != null;
 getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}

W tej klasie obsługuję też wypełnienie ekranu i cofnięcie tego wypełnienia, oraz start aktywności z przygotowaniem całej animacji etc.

Używam tego w ten sposób, że teraz moja metoda onCreate() wygląda tak:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_compare);
        createViews();
    }

dyrektywa super w metodzie createViews() zapewnia mi wywołanie wszystkich uniwersalnych zadań, a ja mogę zająć się tym co jest istotne, czyli logiką.

Start nowej aktywności też zajmuje już tylko kilka linii:

@Override
@OnClick(R.id.successView)
public void onClickSuccessView() {
    nextActivity(true, CompareActivity.this, CompareCheckActivity.class);
}

Podsumowując, próba zrobienia tego refactoringu całkiem sporo mi dała. Musiałem zadać sobie kilka pytań i uświadomić, czego jeszcze nie wiem. A wiedza o tym, czego się nie wie, to już całkiem sporo. Wiem, że jeszcze dużo muszę się nauczyć. I pewnie kiedyś jeszcze napiszę wpis o refactoringu, jak na razie wiem, że nic nie wiem 😉

Projekt na github, gdyby ktoś chciał obejrzeć zmiany, porównywać z 288370c

Pozdrawiam!

#12 Animacja zmiany aktywności

W poprzednich częściach tej serii zatrzymaliśmy się na tym, że jeden kolor wypełnia cały ekran. Dzisiaj pójdziemy krok dalej, czyli opiszę jak zrobić płynne przejście pomiędzy jedną, a drugą aktywnością.

Oto kroki które chcemy wykonać po wypełnieniu ekranu kolorem zieleni lub czerwieni:

1) płynna zmiana koloru na biały
2) jednoczesna animacja obiektu, zamiana symbolu „wykonane” na strzałkę
3) jednoczesna zmiana przycisku fab
4) kiedy już kolor zmieni się na biały usunięcie animacji przejścia pomiędzy aktywnościami
5) start nowej aktywności z animacją pojawiania się

Metoda, które wykonuje pierwsze cztery operacje wygląda tak:

MorphingAnimation morphingAnimationView = new 
    MorphingAnimation(getApplicationContext(), successV);
morphingAnimationView.animImageView(R.drawable.ic_done_to_line);
animateFab(R.drawable.ic_done_to_line, false);
ValueAnimator colorAnim = ObjectAnimator.ofInt(
    layoutSuccess, "backgroundColor",
    getResources().getColor(R.color.colorSuccessGreen), 
 getResources().getColor(R.color.icons));
colorAnim.setDuration(500);
colorAnim.setEvaluator(new ArgbEvaluator());
colorAnim.start();
colorAnim.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        super.onAnimationEnd(animation);
        Intent intent = new Intent(NumbersOrderActivity.this, 
      NumbersCompareActivity.class);
        startActivity(intent);
        overridePendingTransition(0, 0);
    }
});

Nowością która się tutaj pojawia jest klasa ValueAnimator. Klasa akurat w tym przypadku pozwala na płynną zmianę kolorów obiektu, z zielonego na biały. Na klasie jest założony listener, który po zakończeniu animacji uruchamia nową aktywność, z kolei metoda overridePendingTransition(0, 0) powoduje usunięcie domyślnej animacji otwarcia nowego activiti. Chciałem umieścić animację rozjaśniania w tym miejscu, ale zadziałało by to tak, że rozjaśniał by wszystko łącznie z paskiem i przyciskiem fab, a tego nie chciałem, celem było aby zmieniała się tylko zawartość ekranu. Przyjmowane przez metodę parametry to zero i zero co oznacza usunięcie animacji, pierwszy z tych parametrów, to animacja włączenia nowego activiti, drugi to wyłączenie starego. Jest też tutaj pewna asynchroniczność, mianowicie czas zmiany kolou to 500ms, z kolei animacja zmiany kształtu trwa 200ms, możliwe, że jeszcze w jakiś sposób to zmienię, ale na ten moment wygląda chyba nieźle.

Ostatnią istotną rzeczą jest kod, którego użyłem w nowej aktywności, wykonuje on punkt 5, czyli odpowiada za animację rozjaśnienia i jest uruchamiany podczas tworzenia activity, dzięki temu mamy animację już od samego początku. Po stronie javy prezentuje się tak:

Animation animFadeIn = AnimationUtils.loadAnimation(
    getApplicationContext(), R.anim.fade_in);
numbersCompareActivity.startAnimation(animFadeIn);

Po stronie xml w ten sposób:

<alpha
    android:duration="@integer/anim_duration"
    android:fromAlpha="0.0"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:toAlpha="1.0" />

Wydaje mi się, że jest samo-opisywalny i nie za wiele mogę tutaj powiedzieć 😉 A tak wygląda efekt finalny który udało mi się uzyskać:

Dodaliśmy kolejną funkcję, więc czas na rozszerzenie klasy bazowej, aby nie pisać ciągle tego samego. Ale jak bym to widział, opisze już następnym razem 😉
Pozdrawiam!

#11 Cegła

Czasem w projekcie tak się zdarza, że programista musi zająć się grafiką. I to nigdy nie kończy się dobrze 😉 Chociaż ja uważam, że mi wyszło. Ale do rzeczy, jak pisałem wcześniej zastąpię ten jednobarwny obiekt mający przypominać klocek / cegłę właściwym obiektem. Z racji tego, że nie mogłem znaleźć niczego co by pasowało, postanowiłem stworzyć go sam 😉 Tak wygląda efekt mojej pracy.

Blender renderował przez 37 godzin 😉 Oczywiście żartuję, użyłem painta, gdzie wypełniłem obszar, następnie nałożyłem ramkę, oraz użyłem efektu sprayu z kolorem brązowym 🙂
Przy okazji umieszczenia tego obiektu w aplikacji chciałem zwrócić na coś uwagę, mianowicie jak zapobiec zniekształcaniu grafiki w Androidzie. Kilka zrzutów ekranu obrazujących problem:

 

 

Jak widać obrazek w mniejszym lub większym stopniu jest rozciągnięty, co w sumie nie powinno nas dziwić. Ale istnieje lekarstwo, a jego nazwa brzmi – nine patch.
Narzędzie zostało udostępnione razem z sdk Androida i znajduje się w folderze tools, należy uruchomić je z wiersza poleceń, nazywa się draw9patch.bat. W sieci znajduje się również kilka wersji, z jednej z nich skorzystamy, a w ten sposób wygląda narzędzie dostarczane razem z SDK:
My natomiast użyjemy Android Asset Studio – nine patches, dlatego, że ma nieco bardziej przejrzysty interfejs. Ogólnie w naszym przypadku nie ma nic skomplikowanego, zacząłem od zmiany rozmiaru tej belki, ustawiłem go na 100×1000 px, oraz lekkiego wyrównania krawędzi. Następnie wrzuciłem na stronę internetową, i ustawiłem odpowiednio rozciąganie, tak to wyglądało:
Cała logika opiera się na tym, że odpowiednie obiekty rozciągają się w różne strony, według takiego schematu.
Jak widać mamy tutaj 9 obszarów, strzałki oznaczają kierunki rozciągania, z kolei X, oznacza, że obraz w tym miejscu się nie zmienia. Pliki typu nine-path od zwykłych png różnią się dodatkowy rozszerzeniem .9 po nazwie pliku, system Android traktuje je jak zwykłe grafiki. Po pobraniu i podmianie plików otrzymałem taki efekt:

 

Problem udało się rozwiązać, zajęło to dosłownie chwilkę, a znacznie poprawia wizualne doznania użytkowników 😉
Pozdrawiam!

#10 Circular reveal animation

W poprzednim wpisie pokazałem jak działa aktywność dodawania, ale w jednym miejscu nie było w ogóle animacji, co skutkowało dziwnym pojawianiem się obiektów. Dzisiaj zajmiemy się dalej tym oknem i dodamy tą animację.

Po poprawkach aktywność  działa w ten sposób, że po kliknięciu na strzałkę sprawdza, czy wartość jest prawidłowa. Na tej podstawie rozpoczyna animację wypełniania widokiem sukces:

lub porażka

Widoki te są zdefiniowane jako osobne layouty, z ustawionym parametrem hide. Po zakończeniu wypełniania w tle ustawia wartości dla kolejnego zadania, tj liczba belek na stosie, oraz uzupełnia gwiazdkę, w zależności od wyniku poprzedniego działania. Po kliknięciu na środek ekranu cofa animację i wyświetla widok z nowymi wartościami. Dzisiaj chciałem się skupić właśnie na tej animacji wypełniania ekranu zawartością.

Nazywa się ona circular reveal i pojawiła się razem z Material Design, choć aktualnie znajduje się już w support library i możemy z niej korzystać w starszych wersjach. Metoda tworząca tą animację przyjmuje aż 5 parametrów:
– widok na którym wywołujemy animację
– środek tego widoku na którym wywołujemy animację (współrzędne x, y)
– punkt startowy i końcowy promienia animacji.

Oto jak wygląda ta animacja, ustawiłem czas trwania na 4 sekundy:

Kodzik, który obsługuje tą animację w przypadku prawidłowej odpowiedzi wygląda tak:
morphAnim.animFab(R.drawable.ic_line_to_done, true);
Animator anim = ViewAnimationUtils.createCircularReveal(layoutSuccess,
        centerX, centerY, startRadius, endRadius);
layoutSuccess.setVisibility(View.VISIBLE);
anim.addListener(new AnimatorListenerAdapter() {
    @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            addStar(counter == result);
            createReadyView();
        }
    });
anim.start();
Ważną rzeczą jest ustawienie parametru visibility dla layoutu który ma przykryć nasz główny layout. Jeszcze parametry które wykorzystujemy w tej metodzie:
centerX = contentNumbersBricksAdding.getRight();
centerY = contentNumbersBricksAdding.getBottom();
double valueToSqrt =
Math.pow((double) contentNumbersBricksAdding.getWidth(), 2)
+ Math.pow((double) contentNumbersBricksAdding.getHeight(), 2);
endRadius = (int) Math.sqrt(valueToSqrt);

Pobieramy X i Y, czyli środek koła z którego będzie rozpoczynała się animacja, ustawiamy startRadius na 0, oraz obliczamy endRadius z wzoru na przekątną prostokąta, czyli tego wzoru:

Dla animacji powrotnej, zamieniamy po prostu punkt startowy i końcowy promienia, czyli startRadius z endRadius. Oto jaki efekt uzyskujemy finalnie, po ustawieniu animacji do normalnej szybkości:
Ps. To już 10 wpis, czyli jesteśmy na półmetku w konkursie DSP 😉
Pozdrawiam!