Реализация системы скинов в Android-приложении или смена цветовой гаммы в один клик

в 14:19, , рубрики: android, android development, java, mobile development, theme, метки: , ,

Приветствую всех хабравчан!

Совсем недавно реализовывала интересную, на мой взгляд, задачу в андроид приложении и решила поделится опытом с вами.
Задача заключается в следующем: смена цветовой гаммы приложения по одному клику. Так называемая реализация скинов для приложения.

Задача

Cделать динамическими все вьюшки приложения, а так же в рантайме реализовать смену скинов.

Идея решения

При создании активити, система сначала парсит xml-разметку и постоянно дергает оверрайд метод getResources(), когда наталкивается на ссылку на ресурсы из разметки. Вот именно в это звено мы и можем вмешаться и подсовывать системе свои собственные ресурсы, а не те, которые она запрашивает. При этом, не заметив подвоха, система применит нужные ресурсы к вьюшкам. А это то, чего мы, собственно, и добивались.

Реализация идеи

Исходя из сказанного, нам нужно:

1) в разметке обязательно указывать ссылки на ресурсы (т.е. неявно). Ну понятно, всё через drawable указывать, а так же цвета

android:background="@color/color_tabbar_background"

2) создать базовый класс BasicActivity, который наследуется от Activity и от которого будут наследоваться все наши активити в приложении, переопределить в нем метод getResources().

3) Так же, нам понадобится класс, назовём его ResourseManager, который наследуется от Resourses.

Рассмотрим подробнее, что делает метод getResources() в активити. Вот что говорит нам официальный источник . Т.е. он возвращает объект типа Resources, значит нам нужно в нашем классе BasicActivity в переопределённом методе getResources() проверить, какая тема текущая и применена ли она вообще, если да, то создаём свой экземпляр класса ResourseManager. Вот пример кода, для ясности

public class BasicActivity extends Activity {
    private ResourceManager manager = null;
    @Override
    public Resources getResources() {
        if (App.checkCurrentSkin()) {
            if (manager == null) {
                manager = new ResourceManager(super.getResources());
            }
            return manager;
        } else
            return super.getResources();
    }
}

Это всё, что нужно сделать в базовом активити.
Теперь переходим к нашему ResourseManager. В нем нам обязательно нужен конструктор, а так же переопределённые методы getColor(int id) loadDrawable(TypedValue value, int id), getColorStateList(int id). С двумя последними методами у нас возникают небольшие проблемы — они пакетные. Это значит, что доступа к ним у нас нет. Но в таких случая к нам на помощь приходит JAVA Reflection! Как она работает, вы можете ознакомится сами, я рассказывать не буду, т.к. это не тема моей статьи. Просто приведу вам код для лучшего понимания

public class ResourceManager extends Resources {
    Skin mSkin = App.getCurrentSkin();
    Method loadDrawable;
    Method loadColorStateList;
Resources mBaseResources;

