91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Android懸浮窗如何實現

發布時間:2021-11-20 17:02:06 來源:億速云 閱讀:161 作者:小新 欄目:移動開發

小編給大家分享一下Android懸浮窗如何實現,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

效果如下:

Android懸浮窗如何實現

顯示浮窗

原生ViewManager接口提供了向窗口添加并操縱View的方法:

public interface ViewManager{    //'向窗口添加視圖'
    public void addView(View view, ViewGroup.LayoutParams params);    //'更新窗口中視圖'
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);    //'移除窗口中視圖'
    public void removeView(View view);
}
復制代碼

使用這個接口顯示窗口的模版代碼如下:

//'解析布局文件為視圖'val windowView = LayoutInflater.from(context).inflate(R.id.window_view, null)//'獲取WindowManager系統服務'val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager//'構建窗口布局參數'WindowManager.LayoutParams().apply {
    type = WindowManager.LayoutParams.TYPE_APPLICATION
    width = WindowManager.LayoutParams.WRAP_CONTENT
    height = WindowManager.LayoutParams.WRAP_CONTENT
    gravity = Gravity.START or Gravity.TOP
    x = 0
    y = 0}.let { layoutParams->    //'將視圖添加到窗口'
    windowManager.addView(windowView, layoutParams)
}
復制代碼
  • 上述代碼在當前界面的左上角顯示R.id.window_view.xml中定義的布局。

  • 為避免重復,將這段代碼抽象成一個函數,其中窗口視圖內容和展示位置會隨著需求而變,遂將其參數化:

object FloatWindow{    private var context: Context? = null    //'當前窗口參數'
    var windowInfo: WindowInfo? = null    //'把和Window布局有關的參數打包成一個內部類'
    class WindowInfo(var view: View?) {
        var layoutParams: WindowManager.LayoutParams? = null        //'窗口寬'
        var width: Int = 0
        //'窗口高'
        var height: Int = 0
        //'窗口中是否有視圖'
        fun hasView() = view != null && layoutParams != null        //'窗口中視圖是否有父親'
        fun hasParent() = hasView() && view?.parent != null
    }    //'顯示窗口'
    fun show(
        context: Context,
        windowInfo: WindowInfo?,
        x: Int = windowInfo?.layoutParams?.x.value(),
        y: Int = windowInfo?.layoutParams?.y.value(),
    ) {        if (windowInfo == null) { return }        if (windowInfo.view == null) { return }        this.windowInfo = windowInfo        this.context = context        //'創建窗口布局參數'
        windowInfo.layoutParams = createLayoutParam(x, y)        //'顯示窗口'
        if (!windowInfo.hasParent().value()) {
            val windowManager = this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
            windowManager.addView(windowInfo.view, windowInfo.layoutParams)
        }
    }    //'創建窗口布局參數'
    private fun createLayoutParam(x: Int, y: Int): WindowManager.LayoutParams {        if (context == null) { return WindowManager.LayoutParams() }        return WindowManager.LayoutParams().apply {            //'該類型不需要申請權限'
            type = WindowManager.LayoutParams.TYPE_APPLICATION
            format = PixelFormat.TRANSLUCENT
            flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
            gravity = Gravity.START or Gravity.TOP
            width = windowInfo?.width.value()
            height = windowInfo?.height.value()            this.x = x            this.y = y
        }
    }    //'為空Int提供默認值'
    fun Int?.value() = this ?: 0}
復制代碼
  • FloatWindow聲明成了單例,目的是在 app 整個生命周期,任何界面都可以方便地顯示浮窗。

  • 為了方便統一管理窗口的參數,抽象了內部類WindowInfo

  • 現在就可以像這樣在屏幕左上角顯示一個浮窗了:

val windowView = LayoutInflater.from(context).inflate(R.id.window_view, null)
WindowInfo(windowView).apply{
    width = 100
    height = 100
}.let{ windowInfo ->
    FloatWindow.show(context, windowInfo, 0, 0)
}
復制代碼

浮窗背景色

