下沉式通知的一種實(shí)現(xiàn) | Android懸浮窗Window應(yīng)用

當(dāng)你瀏覽公眾號(hào)時(shí)來(lái)了一條新消息,通知在屏幕頂部會(huì)以自頂向下動(dòng)畫(huà)的形式入場(chǎng)勋陪,而且它是跨界面的全局浮窗(效果如下圖)。雖然上一篇中抽象的浮窗工具類(lèi)已經(jīng)能實(shí)現(xiàn)這個(gè)需求坯苹。但本文在此基礎(chǔ)上再封裝一些更加友好的 API 來(lái)實(shí)現(xiàn)下沉式通知逢防。

跨界面浮窗

這是 Android Window 應(yīng)用的第二篇康聂,系列文章目錄如下:

  1. 懸浮窗的一種實(shí)現(xiàn) | Android懸浮窗Window應(yīng)用
  2. 下沉式通知的一種實(shí)現(xiàn) | Android懸浮窗Window應(yīng)用

預(yù)定義常用位置

上一篇抽象的show()接口通過(guò)指定xy能精確地在屏幕任意位置顯示浮窗胞四。但有時(shí)候業(yè)務(wù)需求是模糊的恬汁,比如“在屏幕右側(cè)中間顯示浮窗”。如果能新增一個(gè) API辜伟,再預(yù)定義一些常用位置氓侧,這樣業(yè)務(wù)層就可以不用關(guān)心窗口坐標(biāo)的計(jì)算。

屏幕的上下左右 4 個(gè)方向是常用位置导狡,每個(gè)位置又可以有三種gravity:起點(diǎn)约巷、中點(diǎn)、終點(diǎn)旱捧。組合一下就有 12 個(gè)常用位置独郎。

當(dāng)然可以定義 12 個(gè)常量,它們的值從 0-11 枚赡。但當(dāng)每個(gè)位置新增一種 gravity氓癌,則要新增 4 個(gè)常量。將上下左右的方位和 gravity 分成兩組可以解決這個(gè)問(wèn)題:

object FloatWindow : View.OnTouchListener {
    //'方位常量組'
    const val POSIITION_TOP = 1
    const val POSITION_LEFT = 2
    const val POSITION_RIGHT = 3
    const val POSITION_BOTTOM = 4
    
    //'gravity常量組'
    const val GRAVITY_START = 100
    const val GRAVITY_MID = 101
    const val GRAVITY_END = 102
}

重載一個(gè)帶有常用位置參數(shù)的show()函數(shù):

object FloatWindow : View.OnTouchListener {
    fun show(
        context: Context,
        tag: String,
        windowInfo: WindowInfo? = windowInfoMap[tag],
        //'新增參數(shù):位置'
        position: Int,
        //'新增參數(shù):gravity'
        gravity: Int
    ) {
        //'根據(jù)常用位置計(jì)算窗口左上角坐標(biāo)贫橙,省略了計(jì)算過(guò)程'
        when (position){
            POSITION_TOP -> {
                when (gravity) -> {
                    GRAVITY_START -> {...}
                    GRAVITY_MID -> {...}
                    GRAVITY_END -> {...}
                    else -> {...}
                }
            }
            POSITION_LEFT -> {
                when (gravity) -> {
                    GRAVITY_START -> {...}
                    GRAVITY_MID -> {...}
                    GRAVITY_END -> {...}
                    else -> {...}
                }
            }
            POSITION_RIGHT -> {
                when (gravity) -> {
                    GRAVITY_START -> {...}
                    GRAVITY_MID -> {...}
                    GRAVITY_END -> {...}
                    else -> {...}
                }
            }
            POSITION_BOTTOM -> {
                when (gravity) -> {
                    GRAVITY_START -> {...}
                    GRAVITY_MID -> {...}
                    GRAVITY_END -> {...}
                    else -> {...}
                }
            }
            else -> {...}
        }
        //'將計(jì)算出的坐標(biāo)傳入上一篇抽象的show函數(shù)'
        show( context, tag, windowInfo, x, y, false)
    }
}

