Android, kontrola stylów i sprawdzenie kodu.

1

Naszła mnie ochota, żeby zrobić poprawki do modułu aplikującego kolory ze zrobionych przeze mnie stylów do podanych elementów View. Jedna mała klasa która steruje całym procesem. Dzięki niej mam możliwość posiadania 3210941241 stylów w aplikacji i zmiany ich jednym kliknięciem ( ale dopiero po otworzeniu nowej aktywności, nie chce bawić mi się w dynamiczną implementację), oraz aplikacja kolorów ze stylów do obiektów które "nie są obsługiwane" automatycznie przez parametry stylów systemu Androida.

Czyli poprawianie wyglądu kodu tego gniota którego nie ruszałem od wielu miesięcy:).

Założenia które musiałem tu uwzględnić:

  1. Ma działać ze starymi wersjami Android API ( xxx < API version 21), czyt. ustawienie w plikach xml. koloru jako "?android:nazwa_atrybutu_ze_stylu" działa tylko dla kilku atrybutów; ma to działać np. na API version 16 oraz dla wszelkiego rodzaju dodanych własnoręcznie atrybutów;
  2. Kolory odczytywane są na podstawie podanego atrybutu (R.attr.nazwa_atrybutu) z bieżącego aktywnego stylu ( wszystkie style mają taki sam zestaw atrybutów, nie ma możliwości, że kolor odpowiadający atrybutowi nie zostanie odnaleziony);
  3. Kolor może być ustawiony dla typowego obiektu View ( kolor tła - background) lub dla View z wektorem jako tło (atrybut wektora "fillPath" - nie można ustawić koloru tego atrybutu bezpośrednio w java., ale można ustawić w xml. wektora DOMYŚLNY kolor biały i w java. DODAĆ na ten biały kolor filtr który da wygląd kolorowego obiektu);
  4. Kolory ustawione będą w obiekcie View jako tło ( nazwaView.setBackgroundColor(nazwaKoloru)) a dla View z vectorem jako colorFilter (nazwaView.getBackground.setColorFilter(nazwaKoloru));
  5. Obiekty będące polem klasy implementującej layout można przekazać bezpośrednio ( np. przekazać button mButton do metod wstawionych poniżej). Jeśli obiekt jest zagnieżdżony w innym View nie należy tworzyć nowych zmiennych za pomocą findViewById(...). Tak samo jeśli nie ma swojej reprezentacji jako pole w tamtej klasie ( np. ImageView ze zdefiniowanym obrazkiem, nie robimy pola w klasie MojaKlasa i findViewById(...)/ButterKnife bo nie ma po co). Ogólnie chodzi o to, że jeśli jest kilka obiektów tego samego typu w widoku, należy je wyciągnąć po typie ( oszczędzanie miejsca - przykład widok z 20 obiektami Button- dałby 20 linijek z wywołaniem metody, ale można iterować po wszystkich obiektach wchodzących w skład widoku, wyciągnąć obiekty danego typu (Button) i w ten sposób dodać referencję do nich do jakiejś listy).
  6. Istnieją następujące przypadki:
    a) mamy bezpośredni dostęp do obiektu dla którego chcemy ustawić kolor, jeden obiekt ( np. ustaw kolor dla tego obiektu ImageButton który jest polem w klasie implementującej layout).
    b) nie mamy bezpośredniego dostępu do obiektu dla którego chcemy ustawić kolor, jeden obiekt ( np. obiekt jest wstawiony w inny obiekt ( np. ListLayout z TextView i ImageView w środku i chcemy ustawić kolor dla ImageView)
    c) nie mamy bezpośredniego dostępu do obiektu dla którego chcemy ustawić kolor, więcej niż jeden obiekt ( np. Fragment mający w sobie 10 ImageView i dla nich ma być ustawiony kolor)
    d) wszystkie powyższe jeszcze raz, ale nie wiemy, czy mamy do czynienia z ustawieniem koloru tła czy dodaniem filtru koloru dla obiektu z wektorem;

Powyższe to wszystko co pamiętam na ten moment.
Podejmie się ktoś dyskusji co tu poprawić/połączyć/wywalić? :D

Klasa której kod chcę poprawić:

public class StyleController {

    private Context mContext;

    public StyleController(Context context) {
        mContext = context;
    }

    public void applyColorAsBackground(View view, int attrId) {
        int colorId = getAttrColor(attrId);
        view.setBackgroundColor(colorId);
    }

    public void applyColorToSingleKnownView(View view, int attrId) {
        int colorId = getAttrColor(attrId);
        setColor(view, colorId);
    }