產品要求當浮窗顯示時,屏幕變暗。設置WindowManager.LayoutParams.FLAG_DIM_BEHIND標簽配合dimAmount就能輕松實現:

object FloatWindow{    //當前窗口參數
    var windowInfo: WindowInfo? = null
    private fun createLayoutParam(x: Int, y: Int): WindowManager.LayoutParams {        if (context == null) { return WindowManager.LayoutParams() }        return WindowManager.LayoutParams().apply {
            type = 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            //'設置默認變暗程度為0,即不變暗,1表示全黑'
            dimAmount = 0f
            gravity = Gravity.START or Gravity.TOP
            width = windowInfo?.width.value()
            height = windowInfo?.height.value()            this.x = x            this.y = y
        }
    }    //'供業務界面在需要的時候調整浮窗背景亮暗'
    fun setDimAmount(amount:Float){
        windowInfo?.layoutParams?.let { it.dimAmount = amount }
    }
}
復制代碼

設置浮窗點擊事件

為浮窗設置點擊事件等價于為浮窗視圖設置點擊事件,但如果直接對浮窗視圖使用setOnClickListener()的話,浮窗的觸摸事件就不會被響應,那拖拽就無法實現。所以只能從更底層的觸摸事件著手:

object FloatWindow : View.OnTouchListener{ 
    //'顯示窗口'
    fun show(
        context: Context,
        windowInfo: WindowInfo?,
        x: Int = windowInfo?.layoutParams?.x.value(),
        y: Int = windowInfo?.layoutParams?.y.value(),
    ) {        if (windowInfo == null) { return }        if (windowInfo.view == null) { return }        this.windowInfo = windowInfo        this.context = context        //'為浮窗視圖設置觸摸監聽器'
        windowInfo.view?.setOnTouchListener(this)
        windowInfo.layoutParams = createLayoutParam(x, y)        if (!windowInfo.hasParent().value()) {
            val windowManager = this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
            windowManager.addView(windowInfo.view, windowInfo.layoutParams)
        }
    }    override fun onTouch(v: View, event: MotionEvent): Boolean {        return false
    }
}
復制代碼
  • onTouch(v: View, event: MotionEvent)中可以拿到更詳細的觸摸事件,比如ACTION_DOWNACTION_MOVEACTION_UP。這方便了拖拽的實現,但點擊事件的捕獲變得復雜,因為需要定義上述三個 ACTION 以怎樣的序列出現時才判定為點擊事件。幸好GestureDetector為我們做了這件事:

public class GestureDetector {    public interface OnGestureListener {        //'ACTION_DOWN事件'
        boolean onDown(MotionEvent e);        //'單擊事件'
        boolean onSingleTapUp(MotionEvent e);        //'拖拽事件'
        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
        ...
    }
}
復制代碼

構建GestureDetector實例并將MotionEvent傳遞給它就能將觸摸事件解析成感興趣的上層事件:

object FloatWindow : View.OnTouchListener{ 
    private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())    private var clickListener: WindowClickListener? = null    private var lastTouchX: Int = 0
    private var lastTouchY: Int = 0
    //'為浮窗設置點擊監聽器'
    fun setClickListener(listener: WindowClickListener) {
        clickListener = listener
    }
    override fun onTouch(v: View, event: MotionEvent): Boolean {        //'將觸摸事件傳遞給 GestureDetector 解析'
        gestureDetector.onTouchEvent(event)        return true
    }    //'記憶起始觸摸點坐標'
    private fun onActionDown(event: MotionEvent) {
        lastTouchX = event.rawX.toInt()
        lastTouchY = event.rawY.toInt()
    }    private class GestureListener : GestureDetector.OnGestureListener {        //'記憶起始觸摸點坐標'
        override fun onDown(e: MotionEvent): Boolean {
            onActionDown(e)            return false
        }
        override fun onSingleTapUp(e: MotionEvent): Boolean {            //'點擊事件發生時,調用監聽器'
            return clickListener?.onWindowClick(windowInfo) ?: false
        }
        ...
    }    //'浮窗點擊監聽器'
    interface WindowClickListener {
        fun onWindowClick(windowInfo: WindowInfo?): Boolean
    }
}
復制代碼

