Implementing User Choice of Theme

Implementing user choice of theme

I actually have this feature in my application and additionally, I allow users to change theme at runtime. As reading a value from preferences takes some time, I'm getting a theme id via globally accessible function which holds cached value.

As already pointed out - create some Android themes, using this guide. You will have at least two <style> items in your styles.xml file. For example:

<style name="Theme.App.Light" parent="@style/Theme.Light">...</style>
<style name="Theme.App.Dark" parent="@style/Theme">...</style>

Now, you have to apply one of these styles to your activities. I'm doing this in activitie's onCreate method, before any other call:

setTheme(MyApplication.getThemeId());

getThemeId is a method which returns cached theme ID:

public static int getThemeId()
{
return themeId;
}

This field is being updated by another method:

public static void reloadTheme()
{
themeSetting = PreferenceManager.getDefaultSharedPreferences(context).getString("defaultTheme", "0");
if(themeSetting.equals("0"))
themeId = R.style.Theme_Light;
else
themeId = R.style.Theme_Dark;
}

Which is being called whenever preferences are changed (and, on startup of course). These two methods reside in MyApplication class, which extends Application. The preference change listener is described at the end of this post and resides in main activity class.

The last and pretty important thing - theme is applied, when an activity starts. Assuming, you can change a theme only in preference screen and that there's only one way of getting there, i.e. from only one (main) activity, this activity won't be restarted when you will exit preference screen - the old theme still will be used. Here's the fix for that (restarts your main activity):

@Override
protected void onResume() {
super.onResume();
if(schduledRestart)
{
schduledRestart = false;
Intent i = getBaseContext().getPackageManager().getLaunchIntentForPackage( getBaseContext().getPackageName() );
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(i);
}
}

scheduledRestart is a boolean variable, initially set to false. It's set to true when theme is changed by this listener, which also updates cached theme ID mentioned before:

private class ThemeListener implements OnSharedPreferenceChangeListener{

@Override
public void onSharedPreferenceChanged(SharedPreferences spref, String key) {
if(key.equals("defaultTheme") && !spref.getString(key, "0").equals(MyApplication.getThemeSetting()))
{
MyApplication.reloadTheme();
schduledRestart = true;
}
}

sp = PreferenceManager.getDefaultSharedPreferences(this);

listener = new ThemeListener();
sp.registerOnSharedPreferenceChangeListener(listener);

Remember to hold a reference to the listener object, otherwise it will be garbage colleted (and will cease to work).

What's a good technique for implementing dynamic theming in CSS?

It's actually easier to do the color change in javascript at the same time you are handling the color selection. Use a single set of css classes for the colors, eg textcol and linkcol and apply them with javascript. You need only add 2 lines of css for this, and no jquery or less dependancies.

a, .linkcol {text-decoration:none;}

.big { font-size:1.2em;}
<!DOCTYPE html>

<html>

<body>

<h1>Hello world</h1>

<p>Click the button to change the theme.</p>

<button onclick="colFunction('#808000','#000000')">Green</button>

<button onclick="colFunction('#800000','#666666')">Red</button>

<p id="demo">This <a class="linkcol" href="#">link</a> and <span class="big linkcol">these big words</span> which use multiple classes should be black except for red theme, which makes them gray</p>

<script>

function colFunction(textcol, linkcol) {

document.getElementsByTagName("body")[0].style.color = textcol;

for (i=0;i <= document.getElementsByClassName("linkcol").length;i++) {

document.getElementsByClassName("linkcol")[i].style.color = linkcol;

}

}

</script>

</body>

</html>

How to change current Theme at runtime in Android

I would like to see the method too, where you set once for all your activities. But as far I know you have to set in each activity before showing any views.

For reference check this:

http://www.anddev.org/applying_a_theme_to_your_application-t817.html

Edit (copied from that forum):

    protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Call setTheme before creation of any(!) View.
setTheme(android.R.style.Theme_Dark);

// ...
setContentView(R.layout.main);
}


Edit

If you call setTheme after super.onCreate(savedInstanceState); your activity recreated but if you call setTheme before super.onCreate(savedInstanceState); your theme will set and activity
does not recreate anymore

  protected void onCreate(Bundle savedInstanceState) {
setTheme(android.R.style.Theme_Dark);
super.onCreate(savedInstanceState);

// ...
setContentView(R.layout.main);
}

Implementing Themes for web application

One solution you can do is to use a CSS preprocessor like SASS. Create you stylesheets as normal but instead of writing colours directly create variables for them in separate theme .scss files to be used as a reference.

Example in a light_theme.scss file:

$page_background_color: #f7f7f7;

In a dark_theme.scss file:

$page_background_color: #333;

Then your main .scss file:

body {
background-color: $page_background_color;
}

So the result of this when compiled with SASS would be two .css stylesheets. Include them both in your each with a title attribute. Add the meta tag of Default-style then using JavaScript set that meta tag to be the correct stylesheet. This enables you to dynamically change which stylesheet your page uses.

Depending on your usecase, you could use localstorage to save the users choice of stylesheet and check for that upload page load.

I have a Gist of the above - https://gist.github.com/jmwhittaker/4540000

Hopefully this will point you in the right direction.

How to remember the color scheme once its set by user? (Simple Color Scheme)

Try this simple resolve: to save, load and select a theme from local storage.
Local Storage doesn't working in snippets or sandboxes.

The localStorage read-only property of the window interface allows you to access a Storage object for the Document's origin; the stored data is saved across browser sessions. MDN documentation

JS

// Select class name as in CSS file
const CLASS_NAME = 'chosentheme';