    public void applyColorToSingleUnknownNestedView(ViewGroup rootView, int attrId, Class<?> lookedType) {
        View view = pickSingleNestedView(rootView, lookedType);
        if (view != null) {
            applyColorToSingleKnownView(view, attrId);
        }
    }

    public void applyColorToMultipleViews(ViewGroup rootView, int attrId, Class<?> lookedType) {
        List<View> viewList = new ArrayList<>();
        pickMultipleNestedViews(rootView, viewList, lookedType);
        applyColorToView(viewList, attrId);
    }

    private void pickMultipleNestedViews(ViewGroup viewGroup, List<View> viewList, Class<?> lookedType) {
        for (int i=0; i< viewGroup.getChildCount(); i++) {
            if (viewGroup.getChildAt(i) instanceof ViewGroup) {
                pickMultipleNestedViews((ViewGroup) viewGroup.getChildAt(i), viewList, lookedType);
            } else if (lookedType.isInstance(viewGroup.getChildAt(i))){
                viewList.add(viewGroup.getChildAt(i));
            }
        }
    }

    private View pickSingleNestedView(ViewGroup viewGroup, Class<?> lookedType) {
        for (int i=0; i< viewGroup.getChildCount(); i++) {
            if (viewGroup.getChildAt(i) instanceof ViewGroup) {
                pickSingleNestedView((ViewGroup) viewGroup.getChildAt(i), lookedType);
            } else if (lookedType.isInstance(viewGroup.getChildAt(i))){
                return viewGroup.getChildAt(i);
            }
        }
        return null;
    }

    private void applyColorToView(List<View> viewList, int attrId) {
        int colorId = getAttrColor(attrId);
        for (View view : viewList) {
            setColor(view, colorId);
        }
    }

    public int getAttrColor(int attrId) {
        TypedValue typedValue = new TypedValue();
        // !!! set boolean resolveRes as true to make it work!!!
        mContext.getTheme().resolveAttribute(attrId, typedValue, true);
        return typedValue.data;
    }

    private void setColor(View view, int colorId) {
        if (isBackgroundInvisible(view)) {
            setBackgroundColor(view, colorId);
        } else {
            setColorFilter(view, colorId);
        }
    }

    private boolean isBackgroundInvisible(View view) {
        return view.getBackground() == null || !view.getBackground().isVisible();
    }

    private void setBackgroundColor(View view, int colorId) {
        view.setBackgroundColor(colorId);
    }

    private void setColorFilter(View view, int colorId) {
        view.getBackground().setColorFilter(colorId, PorterDuff.Mode.MULTIPLY);
    }

    public int getStyleAsInteger(String themeName) {
        int themeId;
        switch (themeName) {
            case "Light":
                themeId = R.style.Light;
                break;
            case "Dark":
                themeId = R.style.Dark;
                break;
            case "Candy":
                themeId = R.style.Candy;
                break;
            case "Dracula":
                themeId = R.style.Dracula;
                break;
            case "Ocean":
                themeId = R.style.Ocean;
                break;
            case "Forest":
                themeId = R.style.Forest;
                break;
            default:
                themeId = R.style.Light;
        }
        return themeId;
    }
}

I przykładowe użycie wygląda tak ( przyciski mają w sobie obrazek wektora, więc wymagane dla nich jest ustawienie filtru koloru a nie zmiana koloru tła:

public class ExampleFragment extends BasicFragment {
 
    @BindView(R.id.reset_button) ImageButton mRefreshButton;
    @BindView(R.id.pause_button) ImageButton mPlayPauseButton;
    @BindView(R.id.lock_button) ImageButton mLockButton;
    @BindView(R.id.map_button) ImageButton mMapButton;
    @BindView(R.id.gps_button) ImageButton mGpsButton;
    @BindView(R.id.network_button) ImageButton mNetworkButton;
    @BindView(R.id.barometer_button) ImageButton mBarometerButton;

    private StyleController styleController;

    private void setButtonColorsByStyle() {
        ViewGroup viewContainer =  (ViewGroup) getView();
        Class<?> lookedType = ImageButton.class;
        applyColorToMultipleViews(viewContainer, R.attr.colorButtonPrimary, lookedType);
    }
}

Mi w oczy od razu rzuciły się:

  • możliwy null zwrócony (!!!) w metodzie "pickSingleNestedView"
  • nazewnictwo "applyColorToSingleKnownView" i "applyColorToSingleUnknownNestedView" - robiłem to, więc wiem =, że Known i Unknown miało oznaczać czy mamy dostęp do obiektu ( bezpośredni) i czy trzeba ustawić tam tło, cz filter, ale nie sądzę, żeby obca osoba łatwo to odczytała
  • możliwość połączenia metod "applyColorAsBackground", "applyColorToSingleKnownView" i "applyColorToSingleUnknownNestedView" - wszystkie ustawiają kolor dla jednego widoku, więc należałoby tutaj pokombinować...
  • metoda "getStyleAsInteger" długi switch

Jakiekolwiek uwagi mile widziane. Istotą wstawienia tego jest refactoring tego kodu do lepszej postaci, nie czy jest zasadny ( chociaż też uwagi przyjmę) :)