拖拽浮窗

ViewManager提供了updateViewLayout(View view, ViewGroup.LayoutParams params)用于更新浮窗位置,所以只需監聽ACTION_MOVE事件并實時更新浮窗視圖位置就可實現拖拽。ACTION_MOVE事件被GestureDetector解析成OnGestureListener.onScroll()回調:

object FloatWindow : View.OnTouchListener{ 
    private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())    private var lastTouchX: Int = 0
    private var lastTouchY: Int = 0
    override fun onTouch(v: View, event: MotionEvent): Boolean {        //'將觸摸事件傳遞給GestureDetector解析'
        gestureDetector.onTouchEvent(event)        return true
    }    private class GestureListener : GestureDetector.OnGestureListener {
        override fun onDown(e: MotionEvent): Boolean {
            onActionDown(e)            return false
        }
        override fun onScroll(e1: MotionEvent,e2: MotionEvent,distanceX: Float,distanceY:Float): Boolean {            //'響應手指滾動事件'
            onActionMove(e2)            return true
        }
    }    private fun onActionMove(event: MotionEvent) {        //'獲取當前手指坐標'
        val currentX = event.rawX.toInt()
        val currentY = event.rawY.toInt()        //'獲取手指移動增量'
        val dx = currentX - lastTouchX
        val dy = currentY - lastTouchY        //'將移動增量應用到窗口布局參數上'
        windowInfo?.layoutParams!!.x += dx
        windowInfo?.layoutParams!!.y += dy
        val windowManager = context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        var rightMost = screenWidth - windowInfo?.layoutParams!!.width
        var leftMost = 0
        val topMost = 0
        val bottomMost = screenHeight - windowInfo?.layoutParams!!.height - getNavigationBarHeight(context)        //'將浮窗移動區域限制在屏幕內'
        if (windowInfo?.layoutParams!!.x < leftMost) {
            windowInfo?.layoutParams!!.x = leftMost
        }        if (windowInfo?.layoutParams!!.x > rightMost) {
            windowInfo?.layoutParams!!.x = rightMost
        }        if (windowInfo?.layoutParams!!.y < topMost) {
            windowInfo?.layoutParams!!.y = topMost
        }        if (windowInfo?.layoutParams!!.y > bottomMost) {
            windowInfo?.layoutParams!!.y = bottomMost
        }        //'更新浮窗位置'
        windowManager.updateViewLayout(windowInfo?.view, windowInfo?.layoutParams)
        lastTouchX = currentX
        lastTouchY = currentY
    }
}
復制代碼

浮窗自動貼邊

新的需求來了,拖拽浮窗松手后,需要自動貼邊。

把貼邊理解成一個水平位移動畫。在松手時求出動畫起點和終點橫坐標,利用動畫值不斷更新浮窗位置::

object FloatWindow : View.OnTouchListener{ 
    private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener())    private var lastTouchX: Int = 0
    private var lastTouchY: Int = 0
    //'貼邊動畫'
    private var weltAnimator: ValueAnimator? = null
    override fun onTouch(v: View, event: MotionEvent): Boolean {        //'將觸摸事件傳遞給GestureDetector解析'
        gestureDetector.onTouchEvent(event)        //'處理ACTION_UP事件'
        val action = event.action
        when (action) {
            MotionEvent.ACTION_UP -> onActionUp(event, screenWidth, windowInfo?.width ?: 0)            else -> {
            }
        }        return true
    }    private fun onActionUp(event: MotionEvent, screenWidth: Int, width: Int) {        if (!windowInfo?.hasView().value()) { return }        //'記錄抬手橫坐標'
        val upX = event.rawX.toInt()        //'貼邊動畫終點橫坐標'
        val endX = if (upX > screenWidth / 2) {
            screenWidth - width
        } else {            0
        }        //'構建貼邊動畫'
        if (weltAnimator == null) {
            weltAnimator = ValueAnimator.ofInt(windowInfo?.layoutParams!!.x, endX).apply {
                interpolator = LinearInterpolator()
                duration = 300
                addUpdateListener { animation ->
                    val x = animation.animatedValue as Int                    if (windowInfo?.layoutParams != null) {
                        windowInfo?.layoutParams!!.x = x
                    }
                    val windowManager = context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager                    //'更新窗口位置'
                    if (windowInfo?.hasParent().value()) {
                        windowManager.updateViewLayout(windowInfo?.view, windowInfo?.layoutParams)
                    }
                }
            }
        }
        weltAnimator?.setIntValues(windowInfo?.layoutParams!!.x, endX)
        weltAnimator?.start()
    }    //為空Boolean提供默認值
    fun Boolean?.value() = this ?: false}