沒(méi)毛病贪婉,但show()函數(shù)新增了兩個(gè)參數(shù),而且這兩個(gè)參數(shù)得合起來(lái)才表達(dá)一個(gè)完整的語(yǔ)義:窗口的位置卢肃。

二進(jìn)制位管理多個(gè)狀態(tài)

有沒(méi)有什么辦法將兩個(gè)參數(shù)合并成一個(gè)參數(shù)疲迂?有!更好的解決方案就藏在View的源碼里:

public class View {
    /*
     * '狀態(tài)常量'
     * |-------|-------|-------|-------|
     *                                 1 PFLAG_WANTS_FOCUS
     *                                1  PFLAG_FOCUSED
     *                               1   PFLAG_SELECTED
     *                              1    PFLAG_IS_ROOT_NAMESPACE
     *                             1     PFLAG_HAS_BOUNDS
     *                            1      PFLAG_DRAWN
     *                           1       PFLAG_DRAW_ANIMATION
     *                          1        PFLAG_SKIP_DRAW
     *                        1          PFLAG_REQUEST_TRANSPARENT_REGIONS
     *                       1           PFLAG_DRAWABLE_STATE_DIRTY
     *                      1            PFLAG_MEASURED_DIMENSION_SET
     *                     1             PFLAG_FORCE_LAYOUT
     *                    1              PFLAG_LAYOUT_REQUIRED
     *                   1               PFLAG_PRESSED
     *                  1                PFLAG_DRAWING_CACHE_VALID
     *                 1                 PFLAG_ANIMATION_STARTED
     *                1                  PFLAG_SAVE_STATE_CALLED
     *               1                   PFLAG_ALPHA_SET
     *              1                    PFLAG_SCROLL_CONTAINER
     *             1                     PFLAG_SCROLL_CONTAINER_ADDED
     *            1                      PFLAG_DIRTY
     *            1                      PFLAG_DIRTY_MASK
     *          1                        PFLAG_OPAQUE_BACKGROUND
     *         1                         PFLAG_OPAQUE_SCROLLBARS
     *         11                        PFLAG_OPAQUE_MASK
     *        1                          PFLAG_PREPRESSED
     *       1                           PFLAG_CANCEL_NEXT_UP_EVENT
     *      1                            PFLAG_AWAKEN_SCROLL_BARS_ON_ATTACH
     *     1                             PFLAG_HOVERED
     *    1                              PFLAG_NOTIFY_AUTOFILL_MANAGER_ON_CLICK
     *   1                               PFLAG_ACTIVATED
     *  1                                PFLAG_INVALIDATED
     * |-------|-------|-------|-------|
     */
    /** {@hide} */
    static final int PFLAG_WANTS_FOCUS                 = 0x00000001;
    /** {@hide} */
    static final int PFLAG_FOCUSED                     = 0x00000002;
    /** {@hide} */
    static final int PFLAG_SELECTED                    = 0x00000004;
    /** {@hide} */
    static final int PFLAG_IS_ROOT_NAMESPACE           = 0x00000008;
    //'當(dāng)前狀態(tài)'
    public int mPrivateFlags;
}

View將自身的所有狀態(tài)位存儲(chǔ)在一個(gè)int類(lèi)型的mPrivateFlags變量中莫湘。int占用 4 個(gè)字節(jié)尤蒿,1 個(gè)字節(jié)包含 8 位二進(jìn)制,所以它可以存儲(chǔ) 32 個(gè)二元狀態(tài)幅垮。

狀態(tài)常量也是一個(gè)int值腰池,每個(gè)狀態(tài)常量只和 32 位中的 1 位關(guān)聯(lián),View將其表示成 8 位的十六進(jìn)制军洼。(1 個(gè) 十六進(jìn)制位 等價(jià)于 4 個(gè)二進(jìn)制位巩螃,比如:)