const scheme = document.getElementById('scheme');
// Creating an array of SVG elements
const svgElementsArray = [...scheme.querySelectorAll('svg')];
// Creating a color theme array using the SVG ID attribute
const themeNameArray = svgElementsArray.map(theme => theme.id);
// Get html node (html tag)
const htmlNode = document.documentElement;
// Get color (value) from local storage
const getLocalStorageTheme = localStorage.getItem('theme');

const setTheme = theme => {
// Set class to html node
htmlNode.className = theme;
// Set theme color to local storage
localStorage.setItem('theme', theme);

svgElementsArray.forEach(svg => {
// If we click on the svg and it has a class, do nothing
if (svg.id === theme && svg.classList.contains(CLASS_NAME)) return;
// Check, if svg has the same ID and if it doesn't have a class,
// then we adding class and removing from another svg
if (svg.id === theme && !svg.classList.contains(CLASS_NAME)) {
svg.classList.add(CLASS_NAME);
} else {
svg.classList.remove(CLASS_NAME);
}
});
};

// Find current theme color (value) from array
const findThemeName = themeNameArray.find(theme => theme === getLocalStorageTheme);

// If local storage empty
if (getLocalStorageTheme) {
// Set loaded theme
setTheme(findThemeName);
} else {
// Find last svg and set the class (focus)
svgElementsArray.at(-1).classList.add(CLASS_NAME);
}

document.getElementById('scheme').addEventListener('click', ({ target }) => {
// Getting ID from an attribute
const id = target.getAttribute('id');
// Find current theme color (value) from array
const findThemeName = themeNameArray.find(theme => theme === id);
setTheme(findThemeName);
});

We also need to prevent selection of child elements inside the button (SVG) and to select exactly the button with ID attribute.

CSS

theme svg > * {
pointer-events: none;
}
theme svg {
/* to prevent small shifts,
when adding the chosentheme class */
border: 1px solid transparent;
}
theme svg.chosentheme {
border-color: black;
}

To prevent the webpage from flickering (blinking) while is loading, place this snippent at the top of the head tag. (prevent dark themes from flickering on load)

HTML

<head>
<script>
function getUserPreference() {
if(window.localStorage.getItem('theme')) {
return window.localStorage.getItem('theme')
}
}
document.documentElement.dataset.theme = getUserPreference();
</script>
....
</head>

// Select class name as in CSS file
const CLASS_NAME = 'chosentheme';

const scheme = document.getElementById('scheme');
// Creating an array of SVG elements
const svgElementsArray = [...scheme.querySelectorAll('svg')];
// Creating a color theme array using the SVG ID attribute
const themeNameArray = svgElementsArray.map(theme => theme.id);
// Get html node (html tag)
const htmlNode = document.documentElement;
// Get color (value) from local storage
const getLocalStorageTheme = localStorage.getItem('theme');

const setTheme = theme => {
// Set class to html node
htmlNode.className = theme;
// Set theme color to local storage
localStorage.setItem('theme', theme);

svgElementsArray.forEach(svg => {
// If we click on the svg and it has a class, do nothing
if (svg.id === theme && svg.classList.contains(CLASS_NAME)) return;
// Check, if svg has the same ID and if it doesn't have a class,
// then we adding class and removing from another svg
if (svg.id === theme && !svg.classList.contains(CLASS_NAME)) {
svg.classList.add(CLASS_NAME);
} else {
svg.classList.remove(CLASS_NAME);
}
});
};

// Find current theme color (value) from array
const findThemeName = themeNameArray.find(theme => theme === getLocalStorageTheme);

// If local storage empty
if (getLocalStorageTheme) {
// Set loaded theme
setTheme(findThemeName);
} else {
// Find last svg and set the class (focus)
svgElementsArray.at(-1).classList.add(CLASS_NAME);
}

document.getElementById('scheme').addEventListener('click', ({
target
}) => {
// Getting ID from an attribute
const id = target.getAttribute('id');
// Find current theme color (value) from array
const findThemeName = themeNameArray.find(theme => theme === id);
setTheme(findThemeName);
});
:root {
/* Default Theme, if no theme is manually selected by user */
--bgr: #eee;
--txt: #000;
--flt: none;
}

:root.blackwhite {
--bgr: #fff;
--txt: #000;
--flt: contrast(100%) grayscale(100%);
}

:root.midnight {
--bgr: lightblue;
--txt: red;
--flt: sepia(75%);
}

:root.beach {
--bgr: #fba;
--txt: #269;
--flt: blur(0.25px) saturate(4);
}

/* :root.moody {
--bgr: green;
--txt: yellow;
--flt: drop-shadow(16px 16px 20px yellow) blur(1px);
} */

html {
/* Have something to test */
background: var(--bgr);
color: var(--txt);
filter: var(--flt);
/* important filter that affects everything */
}

h1 {
background: var(--bgr);
color: var(--txt);
}

theme {
/* html element div that contains only SVG graphics */
display: flex;
flex-direction: row;
}

theme svg > * {
/* prevent selection */
pointer-events: none;
}

theme svg {
/* to prevent small shifts,
when adding a focus class */
border: 1px solid transparent;
}

theme svg.chosentheme {
border-color: black;
}
<theme id="scheme">
<svg id="blackwhite"><rect /></svg>
<svg id="midnight"><rect /></svg>
<svg id="beach"><rect /></svg>
<svg id="defaulttheme"><rect /></svg>
</theme>

<h1>Click on a theme to change the color scheme!</h1>
<p>Some Paragraph texts.</p>

Implementing a Custom Theme

You can write a custom button view. Example:

public final class ThemedButton extends Button { } and in the constructor, you can init the view as you want. In the constructor you have the context, so get the preference and set the background color of your themed button. This way you avoid to dirty your activity.

Hope this helps :)



Related Topics



Leave a reply



Submit