Überschreiben von Ressourcen zur Laufzeit

Lesezeit: 8 Minuten

Benutzer-Avatar
Luke Sleemann

Das Problem

Ich möchte meine App-Ressourcen wie R.colour.brand_colour oder R.drawable.ic_action_start zur Laufzeit überschreiben können. Meine Anwendung stellt eine Verbindung zu einem CMS-System her, das Branding-Farben und -Bilder bereitstellt. Nachdem die App die CMS-Daten heruntergeladen hat, muss sie in der Lage sein, sich selbst neu zu erstellen.

Ich weiß, was Sie sagen wollen – das Überschreiben von Ressourcen zur Laufzeit ist nicht möglich.

Außer, dass es irgendwie so ist. Insbesondere habe ich dies gefunden Bachelorarbeit aus dem Jahr 2012, das das Grundkonzept erklärt – Die Aktivitätsklasse in Android erweitert ContextWrapper, die die Methode „attachBaseContext“ enthält. Sie können „attachBaseContext“ überschreiben, um den „Context“ mit Ihrer eigenen benutzerdefinierten Klasse zu umschließen, die Methoden wie „getColor“ und „getDrawable“ überschreibt. Ihre eigene Implementierung von getColor könnte die Farbe beliebig nachschlagen. Das Bibliothek für Kalligrafie verwendet einen ähnlichen Ansatz, um einen benutzerdefinierten LayoutInflator einzufügen, der mit dem Laden benutzerdefinierter Schriftarten umgehen kann.

Der Code

Ich habe eine einfache Aktivität erstellt, die diesen Ansatz verwendet, um das Laden einer Farbe zu überschreiben.

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;
        }
    }
}

Das Problem ist, es funktioniert nicht! Die Protokollierung zeigt Aufrufe zum Laden von Ressourcen wie layout/activity_main und mipmap/ic_launcher, aber color/theme_colour wird nie geladen. Es scheint, dass der Kontext verwendet wird, um das Fenster und die Aktionsleiste zu erstellen, aber nicht die Inhaltsansicht der Aktivität.

Meine Fragen sind – Woher lädt der Layout-Inflator Ressourcen, wenn nicht aus dem Aktivitätskontext? Ich würde auch gerne wissen – Gibt es eine praktikable Möglichkeit, das Laden von Farben und Drawables zur Laufzeit zu überschreiben?

Ein Wort zu alternativen Ansätzen

Ich weiß, dass es möglich ist, eine App aus CMS-Daten auf andere Weise zu thematisieren – zum Beispiel könnten wir eine Methode erstellen getCMSColour(String key) dann in unserem onCreate() Wir haben eine Menge Code in der Art von:

myTextView.setTextColour(getCMSColour("heading_text_colour"))

Ein ähnlicher Ansatz könnte für Drawables, Strings usw. gewählt werden. Dies würde jedoch zu einer großen Menge an Boilerplate-Code führen, der alle gewartet werden muss. Beim Ändern der Benutzeroberfläche würde man leicht vergessen, die Farbe für eine bestimmte Ansicht festzulegen.