十六進(jìn)制 二進(jìn)制
1 0001
2 0010
3 0011

我原先習(xí)慣將一個(gè)狀態(tài)位定義成一個(gè)int值,現(xiàn)在看來(lái)匕争,可以將 32 個(gè)int狀態(tài)值濃縮在一個(gè)int值中。

新增狀態(tài)時(shí)爷耀,只需進(jìn)行位或操作:

mPrivateFlags |= PFLAG_DRAWN;

判斷當(dāng)前是否具有某種狀態(tài)時(shí)甘桑,只需位與操作:

public boolean hasFocus() {
    return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}

刪除狀態(tài)時(shí),只需取反加位與操作:

mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;

使用二進(jìn)制位來(lái)管理數(shù)量眾多的狀態(tài)時(shí)不僅節(jié)約了內(nèi)存,而且狀態(tài)的變更和判斷變得輕松跑杭,復(fù)合狀態(tài)的表達(dá)變得簡(jiǎn)單

雖然當(dāng)前業(yè)務(wù)場(chǎng)景中只包含一種狀態(tài)铆帽,即浮窗的常用位置,但它是一種復(fù)合狀態(tài)德谅,包含了位置和 gravity爹橱,使用二進(jìn)制位管理,可以簡(jiǎn)化代碼:

object FloatWindow : View.OnTouchListener {
    //'使用0-3位表示gravity'
    const val FLAG_START = 0x00000001
    const val FLAG_MID = 0x00000002
    const val FLAG_END = 0x00000004
    //'使用第4-7位表示位置'
    const val FLAG_TOP = 0x00000010
    const val FLAG_LEFT = 0x00000020
    const val FLAG_RIGHT = 0x00000040
    const val FLAG_BOTTOM = 0x00000080
}

這樣show()的參數(shù)表就可以被簡(jiǎn)化:

object FloatWindow : View.OnTouchListener {
    fun show(
        context: Context,
        tag: String,
        //包含窗口寬高和坐標(biāo)的包裝類(lèi)
        windowInfo: WindowInfo? = windowInfoMap[tag],
        //'flag包含了位置和gravity信息'
        flag: Int
    ) {
        //'將 flag 解析成坐標(biāo)并顯示浮窗'
        getShowPoint(flag, windowInfo, offset).let { show(context, tag, windowInfo, it.x, it.y, false) }
    }
}

flag 的解析寫(xiě)在getShowPoint()中:

object FloatWindow : View.OnTouchListener {
    private fun getShowPoint(flag: Int, windowInfo: WindowInfo?): Point {
        return when {
            //'構(gòu)建頂部浮窗坐標(biāo)'
            flag.and(FLAG_TOP) != 0 -> {
                val y = -windowInfo?.height.value()
                //'解析flag中的gravity部分'
                val x = getValueByGravity(flag, screenWidth, windowInfo?.width.value())
                Point(x, y)
            }
            //'構(gòu)建底部浮窗坐標(biāo)'
            flag.and(FLAG_BOTTOM) != 0 -> {
                val y = screenHeight
                val x = getValueByGravity(flag, screenWidth, windowInfo?.width.value())
                Point(x, y)
            }
            //'構(gòu)建左邊浮窗坐標(biāo)'
            flag.and(FLAG_LEFT) != 0 -> {
                val x = -windowInfo?.width.value()
                val y = getValueByGravity(flag, screenHeight, windowInfo?.height.value())
                Point(x, y)
            }
            //'構(gòu)建右邊浮窗坐標(biāo)'
            flag.and(FLAG_RIGHT) != 0 -> {
                val x = screenWidth
                val y = getValueByGravity(flag, screenHeight, windowInfo?.height.value())
                Point(x, y)
            }
            else -> Point(0, 0)
        }
    }
}

解析 flag 分兩步窄做,先解析位置再解析 gravity愧驱。