    public ResourceManager(Resources baseResources) {
        super(baseResources.getAssets(), baseResources.getDisplayMetrics(), baseResources.getConfiguration());
        mBaseResources = baseResources;        Method[] methods = Resources.class.getDeclaredMethods();
        for (Method method : methods) {
            if (method.getName().equals("loadDrawable")) {
                loadDrawable = method;
                loadDrawable.setAccessible(true);
            } else if (method.getName().equals("loadColorStateList")) {
                loadColorStateList = method;
                loadColorStateList.setAccessible(true);
            }
        }
    }

Т.о. мы разрешили использование нужных нам методов. Теперь дело осталось за малым, в этих методах определить нужный ресурс по его айдишнику и вернуть другой. В своём приложении вы как угодно можете хранить объект Skin, в котором описаны поля, соответствующие вашим динамическим вьюшкам и значению их цветов. Вот вам кусок реализации методов

@Override
    public int getColor(int id) throws NotFoundException {
        String color;
        switch (id) {
            case R.color.chat_in:
                color = mSkin.getControlWithType(SkinConstants.SKIN_CONTROL_RECEIVE_MESSAGE_BUBLE).getBackgroundColor();      // просто значание цвета #b7c0c7
                return Color.parseColor(color);
            case R.color.chat_send:
                color = mSkin.getControlWithType(SkinConstants.SKIN_CONTROL_SEND_MESSAGE_BUBLE).getBackgroundColor();
                return Color.parseColor(color);
….

Следующий метод

public Drawable loadDrawable(TypedValue value, int id) {
        Drawable d = null;
        String color;
        String colorSel;
        GradientDrawable result;
        switch (value.resourceId) {
            case R.color.color_background_main:
                color = mSkin.getControlWithType(SkinConstants.SKIN_CONTROL_VIEW).getBackgroundColor();
                return new ColorDrawable(Color.parseColor(color));

case R.drawable.button_blue_selector:
                color = mSkin.getControlWithType(SkinConstants.SKIN_CONTROL_BUTTON_BLUE).getBackgroundColor();
                colorSel = mSkin.getControlWithType(SkinConstants.SKIN_CONTROL_BUTTON_BLUE).getHighlightedBackgroundColor();
                return createSelector(Color.parseColor(color), getDarkerColor(color), colorSel);
case R.drawable.chat_in:
                color = mSkin.getControlWithType(SkinConstants.SKIN_CONTROL_RECEIVE_MESSAGE_BUBLE).getBackgroundColor();
                result = new GradientDrawable(Orientation.TOP_BOTTOM, new int[]{Color.parseColor(color),
                        Color.parseColor(color)});
                result.setCornerRadius(25);
                return result;
case R.drawable.list_view_selector:
                colorSel = mSkin.getControlWithType(SkinConstants.SKIN_CONTROL_TABLE_VIEW_CELL)
                        .getHighlightedBackgroundColor();
                ColorDrawable drawable = new ColorDrawable(Color.parseColor(colorSel));
                StateListDrawable listDrawable = new StateListDrawable();
                listDrawable.addState(new int[]{android.R.attr.state_pressed}, drawable);
                return listDrawable;
case R.drawable.button_last_menu_selector:
                color = mSkin.getControlWithType(SkinConstants.SKIN_CONTROL_TABLE_VIEW_CELL).getBackgroundColor();
                colorSel = mSkin.getControlWithType(SkinConstants.SKIN_CONTROL_TABLE_VIEW_CELL)
                        .getHighlightedBackgroundColor();
                return createSelectorForDifferentCorers(Color.parseColor(color), getDarkerColor(color), colorSel, 0, 0, 15,
                        15);
default:
                break;
        }

        try {
            d = (Drawable) loadDrawable.invoke(mBaseResources, value, id);
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        return d;
    }

И последний

@Override
    public ColorStateList getColorStateList(int id) throws NotFoundException {
        String color;
        String colorSel;
        ColorStateList res;
        switch (id) {
            case R.drawable.tab_text_selector:
                color = mSkin.getControlWithType(SkinConstants.SKIN_CONTROL_TABBAR_ITEM).getTextColor();
                colorSel = mSkin.getControlWithType(SkinConstants.SKIN_CONTROL_TABBAR_ITEM).getHighlightedTextColor();
                res = new ColorStateList(new int[][]{new int[]{android.R.attr.state_selected},
                        new int[]{-android.R.attr.state_selected}}, new int[]{Color.parseColor(colorSel),
                        Color.parseColor(color)});
                return res;
            default:
                break;
        }
        return super.getColorStateList(id);
    }

Из этого кода понятно, что мы программно создаём все drawabl`ы. К примеру

private Drawable createSelectorForDifferentCorers(int startColor, int endColor, String colorSel, float topLeft,
                                                      float topRight, float bottomRight, float bottomLeft) {
        StateListDrawable drawable = new StateListDrawable();
        GradientDrawable gradientDrawable1 = new GradientDrawable(Orientation.TOP_BOTTOM, new int[]{startColor,
                endColor});
        GradientDrawable gradientDrawable2 = new GradientDrawable(Orientation.TOP_BOTTOM, new int[]{
                Color.parseColor(colorSel), getDarkerColor(colorSel)});

        gradientDrawable1.setShape(GradientDrawable.RECTANGLE);
        gradientDrawable1.setCornerRadii(new float[]{topLeft, topLeft, topRight, topRight, bottomRight, bottomRight,
                bottomLeft, bottomLeft});
        gradientDrawable1.setStroke(1, endColor);

        gradientDrawable2.setShape(GradientDrawable.RECTANGLE);
        gradientDrawable2.setCornerRadii(new float[]{topLeft, topLeft, topRight, topRight, bottomRight, bottomRight,
                bottomLeft, bottomLeft});
        gradientDrawable2.setStroke(1, endColor);

        drawable.addState(new int[]{android.R.attr.state_pressed}, gradientDrawable2);
        drawable.addState(new int[]{android.R.attr.state_checked}, gradientDrawable2);
        drawable.addState(new int[]{-android.R.attr.state_selected}, gradientDrawable1);
        return drawable;
    }

В общем, огромный полёт для вашей фантазии.
Ещё хочу сразу обговорить один важный момент. Если у вас сложная картинка, которую программно не отрисуешь, например иконка для таба, то смену её цвета тоже можно организовать. Для этого нужно чтобы картинка для иконки была чисто белого цвета, так как мы будет смешивать её с нужным нам цветом путём перемножения значений цветов. Вот мы получаем нужный нам цвет картинки

Drawable example = ImageManager.blendColorDrawable(this, R.drawable.arrow, R.color.color_2_end); 

и можем делать с ней, всё, что угодно. Хоть ставить иконкой на таб.

public static Drawable blendColorDrawable(Context context, int baseId, int colorId) {

        Resources res = context.getResources();
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        Bitmap base = BitmapFactory.decodeResource(res, baseId, options);

        Bitmap blend = Bitmap.createBitmap(base.getWidth(), base.getHeight(), Config.ARGB_8888);
        blend.eraseColor(context.getResources().getColor(colorId));

        Bitmap result = base.copy(Config.ARGB_8888, true);
        Paint p = new Paint();
        p.setXfermode(new PorterDuffXfermode(Mode.MULTIPLY));
        p.setShader(new BitmapShader(blend, TileMode.CLAMP, TileMode.CLAMP));

        Canvas c = new Canvas();
        c.setBitmap(result);
        c.drawBitmap(base, 0, 0, null);
        c.drawRect(0, 0, base.getWidth(), base.getHeight(), p);

        return new BitmapDrawable(context.getResources(), result);

Т.к. я выбирала куски из большого проекта, где это всёго лишь одна из фич, то пример проекта не прилагаю. Если не разберётесь и очень будет нужно, могу накидать вам примерчик с исходным кодом. Пишите в комментариях!

P.S. Помните, чтобы изменения вошли в силу, нужно пересоздать активити

Автор: Juli_Incognito

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js