1

Prościej by było jakbyś zapodał kod na githuba z przykładową apką.
Co do refaktoringu.

  1. Próbowałeś do tego napisać jakiekolwiek testy?
  2. Skorzystaj może z annotacji, że metoda może rzucić nulla
  3. Ten switch to powinien przyjmować jakiegoś enuma, a nie string
0
panryz napisał(a):

Prościej by było jakbyś zapodał kod na githuba z przykładową apką.
Co do refaktoringu.

  1. Próbowałeś do tego napisać jakiekolwiek testy?
  2. Skorzystaj może z annotacji, że metoda może rzucić nulla
  3. Ten switch to powinien przyjmować jakiegoś enuma, a nie string

Dzięki za podpowiedzi. Co do uwag:

  1. Nie, tak więc chyba powinienem:).

  2. Może jakoś uda mi się połączyć te wszystkie trzy metody o których wspomniałem i wywalić to miejsce.

  3. Co do ostatniego, tłumaczenie skąd pojawił się w ogóle switch ze string/int:
    Style mogę zmieniać/zmieniam z poziomu modułu który implementuje PreferenceScreen z androida.
    W Preferences mam listę która ma w sobie wszystkie możliwe do wyboru style ( w formie ListPreference a w array.xml listy z nagłówkami i wartościami).
    ListPreference obsługuje tablice z wartościami String, nie "int/Integer" ( stąd muszę operować na stringach z nazwą i później jakoś "przekształcić" tę nazwę na id stylu w formie "int" - tutaj wchodzi switch).
    Styl jest przypisywany do Activity w onCreate(), przed wywołaniem super.onCreate(). Tym sposobem jest przypisany do kontekstu i wszystkie fragmenty, widgety itp. które wchodzą w skład tej aktywności będą mieć ten styl ( z kontekstu).
    Przypisanie wygląda tak, że idę do SharedPreferences i odczytuję "parametr" który odpowiada zaznaczonemu polu z ListPreferences ze stylami.
    Tak jak pisałem wcześniej, zwrócony może być tylko String, więc muszę operować na nim.
    Jeśli da się to załatwić za pomocą enumów, to nie wiem o tym ( użyłem ich tylko raz w życiu, więc nie wiem za wiele o nich ^^).
    Idealnie byłoby gdybym mógł zwrócić poprzez SharedPreferences wartość stylu jako "int" ( czyli jego id) i po prostu bym je dalej przekazał, ale nie udało mi się znaleźć takiej możliwości.

Postaram się wstawić całą apkę tuta i dodam razem z linkiem do githuba.
PS. jakbym wstawiał tutaj apk., znasz może jakiś serwis żeby przepuścić jakimś "antywirusem"? Pamiętam, że odwiecznie ktoś narzeka, że "niewiadome źródło" i niesprawdzone/zawirusowane.

0

Podrzucam apk i linki.
plik apk.
xxx

powyższa klasa StyleController ( żeby nie szukać)
xxx

Od siebie dodam, że po intensywnym waleniu we wszystkie widgety, udało mi się zrobić crash aplikacji, ale nie mam czasu poprawić tego teraz. ( w jakimś przypadku musi być gdzieś błąd przy kliknięciu przycisku kłódki "lock" w module z diagramem).
Nie ustawiłem też dokładnie wszystkich kolorów w stylach ( no ale to do dopracowania, sama wizualizacja).
Najlepiej przeczytać readme.me ( które muszę zaktualizować, bo trochę tam rzeczy doszło już).
Tak więc miłego bawienia się:).

EDIT: A, zapomniałbym: włączenie usługi lokalizacji wymagane, bom leń i nie zrobiłem automatycznego żądania tego jeśli jest wyłączona.

EDIT po 2 dniach: zgodnie z przewidywaniami znikome zainteresowanie więc kasuję linki. Gdyby jakiś mod to przeczytał to temat do usunięcia.

1 użytkowników online, w tym zalogowanych: 0, gości: 1