解析位置時(shí),為了使浮窗有移入屏幕的效果椭盏,遂將其初始位置都置于屏幕外且緊貼屏幕邊緣组砚。比如頂部浮窗的下邊緣貼住屏幕上邊緣,所以浮窗左上角 y = -浮窗高度

解析 gravity 邏輯寫(xiě)在getValueByGravity()中:

private fun getValueByGravity(flag: Int, total: Int, actual: Int): Int = when {
    flag.and(FLAG_START) != 0 -> 0
    flag.and(FLAG_MID) != 0 -> (total - actual) / 2
    flag.and(FLAG_END) != 0 -> (total - actual)
    else -> 0
}

其中total表示邊屏幕寬(高)掏颊,actual表示對(duì)應(yīng)的浮窗寬(高)

移入動(dòng)畫(huà)

將浮窗初始位置置于屏幕之外且緊貼屏幕后糟红,只需要再設(shè)置一個(gè)位移動(dòng)畫(huà)即可實(shí)現(xiàn)移入屏幕的效果:

object FloatWindow : View.OnTouchListener {
    fun show(
        context: Context,
        tag: String,
        windowInfo: WindowInfo? = windowInfoMap[tag],
        flag: Int,
        //'窗口位移動(dòng)畫(huà)回調(diào)'
        onAnimateWindow: ((WindowInfo?) -> Unit)?
    ) {
        getShowPoint(flag, windowInfo).let { show(context, tag, windowInfo, it.x, it.y, false) }
        //'在當(dāng)前消息隊(duì)列末尾執(zhí)行窗口位移動(dòng)畫(huà)'
        windowInfo?.view?.post { onAnimateWindow?.invoke(windowInfo) }
    }
}

這個(gè)重載show()函數(shù)將動(dòng)畫(huà)的實(shí)現(xiàn)交給業(yè)務(wù)層,動(dòng)畫(huà)執(zhí)行的時(shí)間點(diǎn)被安排在消息隊(duì)列末尾乌叶,之所以這樣做是因?yàn)橐_保動(dòng)畫(huà)在窗口顯示之后執(zhí)行盆偿。

現(xiàn)在業(yè)務(wù)界面就可以像這樣顯示頂部下沉窗口:

var handler = Handler(Looper.getMainLooper())
val view = LayoutInflater.from(this).inflate(R.layout.gravity_vertical_window, null)
//'構(gòu)建窗口寬高參數(shù)'
val windowInfo = FloatWindow.WindowInfo(view).apply {
    width = DimensionUtil.dp2px(300.0)
    height = DimensionUtil.dp2px(80.0)
}

//'在屏幕頂部的正中位置顯示下沉式窗口'
FloatWindow.show(this, "top", windowInfo, FLAG_TOP or FLAG_MID) { info ->
    val anim = animSet {
        anim {
            values = intArrayOf(info.layoutParams?.y ?: 0, 0)
            interpolator = LinearOutSlowInInterpolator()
            duration = 250L
            action = { value -> FloatWindow.updateWindowView(y = value as Int) }
        }
        start()
    }
    //'1500毫秒后反向播放動(dòng)畫(huà),即隱藏下沉式窗口'
    handler.postDelayed({ anim.reverse() }, 1500)
}

animSetanim是自定義 DSL准浴,用于簡(jiǎn)化構(gòu)建動(dòng)畫(huà)代碼陈肛,其運(yùn)用的 Kotlin 語(yǔ)法糖分析,可以點(diǎn)擊Kotlin進(jìn)階:動(dòng)畫(huà)代碼太丑兄裂,用DSL動(dòng)畫(huà)庫(kù)拯救句旱,像說(shuō)話一樣寫(xiě)代碼喲!晰奖。

進(jìn)一步重載提供默認(rèn)動(dòng)畫(huà)