復制代碼
  • GestureDetector解析后ACTION_UP事件被吞掉了,所以只能在onTouch()中截獲它。

  • 根據抬手橫坐標和屏幕中點橫坐標的大小關系,來決定浮窗貼向左邊還是右邊。

管理多個浮窗

若 app 的不同業務界面同時需要顯示浮窗:進入 界面A 時顯示 浮窗A,然后它被拖拽到右下角,退出 界面A 進入 界面B,顯示浮窗B,當再次進入 界面A 時,期望還原上次離開時的浮窗A的位置。

當前FloatWindow中用windowInfo成員存儲單個浮窗參數,為了同時管理多個浮窗,需要將所有浮窗參數保存在Map結構中用 tag 區分:

object FloatWindow : View.OnTouchListener {    //'浮窗參數容器'
    private var windowInfoMap: HashMap<String, WindowInfo?> = HashMap()    //'當前浮窗參數'
    var windowInfo: WindowInfo? = null
    //'顯示浮窗'
    fun show(
        context: Context,        //'浮窗標簽'
        tag: String,        //'若不提供浮窗參數則從參數容器中獲取該tag上次保存的參數'
        windowInfo: WindowInfo? = windowInfoMap[tag],        x: Int = windowInfo?.layoutParams?.x.value(),        y: Int = windowInfo?.layoutParams?.y.value()
    ) {        if (windowInfo == null) { return }        if (windowInfo.view == null) { return }        //'更新當前浮窗參數'
        this.windowInfo = windowInfo        //'將浮窗參數存入容器'
        windowInfoMap[tag] = windowInfo
        windowInfo.view?.setOnTouchListener(this)        this.context = context
        windowInfo.layoutParams = createLayoutParam(x, y)        if (!windowInfo.hasParent().value()) {
            val windowManager =this.context?.getSystemService(Context.WINDOW_SERVICE) as WindowManager
            windowManager.addView(windowInfo.view, windowInfo.layoutParams)
        }
    }
}
復制代碼

在顯示浮窗時,增加tag標簽參數用以唯一標識浮窗,并且為windowInfo提供默認參數,當恢復原有浮窗時,可以不提供windowInfo參數,FloatWindow就會去windowInfoMap中根據給定tag尋找對應windowInfo

監聽浮窗界外點擊事件

新的需求來了,點擊浮窗時,貼邊的浮窗像抽屜一樣展示,點擊浮窗以外區域時,抽屜收起。

剛開始接到這個新需求時,沒什么思路。轉念一想PopupWindow有一個setOutsideTouchable()

public class PopupWindow {    /**
     * <p>Controls whether the pop-up will be informed of touch events outside
     * of its window. 
     *
     * @param touchable true if the popup should receive outside
     * touch events, false otherwise
     */
    public void setOutsideTouchable(boolean touchable) {
        mOutsideTouchable = touchable;
    }
}
復制代碼

該函數用于設置是否允許 window 邊界外的觸摸事件傳遞給 window。跟蹤mOutsideTouchable變量應該就能找到更多線索:

public class PopupWindow {
    private int computeFlags(int curFlags) {
        curFlags &= ~(
                WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES |
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
                WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
                WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS |
                WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |
                WindowManager.LayoutParams.FLAG_SPLIT_TOUCH);
        ...        //'如果界外可觸摸,則將FLAG_WATCH_OUTSIDE_TOUCH賦值給flag'
        if (mOutsideTouchable) {
            curFlags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
        }
        ...
    }
}
復制代碼

