There are many ways to implement the night mode, and after many attempts, a cost-effective one has been found.
Theme way
This is the most orthodox approach, but it’s a lot of work because you’re globally replacing all hard-coded color values in the XML layout with theme colors. Then change the theme to achieve the effect of skin.
The window way
Is it possible to cover all screens with a translucent window, like wearing sunglasses to look at the screen? Although this is the “next best thing” of the skin scheme, it can also achieve a non-dazzling effect:
open class BaseActivity : AppCompatActivity() {
// Display global translucent floating window
private fun showMaskWindow(a) {
// Float window contents
val view = View {
layout_width = match_parent
layout_height = match_parent
background_color = "#c8000000"
}
val windowInfo = FloatWindow.WindowInfo(view).apply {
width = DimensionUtil.getScreenWidth(this@BaseActivity)
height = DimensionUtil.getScreenHeight(this@BaseActivity)}// Display floating window
FloatWindow.show(this."mask", windowInfo, 0.100.false.false.true)}}Copy the code
View{}, which is a DSL for building a layout, instantiates a translucent View, which can be explained here.
Where FloatWindow is the FloatWindow management class, the show() method adds a global Window to the interface.
- In order to make the floating window span
Activity
Display, need to put the windowtype
Set toWindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
. - To allow touch events to pass through the float window to
Activity
, you need to add the following for the windowflag
.FLAG_NOT_FOCUSABLE
,FLAG_NOT_TOUCHABLE
,FLAG_NOT_TOUCH_MODAL
,FLAG_FULLSCREEN
.
These details are encapsulated in FloatWindow.
A drawback of this scheme is that the global floating window will disappear when the system is shown multitasking, with the following effect:
Subview mode
Is it possible to add a translucent View to each current screen as a mask?
fun Activity.nightMode(lightOff: Boolean, color: String) {
// Build the main thread message handler
val handler = Handler(Looper.getMainLooper())
// Mask control ID
val id = "darkMask"
// Enable night mode
if (lightOff) {
// Insert the show mask task to the header of the main thread message queue
handler.postAtFrontOfQueue {
// Build the mask view
val maskView = View {
layout_id = id
layout_width = match_parent
layout_height = match_parent
background_color = color
}
// Add a mask view to the top view of the current interfacedecorView? .apply {val view = findViewById<View>(id.toLayoutId())
if (view == null) { addView(maskView) }
}
}
}
// Turn off night mode
else {
// Remove the mask view from the top view of the current interfacedecorView? .apply { find<View>(id)? .let { removeView(it) } } } }Copy the code
This extends a method for AppCompatActivity that can be used to switch on or off the night mode. Turn on the night mode by adding a mask view to the top view of the current interface.
- Among them
decorView
isActivity
An extended property of:
val Activity.decorView: FrameLayout?
get() = (takeIf { ! isFinishing && ! isDestroyed }? .window? .decorView)as? FrameLayout
Copy the code
Get the DecorView from its Window while the Activity is still displayed.
- Among them
toLayoutId()
isString
Extension method:
fun String.toLayoutId(a): Int {
var id = java.lang.String(this).bytes.sum()
if (id == 48) id = 0
return id
}
Copy the code
It converts a String to an Int by first converting the String to bytes and then adding up all the bytes, which can be explained here.
To avoid blackening the interface, add the “Add Mask” task to the head of the main thread message queue and process it first.
Then simply listen for the Activity’s life cycle in the Application and switch on or off the night mode in onCreate() :
class TaylorApplication : Application() {
private val preference by lazy { Preference(getSharedPreferences("dark-mode", Context.MODE_PRIVATE)) }
override fun onCreate(a) {
super.onCreate()
registerActivityLifecycleCallbacks(object :ActivityLifecycleCallbacks{
override fun onActivityPaused(activity: Activity?). {}
override fun onActivityResumed(activity: Activity?). {}
override fun onActivityStarted(activity: Activity?). {}
override fun onActivityDestroyed(activity: Activity?). {}
override fun onActivitySaveInstanceState(activity: Activity? , outState:Bundle?). {}
override fun onActivityStopped(activity: Activity?). {}
override fun onActivityCreated(activity: Activity? , savedInstanceState:Bundle?).{ activity? .night(preference["dark-mode".false])}}}}Copy the code
Preference is the encapsulation of SharedPreference. It uses a more concise syntax to realize the access of values and can ignore types. For details, click here.
The effect is as follows:
This solution is not global, but for single interface, so the DialogFragment will be above the mask, then use the same method to cover the dialog box with a mask:
fun DialogFragment.nightMode(lightOff: Boolean, color: String = "#c8000000") {
val handler = Handler(Looper.getMainLooper())
val id = "darkMask"
if (lightOff) {
handler.postAtFrontOfQueue {
valmaskView = View { layout_id = id layout_width = match_parent layout_height = match_parent background_color = color } decorView? .apply {val view = findViewById<View>(id.toLayoutId())
if (view == null) {
addView(maskView)
}
}
}
} else{ decorView? .apply { find<View>(id)? .let { removeView(it) } } } }// Get the root view of the dialog box
val DialogFragment.decorView: ViewGroup?
get() {
returnview? .parentas? ViewGroup
}
Copy the code
The algorithm for adding masks is exactly the same as before, but this time it is an extension of DialogFragment.
It is not enough to overwrite activities and dialogfragments. Some popovers in the project are implemented using Windows.
fun Window.nightMode(lightOff: Boolean, color: String = "#c8000000") {
val handler = Handler(Looper.getMainLooper())
val id = "darkMask"
if (lightOff) {
handler.postAtFrontOfQueue {
val maskView = View(context).apply {
setId(id.toLayoutId())
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
setBackgroundColor(Color.parseColor(color))
}
(decorView as? ViewGroup)? .apply {val view = findViewById<View>(id.toLayoutId())
if (view == null) {
addView(maskView)
}
}
}
} else {
(decorView as? ViewGroup)? .apply { find<View>(id)? .let { removeView(it) } } } }Copy the code
The algorithm is exactly the same as before. Add a mask to the DecorView
Is that the end of the story? There is also PopupWindow, which is a little more complicated this time because there is no way to retrieve its DecorView
public class PopupWindow {
private PopupDecorView mDecorView;
private class PopupDecorView extends FrameLayout {... }}Copy the code
PopupWindow’s root view is a private member, so it can only be retrieved by reflection:
fun PopupWindow.nightMode(lightOff: Boolean, color: String = "#c8000000"){
contentView.post {
try {
// Get the mDecorView instance by reflection
val windowClass: Class<*>? = this.javaClass
valpopupDecorView = windowClass? .getDeclaredField("mDecorView") popupDecorView? .isAccessible =true
val mask = contentView.context.run {
View {
layout_width = contentView.width
layout_height = contentView.height
background_color = color
}
}
// Add a mask to mDecorView(popupDecorView? .get(this) as? FrameLayout)? .addView(mask, FrameLayout.LayoutParams(contentView.width, contentView.height)) }catch (e: Exception) {
}
}
}
Copy the code
Superview mode
The subview approach works well in an Activity, but can cause layout problems for dialogfragments that are not full-screen. Because adding a MATCH_PARENT child to a container control would probably split the parent view, use dialogfragment.nightmode () above when the Window width of the Dialog is WRAP_CONTENT. Many dialogs in the app are full screen.
Another way to think about it, inDialogFragment
Draw a semi-transparent rectangle in the superview:
// Dialog box base class
abstract class BaseDialogFragment : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View? {
// Wrap a mask around the dialog view
returncontext? .let { MaskViewGroup(it).apply { addView(createView(inflater, container, savedInstanceState)) } } }// Subclasses must override this method to customize the layout
abstract fun createView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View?
Copy the code
Where MaskViewGroup is defined as follows:
// Mask the container control
class MaskViewGroup @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) {
private lateinit var paint: Paint
init {
// Allow ViewGroup to draw content on its artboard
setWillNotDraw(false)
paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = Color.parseColor("#c8000000")}override fun onDrawForeground(canvas: Canvas?). {
super.onDrawForeground(canvas)
// Draw a grey foregroundcanvas? .drawRect(0f.0f, right.toFloat(), bottom.toFloat(), paint)
}
}
Copy the code
About how to draw in the parent control content details can click on the Android custom controls | three implementation of red dots (below)
Talk is cheap, show me the code
For the complete code, click here