把構(gòu)建浮窗入場(chǎng)動(dòng)畫(huà)的細(xì)節(jié)交由業(yè)務(wù)界面實(shí)現(xiàn)谈撒,這樣雖然增加了靈活度,但也增加了業(yè)務(wù)代碼的復(fù)雜度匾南。如果能提供默認(rèn)動(dòng)畫(huà)就更好了啃匿,重載show()

object FloatWindow : View.OnTouchListener {
    //'提供默認(rèn)動(dòng)畫(huà)的浮窗顯示重載函數(shù)'
    fun show(
        context: Context,
        tag: String,
        windowInfo: WindowInfo? = windowInfoMap[tag],
        flag: Int,
        offset: Int = 0,
        //'浮窗顯示和隱藏動(dòng)畫(huà)時(shí)長(zhǎng)'
        duration: Long = 250L,
        //'浮窗停留時(shí)長(zhǎng)'
        stayTime: Long = 1500L
    ) {
        getShowPoint(flag, windowInfo, offset).let { show(context, tag, windowInfo, it.x, it.y, false) }
        windowInfo?.view?.post {
            //'構(gòu)建浮窗出場(chǎng)動(dòng)畫(huà)'
            getShowAnim(flag, windowInfo, duration)?.also { anim ->
                anim.start()
                //'延遲隱藏浮窗'
                handler.postDelayed({
                    anim.reverse()
                    //'隱藏浮窗動(dòng)畫(huà)結(jié)束后,解散浮窗'
                    anim.onEnd = { dismiss(windowInfo) }
                }, stayTime)
            }
        }
    }
}

getShowAnim()通過(guò)解析 flag 構(gòu)建對(duì)應(yīng)出場(chǎng)動(dòng)畫(huà):

object FloatWindow : View.OnTouchListener {
    private fun getShowAnim(flag: Int, windowInfo: WindowInfo?, duration: Long): AnimSet? = when {
        //'構(gòu)建自頂向下動(dòng)畫(huà)'
        flag.and(FLAG_TOP) != 0 -> {
            animSet {
                anim {
                    values = intArrayOf(windowInfo?.layoutParams?.y.value(), 0)
                    this.duration = duration
                    interpolator = LinearOutSlowInInterpolator()
                    action = { value -> updateWindowView(y = value as Int) }
                }
            }
        }
        //'構(gòu)建自底向上動(dòng)畫(huà)'
        flag.and(FLAG_BOTTOM) != 0 -> {
            animSet {
                anim {
                    values = intArrayOf(windowInfo?.layoutParams?.y.value(), windowInfo?.layoutParams?.y.value() - windowInfo?.height.value())
                    this.duration = duration
                    interpolator = LinearOutSlowInInterpolator()
                    action = { value -> updateWindowView(y = value as Int) }
                }
            }
        }
        //'構(gòu)建從左往右動(dòng)畫(huà)'
        flag.and(FLAG_LEFT) != 0 -> {
            animSet {
                anim {
                    values = intArrayOf(windowInfo?.layoutParams?.x.value(), 0)
                    this.duration = duration
                    interpolator = LinearOutSlowInInterpolator()
                    action = { value -> updateWindowView(x = value as Int) }
                }
            }
        }
        //'構(gòu)建從右往左動(dòng)畫(huà)'
        flag.and(FLAG_RIGHT) != 0 -> {
            animSet {
                anim {
                    values = intArrayOf(windowInfo?.layoutParams?.x.value(), windowInfo?.layoutParams?.x.value() - windowInfo?.layoutParams?.width.value())
                    this.duration = duration
                    interpolator = LinearOutSlowInInterpolator()
                    action = { value -> updateWindowView(x = value as Int) }
                }
            }
        }
        else -> null
    }
}

雖然有 12 個(gè)常用位置蛆楞,但浮窗出場(chǎng)動(dòng)畫(huà)只有 4 個(gè)方位溯乒,即自頂向下、自底向上豹爹、從左往右裆悄、從右往左。

