Подтвердить что ты не робот

Переопределение ресурсов во время выполнения

Проблема

Я хотел бы иметь возможность переопределять ресурсы моих приложений, такие как R.colour.brand_colour или R.drawable.ic_action_start во время выполнения. Мое приложение подключается к системе CMS, которая будет обеспечивать цвета и изображения брендинга. После того, как приложение загрузило данные CMS, оно должно быть в состоянии перекрасить себя.

Я знаю, что вы собираетесь сказать - переопределение ресурсов во время выполнения невозможно.

Кроме того, что это не так. В частности, я нашел эту Бакалаврскую диссертацию с 2012 года, которая объясняет основную концепцию - класс Activity в android extends ContextWrapper, который содержит метод attachBaseContext. Вы можете переопределить attachBaseContext, чтобы обернуть контекст своим собственным пользовательским классом, который переопределяет такие методы, как getColor и getDrawable. Ваша собственная реализация getColor может выглядеть так, как хотелось бы. Библиотека Каллиграфия использует аналогичный подход для ввода пользовательского LayoutInflator, который может работать с загрузкой пользовательских шрифтов.

Код

Я создал простую операцию, которая использует этот подход для переопределения загрузки цвета.

public class MainActivity extends Activity {

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

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(new CmsThemeContextWrapper(newBase));
    }

    private class CmsThemeContextWrapper extends ContextWrapper{

        private Resources resources;

        public CmsThemeContextWrapper(Context base) {
            super(base);
            resources = new Resources(base.getAssets(), base.getResources().getDisplayMetrics(), base.getResources().getConfiguration()){
                @Override
                public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException {
                    Log.i("ThemeTest", "Getting value for resource " + getResourceName(id));
                    super.getValue(id, outValue, resolveRefs);
                    if(id == R.color.theme_colour){
                        outValue.data = Color.GREEN;
                    }
                }

                @Override
                public int getColor(int id) throws NotFoundException {
                    Log.i("ThemeTest", "Getting colour for resource " + getResourceName(id));
                    if(id == R.color.theme_colour){
                        return Color.GREEN;
                    }
                    else{
                        return super.getColor(id);
                    }
                }
            };
        }

        @Override
        public Resources getResources() {
            return resources;
        }
    }
}

Проблема в том, что она не работает! Журналы показывают вызовы для загрузки ресурсов, таких как layout/activity_main и mipmap/ic_launcher, однако цвет/тема_цвет никогда не загружается. Кажется, что контекст используется для создания окна и панели действий, но не для представления содержимого активности.

Мои вопросы - Где загрузочный ресурс компоновщика компоновки, если не контекст действий? Я также хотел бы знать - Существует ли работоспособный способ переопределить загрузку цветов и чертежи во время выполнения?

Слово об альтернативных подходах

Я знаю, что его можно использовать для приложения из данных CMS другими способами - например, мы могли бы создать метод getCMSColour(String key), тогда внутри нашего onCreate() у нас есть куча кода по строкам:

myTextView.setTextColour(getCMSColour("heading_text_colour"))

Аналогичный подход можно было бы использовать для чертежей, строк и т.д. Однако это привело бы к большому количеству кода шаблона, все из которых нуждается в поддержании. При изменении пользовательского интерфейса было бы легко забыть установить цвет в определенном виде.

Обтекание контекста для возврата наших собственных значений является "более чистым" и менее подверженным поломке. Я хотел бы понять, почему он не работает, прежде чем исследовать альтернативные подходы.

4b9b3361

Ответ 1

В то время как "динамически переопределяющие ресурсы" могут показаться простым решением вашей проблемы, я считаю, что более чистым подходом было бы использовать официальную реализацию привязки данных https://developer.android.com/tools/data-binding/guide.html, поскольку это не подразумевает взлом андроида.

Вы можете передать настройки своего бренда с помощью POJO. Вместо использования статических стилей, таких как @color/button_color, вы можете написать @{brandingConfig.buttonColor} и привязать свои представления к нужным значениям. При правильной иерархии действий он не должен добавлять слишком много шаблонов.

Это также дает вам возможность изменять более сложные элементы на вашем макете, в том числе различные макеты на другом макете в зависимости от настроек брендинга, что делает ваш пользовательский интерфейс очень настраиваемым без особых усилий.

Ответ 2

После долгого поиска я наконец нашел отличное решение.

protected void redefineStringResourceId(final String resourceName, final int newId) {
        try {
            final Field field = R.string.class.getDeclaredField(resourceName);
            field.setAccessible(true);
            field.set(null, newId);
        } catch (Exception e) {
            Log.e(getClass().getName(), "Couldn't redefine resource id", e);
        }
    }

Для образца теста

private Object initialStringValue() {
                // TODO Auto-generated method stub
                 return getString(R.string.initial_value);
            }

И внутри основного действия

before.setText(getString(R.string.before, initialStringValue()));

            final String resourceName = getResources().getResourceEntryName(R.string.initial_value);
            redefineStringResourceId(resourceName, R.string.evil_value);

            after.setText(getString(R.string.after, initialStringValue()));

Это решение было первоначально опубликовано, Роман Жилич

ResourceHackActivity

Ответ 3

Имея в основном ту же проблему, что и Люк Слеман, я посмотрел, как LayoutInflater создает представления при анализе файлов макета XML. Я сосредоточился на проверке того, почему строковые ресурсы, назначенные текстовому атрибуту TextView внутри макета, не перезаписываются моим объектом Resources, возвращенным пользовательским ContextWrapper. В то же время строки переписываются, как и ожидалось, при программировании текста или подсказки с помощью TextView.setText() или TextView.setHint().

Вот как текст получен как CharSequence внутри конструктора TextView (sdk v 23.0.1):

// android.widget.TextView.java, line 973
text = a.getText(attr);

где a является TypedArray, полученным ранее:

 // android.widget.TextView.java, line 721
 a = theme.obtainStyledAttributes(attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);

Метод Theme.obtainStyledAttributes() вызывает собственный метод на AssetManager:

// android.content.res.Resources.java line 1593
public TypedArray obtainStyledAttributes(AttributeSet set,
            @StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
...
        AssetManager.applyStyle(mTheme, defStyleAttr, defStyleRes,
                parser != null ? parser.mParseState : 0, attrs, array.mData, array.mIndices);

...

И это объявление метода AssetManager.applyStyle():

// android.content.res.AssetManager.java, line 746
/*package*/ native static final boolean applyStyle(long theme,
        int defStyleAttr, int defStyleRes, long xmlParser,
        int[] inAttrs, int[] outValues, int[] outIndices);


В заключение, несмотря на то, что LayoutInflater использует правильный расширенный контекст, при раздувании XML-макетов и создании представлений методы Resources.getText() (на ресурсах, возвращаемых пользовательским ContextWrapper), никогда не вызываются, чтобы получить строки для текстового атрибута, потому что конструктор TextView использует AssetManager непосредственно для загрузки ресурсов для атрибутов. То же самое может быть применимо и для других видов и атрибутов.