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 spanActivityDisplay, need to put the windowtypeSet toWindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY.
  • To allow touch events to pass through the float window toActivity, 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 themdecorViewisActivityAn 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 themtoLayoutId()isStringExtension 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, inDialogFragmentDraw 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