上滑隱藏浮窗

若想實(shí)現(xiàn) “手指上滑隱藏不感興趣的通知”臂聋,只需在監(jiān)聽(tīng)到 fling 手勢(shì)時(shí)反向播放動(dòng)畫(huà):

object FloatWindow : View.OnTouchListener {
    private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())
    //'浮窗出入場(chǎng)動(dòng)畫(huà)'
    private var inAndOutAnim: Anim? = null
    
    override fun onTouch(v: View, event: MotionEvent): Boolean {
        //'將觸摸事件傳遞給GestureDetector解析'
        gestureDetector.onTouchEvent(event)
        return true
    }
    
    private class GestureListener : GestureDetector.OnGestureListener {
        ...
        //'GestureDetector解析觸摸事件成fling事件'
        override fun onFling(
            e1: MotionEvent,
            e2: MotionEvent,
            velocityX: Float,
            velocityY: Float
        ): Boolean {
            //'反轉(zhuǎn)入場(chǎng)動(dòng)畫(huà)'
            inAndOutAnim?.let { anim ->
                anim.reverse()
                anim.onEnd = { dismiss(windowInfo) }
                return true
            }
            return false
        }
    }
}

inAndOutAnim應(yīng)該在兩個(gè)重載show()函數(shù)中被賦值光稼,遂修改show()函數(shù)如下:

object FloatWindow : View.OnTouchListener {
    fun show(
        context: Context,
        tag: String,
        windowInfo: WindowInfo? = windowInfoMap[tag],
        flag: Int,
        offset: Int = 0,
        duration: Long = 250L,
        stayTime: Long = 1500L
    ) {
        getShowPoint(flag, windowInfo, offset).let { show(context, tag, windowInfo, it.x, it.y, false) }
        windowInfo?.view?.post {
            //'構(gòu)建默認(rèn)出入場(chǎng)動(dòng)畫(huà)時(shí)給inAndOutAnim賦值'
            inAndOutAnim = getShowAnim(flag, windowInfo, duration)?.also { anim ->
                anim.start()
                handler.postDelayed({
                    anim.reverse()
                    anim.onEnd = { dismiss(windowInfo) }
                }, stayTime)
            }
        }
    }
    
    fun show(
        context: Context,
        tag: String,
        windowInfo: WindowInfo? = windowInfoMap[tag],
        flag: Int,
        offset: Int = 0,
        //'修改lambda返回值為Anim'
        onAnimateWindow: ((WindowInfo) -> Anim)?
    ) {
        getShowPoint(flag, windowInfo, offset).let { show(context, tag, windowInfo, it.x, it.y, false) }
        //'業(yè)務(wù)界面構(gòu)建的出入場(chǎng)動(dòng)畫(huà)作為lambda的返回值并賦給inAndOutAnim'
        windowInfo?.view?.post { inAndOutAnim = onAnimateWindow?.invoke(windowInfo) }
    }
}

//'業(yè)務(wù)界面這樣顯示下沉式通知'
val view = LayoutInflater.from(this).inflate(R.layout.gravity_vertical_window, null)
    val windowInfo = FloatWindow.WindowInfo(view).apply {
        width = DimensionUtil.dp2px(300.0)
        height = DimensionUtil.dp2px(80.0)
    }

    FloatWindow.show(this, "top", windowInfo, FLAG_TOP or FLAG_MID) { info ->
        val anim = animSet {
            anim {
                values = intArrayOf(info.layoutParams?.y ?: 0, 0)
                interpolator = LinearOutSlowInInterpolator()
                duration = 250L
                action = { value -> FloatWindow.updateWindowView(y = value as Int) }
            }
            start()
        }
        handler.postDelayed({ anim.reverse() }, 1500)
        //'將構(gòu)建的動(dòng)畫(huà)實(shí)例作為lambda值'
        anim
    }

全局浮窗