繼續往上跟蹤computeFlags()調用的地方:

public class PopupWindow {    protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
        p.gravity = computeGravity();        //'計算窗口布局參數flag屬性并賦值'
        p.flags = computeFlags(p.flags);
        p.type = mWindowLayoutType;
        p.token = token;
        ...
    }
}
復制代碼

createPopupLayoutParams()會在窗口顯示的時候被調用:

public class PopupWindow {    public void showAtLocation(IBinder token, int gravity, int x, int y) {        if (isShowing() || mContentView == null) { return; }
        TransitionManager.endTransitions(mDecorView);
        detachFromAnchor();
        mIsShowing = true;
        mIsDropdown = false;
        mGravity = gravity;        //'構建窗口布局參數'
        final WindowManager.LayoutParams p = createPopupLayoutParams(token);
        preparePopup(p);
        p.x = x;
        p.y = y;
        invokePopup(p);
    }
}
復制代碼

想在源碼中繼續搜索,但到FLAG_WATCH_OUTSIDE_TOUCH,線索就斷了。現在只知道為了讓界外點擊事件傳遞給 window,必須為布局參數設置FLAG_WATCH_OUTSIDE_TOUCH。但事件響應邏輯應該寫在哪里?

當調用PopupWindow.setOutsideTouchable(true),在窗口界外點擊后,窗口會消失。這必然是調用了dismiss(),沿著dismiss()的調用鏈往上找一定能找到界外點擊事件的響應邏輯:

public class PopupWindow {    //'窗口根視圖'
    private class PopupDecorView extends FrameLayout {        //'窗口根視圖觸摸事件'
        @Override
        public boolean onTouchEvent(MotionEvent event) {            final int x = (int) event.getX();            final int y = (int) event.getY();            if ((event.getAction() == MotionEvent.ACTION_DOWN)
                    && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
                dismiss();                return true;            //'如果發生了界外觸摸事件則解散窗口'
            } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                dismiss();                return true;
            } else {                return super.onTouchEvent(event);
            }
        }
    }
}
復制代碼

所以只需要在窗口根視圖的觸摸事件回調中捕獲ACTION_OUTSIDE即可:

object FloatWindow : View.OnTouchListener {    //'界外觸摸事件回調'
    private var onTouchOutside: (() -> Unit)? = null
    //'設置是否響應界外點擊事件'
    fun setOutsideTouchable(enable: Boolean, onTouchOutside: (() -> Unit)? = null) {
        windowInfo?.layoutParams?.let { layoutParams ->
            layoutParams.flags = layoutParams.flags or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH            this.onTouchOutside = onTouchOutside
        }
    }    override fun onTouch(v: View, event: MotionEvent): Boolean {        //'界外觸摸事件處理'
        if (event.action == MotionEvent.ACTION_OUTSIDE) {
            onTouchOutside?.invoke()            return true
        }        //'點擊和拖拽事件處理'
        gestureDetector.onTouchEvent(event).takeIf { !it }?.also {            //there is no ACTION_UP event in GestureDetector
            val action = event.action            when (action) {
                MotionEvent.ACTION_UP -> onActionUp(event, screenWidth, windowInfo?.width ?: 0)                else -> {
                }
            }
        }        return true
    }
}
復制代碼

以上是“Android懸浮窗如何實現”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

凤山市| 延长县| 仙桃市| 花莲市| 西畴县| 铜梁县| 云霄县| 平遥县| 茌平县| 和龙市| 林周县| 吉首市| 密山市| 蓬莱市| 永川市| 加查县| 咸阳市| 保定市| 牙克石市| 宜城市| 比如县| 高雄市| 密云县| 南昌县| 晋江市| 镇赉县| 河西区| 乌苏市| 丹凤县| 德阳市| 夏津县| 彭阳县| 天等县| 东乌珠穆沁旗| 新沂市| 阜平县| 丹棱县| 都昌县| 英山县| 林芝县| 衡水市|