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