通知類(lèi)型浮窗和其他浮窗不同或南,它是全局的,當(dāng)切換 Activity 時(shí)要求浮窗持續(xù)展示艾君。只需要靜態(tài)申請(qǐng)一個(gè)權(quán)限并修改窗口類(lèi)型即可實(shí)現(xiàn):

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

AndroidManifest.xml中添加這個(gè)權(quán)限采够,然后修改show()函數(shù),增加全局參數(shù):

object FloatWindow : View.OnTouchListener {
    fun show(
        context: Context,
        tag: String,
        windowInfo: WindowInfo? = windowInfoMap[tag],
        x: Int = windowInfo?.layoutParams?.x.value(),
        y: Int = windowInfo?.layoutParams?.y.value(),
        dragEnable: Boolean = false,
        //'是否是全局浮窗'
        overall: Boolean = false
    ) {
        ...
        //'構(gòu)建全局浮窗布局參數(shù)'
        windowInfo.layoutParams = createLayoutParam(x, y, overall)
        if (!windowInfo.hasParent().value()) {
            val windowManager = this.context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
            prepareScreenDimension(windowManager)
            windowManager?.addView(windowInfo.view, windowInfo.layoutParams)
            updateWindowViewSize()
            onWindowShow?.invoke()
        }
    }
    
    private fun createLayoutParam(x: Int, y: Int, overall: Boolean): WindowManager.LayoutParams {
        if (context == null) {
            return WindowManager.LayoutParams()
        }

        return WindowManager.LayoutParams().apply {
            //'為全局浮窗指定不一樣的窗口類(lèi)型'
            type = if (overall) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
                } else {
                    WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
                }
            } else {
                WindowManager.LayoutParams.TYPE_APPLICATION
            }
            format = PixelFormat.TRANSLUCENT
            flags =
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or WindowManager.LayoutParams.FLAG_DIM_BEHIND
            dimAmount = 0f
            this.gravity = Gravity.START or Gravity.TOP
            width = windowInfo?.width.value()
            height = windowInfo?.height.value()
            this.x = x
            this.y = y
        }
    }
}

當(dāng) Window 類(lèi)型設(shè)置為TYPE_APPLICATION_OVERLAYTYPE_SYSTEM_ALERT時(shí)冰垄,窗口就不隸屬于某一個(gè) Activity蹬癌。這樣就做到了全局展示。

talk is cheap, show me the code

完整代碼可點(diǎn)擊上面鏈接虹茶。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末逝薪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子写烤,更是在濱河造成了極大的恐慌翼闽,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洲炊,死亡現(xiàn)場(chǎng)離奇詭異感局,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)暂衡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)询微,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人狂巢,你說(shuō)我怎么就攤上這事撑毛。” “怎么了唧领?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵藻雌,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我斩个,道長(zhǎng)胯杭,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任受啥,我火速辦了婚禮做个,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘滚局。我一直安慰自己居暖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布藤肢。 她就那樣靜靜地躺著太闺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谤草。 梳的紋絲不亂的頭發(fā)上跟束,一...
    開(kāi)封第一講書(shū)人閱讀 51,562評(píng)論 1 305
  • 那天莺奸,我揣著相機(jī)與錄音丑孩,去河邊找鬼冀宴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛温学,可吹牛的內(nèi)容都是我干的略贮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼仗岖,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼逃延!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起轧拄,我...
    開(kāi)封第一講書(shū)人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤揽祥,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后檩电,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體拄丰,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年俐末,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了料按。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡卓箫,死狀恐怖载矿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情烹卒,我是刑警寧澤闷盔,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站旅急,受9級(jí)特大地震影響逢勾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜坠非,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一敏沉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧炎码,春花似錦盟迟、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至歉闰,卻和暖如春辖众,著一層夾襖步出監(jiān)牢的瞬間卓起,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工凹炸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留戏阅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓啤它,卻偏偏與公主長(zhǎng)得像奕筐,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子变骡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容