Das Umschließen des Kontexts, um unsere eigenen benutzerdefinierten Werte zurückzugeben, ist „sauberer“ und weniger anfällig für Fehler. Ich würde gerne verstehen, warum es nicht funktioniert, bevor ich alternative Ansätze erkunde.

  • Ihre Lösung funktioniert: Wenn Sie in der Aktivität getResources().getColor(R.color.theme_colour) aufrufen, ist das Ergebnis wie erwartet Color.GREEN. Der Inflater scheint andere Methoden zum Abrufen von Farben zu verwenden, ich weiß nicht, welche davon. Ich habe versucht, den Anwendungskontext zu umschließen, aber wir haben das gleiche Ergebnis …

    – Médéric

    29. Mai 2015 um 7:08 Uhr

  • Ja, ich weiß, dass der Aufruf von getResource().getColour() grün zurückgibt. Meine Frage ist jedoch, wenn das Layout aufgeblasen ist, warum sind die Steuerelemente, die ich android:colour=”@color/theme_colour” eingestellt habe, nicht grün!

    – Luke Sleemann

    31. Mai 2015 um 0:58 Uhr

  • Keine Antwort auf Ihre Frage (und ich wäre in der Tat sehr interessiert, wenn dies möglich wäre), aber als weiterer alternativer Ansatz könnten Sie die Widgets selbst überschreiben (TextView, ImageView usw.), die Ihre eigene Implementierung von “Ressourcenanbieter” verwenden. (den Sie in Ihrem Absatz “alternativer Ansatz” hinzugefügt und in Ihren Ansichten verwendet haben. Auf diese Weise reduzieren Sie die Menge an Boilerplate-Code und es ist auch viel einfacher, Designs zu pflegen. Zumindest ist das der Ansatz, den ich persönlich wählen würde wenn alles andere schlug fehl, anstatt die Themen und Ressourcen in jeder Aktivität/jedem Fragment zu überschreiben.

    – kha

    5. Juni 2015 um 9:48 Uhr

  • @kha hat hier einen vernünftigen Ansatz. Abgesehen davon gibt es keine zuverlässige Möglichkeit, Werte, auf die von XML verwiesen wird, dynamisch durch beliebige Daten zu ersetzen. Jeder Ansatz, der Reflexion beinhaltet Wille Ihre App kaputt machen (und wir sehen viele davon aufgrund von Ressourcen-Framework-Änderungen in M).

    – alanv

    5. Juni 2015 um 20:26 Uhr

  • Ah ich sehe. Ich entschuldige mich für meine vorherige Verwirrung. In einer idealen Welt würde Ihr Ansatz funktionieren. In einer idealen Welt hätte ich Haare. Es ist keine heile Welt, mehr noch.

    – CommonsWare

    9. Juni 2015 um 15:22 Uhr

Benutzer-Avatar
Login

Während “dynamisches Überschreiben von Ressourcen” die einfache Lösung für Ihr Problem zu sein scheint, wäre meiner Meinung nach ein saubererer Ansatz die Verwendung der offiziellen Datenbindungsimplementierung https://developer.android.com/tools/data-binding/guide.html da es nicht impliziert hacken der Android-Weg.

Sie könnten Ihre Branding-Einstellungen mit einem POJO übergeben. Anstatt statische Stile wie @color/button_color du könntest schreiben @{brandingConfig.buttonColor} und binden Sie Ihre Ansichten mit den gewünschten Werten. Mit einer richtigen Aktivitätshierarchie sollte es nicht zu viele Boilerplates hinzufügen.

Dies gibt Ihnen auch die Möglichkeit, komplexere Elemente in Ihrem Layout zu ändern, dh je nach Branding-Einstellungen unterschiedliche Layouts in andere Layouts aufzunehmen, wodurch Ihre Benutzeroberfläche ohne allzu großen Aufwand hochgradig konfigurierbar wird.

  • Ich habe gerade mit der Datenbindungsbibliothek gespielt und sie scheint für das, was wir brauchen, gut zu funktionieren. Das einzige, was fehlt, sind schöne Designvorschauen der datengebundenen Branding-Konfiguration – z. B. so etwas wie android:textColor="@{brandingConfig.buttonColor}" führt zu nichts Interessantem in der visuellen Layoutvorschau in Android Studio. Dies kann immer umgangen werden, indem a hinzugefügt wird tools: Präfix-Attribut, um Dinge wie Strings, Drawables usw. für die Vorschau festzulegen

    – Luke Sleemann

    10. Juni 2015 um 15:05 Uhr

  • Nach einigem Nachdenken markiere ich dies als Antwort. Sie haben die Frage “Woher lädt der Layout-Inflator Ressourcen, wenn nicht aus dem Aktivitätskontext?” nicht beantwortet. Sie haben jedoch erfolgreich die Frage beantwortet: “Gibt es eine praktikable Möglichkeit, das Laden von Farben und Drawables zur Laufzeit zu überschreiben?” mit einem System, das dem nahe kommt, was ich erreichen wollte. Ein großes Lob an Sie, dass Sie erkannt haben, dass es sich bei dem Problem um die Datenbindung handelte. Im Wesentlichen habe ich versucht, viele Werte aus Java in das XML-Layout mit der minimalen Boilerplate zu bekommen.

    – Luke Sleemann

    11. Juni 2015 um 1:20 Uhr

Da ich im Grunde das gleiche Problem wie Luke Sleeman hatte, habe ich mir angesehen, wie die LayoutInflater erstellt die Ansichten beim Analysieren der XML-Layoutdateien. Ich habe mich darauf konzentriert, zu überprüfen, warum die dem Textattribut von zugewiesenen String-Ressourcen TextViews innerhalb des Layouts werden von my nicht überschrieben Resources Objekt, das von einem Benutzer zurückgegeben wird ContextWrapper. Gleichzeitig werden die Strings wie erwartet überschrieben, wenn der Text oder Hinweis programmatisch durchgesetzt wird TextView.setText() oder TextView.setHint().

So wird der Text als empfangen CharSequence innerhalb des Konstruktors der TextView (SDK-Version 23.0.1):

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

wo a ist ein TypedArray früher erhalten:

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

Das Theme.obtainStyledAttributes() -Methode ruft eine native Methode auf der auf 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);

...

Und das ist die Erklärung des AssetManager.applyStyle() Methode:

// 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);

Abschließend, obwohl die LayoutInflater verwendet den korrekten erweiterten Kontext beim Aufblasen der XML-Layouts und beim Erstellen der Ansichten, der Methoden Resources.getText() (auf den vom Benutzer zurückgegebenen Ressourcen ContextWrapper) werden nie aufgerufen, um die Zeichenfolgen für das Textattribut abzurufen, da der Konstruktor der TextView verwendet die AssetManager direkt, um die Ressourcen für die Attribute zu laden. Dasselbe gilt möglicherweise für andere Ansichten und Attribute.

  • Ahhh – danke, dass Sie das letzte Puzzleteil herausgefunden haben – woher der Layout-Inflator Ressourcen lädt! Es ist bedauerlich zu sehen, dass der Ansatz, den Kontext zu verpacken, um dynamische Themen zu implementieren, niemals funktionieren wird.

    – Luke Sleemann

    15. November 2015 um 23:03 Uhr

  • Dies scheint die native applyStyle-Implementierung zu sein: github.com/aosp-mirror/platform_frameworks_base/blob/…

    – tmm1

    12. März 2021 um 0:34 Uhr

Nach langem Suchen habe ich endlich eine hervorragende Lösung gefunden.

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);
        }
    }

Für einen Probetest,

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

Und innerhalb der Hauptaktivität,

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()));

Diese Lösung wurde ursprünglich gepostet von, Roman Zhilich

ResourceHackActivity

  • Tatsächlich ist es möglich, Reflektion zu verwenden, um Felder auf dem R-Objekt auf neue Ressourcen verweisen zu lassen – das Problem ist, dass die neuen Ressourcen auch statisch in XML definiert werden müssen! Wir interessieren uns für Farben, Strings, Drawables usw., die dynamisch aus einem CMS kommen. Mit Ihrer Lösung können Sie also ganz einfach R.string erstellen. initial_value = R.string. evil_value, aber Sie können keine dynamische Zeichenfolge von woanders einfügen.

    – Luke Sleemann

    4. Juni 2015 um 22:56 Uhr

1082630cookie-checkÜberschreiben von Ressourcen zur Laufzeit

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy