Implementing Night Mode for Sense on Android
Hello!
Sense is a companion app for Hello’s bedside sleep and room condition hardware products the Sense and Sleep Pill. One of the most requested user features has been Night Mode support, so users could easily navigate the app without straining their eyes against a harsh light theme. From initial design to production launch, this article hopes to illustrate the challenges and nuances we faced to make Night Mode a success so you can too!
When planning the night mode release for the Sense Android app, a quick search led to Chris Bane’s article. It provided us with a good starting point of how much needed to be changed to support night mode.
With a solid design spec established for day and night mode thanks to the incredible Nathan Hills, we could quickly build the complementary styles needed for our base UI components.
Night Mode Resources
An easy win with the Android resource management system was being able to make a night mode folder that would be used when night mode was active. For example, we were able to specify night mode colors simply by putting color values with identical names inside a resources folder called colors-night. The same was done for certain drawable assets by making folders such as drawable-night-xxhdpi. A drawback of this approach is 2x increasing the app size for each original asset. Some of this duplication was avoided by updating original assets to use transparent backgrounds.
While most large assets such as those could reliably be rendered from the appropriate folder, icons would be tinted the appropriate color based on the theme mode colors. This was done so it would be easy to switch colors of icons in the future. Fortunately because we updated all our activities to extend AppCompat we could rely on any resource used inside an ImageView to be properly tinted through all our supported sdk versions with just the tint xml field. One caveat to this approach is that ColorStateList resources will not work on pre-Lollipop devices when using the tint xml field.
However, because the minSdk we support is Kitkat (19), many of the ways to tint drawables relied heavily on DrawableCompat. Any reference to ColorRes inside a ShapeDrawable pre Lollipop (21) would not be updated after a theme change, so certain components of the app that used ShapeDrawables as background resources would appear in the wrong theme. Here is a snippet of a utility method to achieve proper tinting:
The mutate() method is required because if the drawable resource is reused on a different screen, any changes applied such as tint should not affect other usages of the same drawable.
Setting the ColorStateList for the drawable is useful if you want to handle states like pressed or disabled and is recommended to make the interactions more tactile and tangible.
Update 5/6/2017: Drawable Resource Image Caching
Another gotcha we found when using our configuration of the image fetching library Picasso was that cached drawable resources from before a theme change would be rendered.
Example:
The default profile image resource was cached for the day theme. Consequently, the same day theme default profile image was used instead of fetching the appropriate night theme resource when expected.
To prevent the cache from incorrectly fetching images, it is recommended to explicitly clear the cache when a theme change is detected. This can be done on a per request level or with the entire cache that Picasso uses.
Picasso picasso = new Picasso.Builder(context)
.memoryCache(cache)
.build();
# Clear entire cache
cache.clear();# Per request basis
picasso.load(R.drawable.default_profile_image)
.memoryPolicy(MemoryPolicy.NO_CACHE)
.into(imageView);
Scheduled Night Mode
Currently the most popular mode among both Android and iOS users is the scheduled night mode feature which allows the theme to change with the sunset and sunrise.
In order to enable a scheduled night mode several checkpoints must pass:
- User must grant runtime location permission to the app. (sdk 23+)
- Device location service settings are enabled and correct location mode is selected.
- A current location can be determined and stored for reference to calculate sunset and sunrise period.
The first step was easy to handle with an initial custom dialog triggered by selecting a text link. We want to provide the user context into why we require their location as well as provide a fallback to direct users if they deny our initial request.
The second step is needed for more accurate scheduled theme transitions to avoid defaulting to hardcoded values if no location could be determined. We use Google Play Service’s Location API to make a location settings request which can resolve a common edge case where the device’s location services are off or using the wrong mode. The best thing about handling a location request through their API is allowing a user to stay in the app to update the device’s location service settings.
Note: A small snag implementing scheduled night mode was found prior to the first beta launch as a result of requesting LocationRequest.PRIORITY_HIGH_ACCURACY when no location is initially known by the app to quickly return a result. When publishing to devices with api 21+ in the Play Store, if no explicit uses-feature statement is made all devices without gps hardware will not be able to install the app. So to avoid this, please make sure that gps hardware is not required.
The original plan for scheduled night mode was to use Google’s implementation, to avoid step 3 entirely and keep the business logic of when to exactly to switch themes out of the Sense app. However, we found in the middle of the process that AppCompatDelegate.MODE_NIGHT_AUTO was unreliable for our purposes in its current version (25.0.1).
The issues with the auto mode provided by Google for us could be broken down to the following:
- no way to be notified of an incoming scheduled theme change for existing activities.
- no way to listen for location updates if failed to fetch last known location despite granting location runtime permission and enabling device location services.
- not documented when the location used to calculate the sunset sunrise times would be updated to provide accurate scheduled theme changes.
However it was not too much effort to expand our usage of the Google Play Location Services API to also fetch for the last known location or request location updates if the former returned empty. Once we obtain a valid Location object we wrap only the Lat Long attributes in a serializable UserLocation object (as well as the current night mode selected) for persistent storage based on internal account id, so that when different users log out and log in their individual preferences are respected.
Listening for scheduled theme changes was done through the help of RxJava Subjects. Any existing activity subscribed to a night mode Subject would be notified when a theme change occurred and recreate itself appropriately.
Finishing Touches
To tie things together nicely when implementing night mode on Android, we needed to achieve a buttery smooth transition when a user changes modes.
Because we only wanted to run window fade enter/exit animations when a theme change occurred instead of specifying the window animations directly in the activity theme, we overrode Activity’s recreate() method:
Lastly, when returning to the home screen from night mode settings we needed to recreate the task stack so all existing activities in the current task can be refreshed with the correct theme. This is an expensive operation, so we only act upon this if an actual theme change occurred when returning from night mode settings. If a user just toggles between always on and off and no theme change is required we avoid recreating the task stack.
Thanks for reading, and I hope that you’ve learned a thing or two!