小編給大家分享一下Android懸浮窗如何實(shí)現(xiàn),相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
站在用戶的角度思考問題,與客戶深入溝通,找到寧波網(wǎng)站設(shè)計(jì)與寧波網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗(yàn),讓設(shè)計(jì)與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個(gè)性化、用戶體驗(yàn)好的作品,建站類型包括:成都網(wǎng)站制作、成都網(wǎng)站建設(shè)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、空間域名、網(wǎng)絡(luò)空間、企業(yè)郵箱。業(yè)務(wù)覆蓋寧波地區(qū)。
效果如下:
原生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); } 復(fù)制代碼
使用這個(gè)接口顯示窗口的模版代碼如下:
//'解析布局文件為視圖'val windowView = LayoutInflater.from(context).inflate(R.id.window_view, null)//'獲取WindowManager系統(tǒng)服務(wù)'val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager//'構(gòu)建窗口布局參數(shù)'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) } 復(fù)制代碼
上述代碼在當(dāng)前界面的左上角顯示R.id.window_view.xml
中定義的布局。
為避免重復(fù),將這段代碼抽象成一個(gè)函數(shù),其中窗口視圖內(nèi)容和展示位置會(huì)隨著需求而變,遂將其參數(shù)化:
object FloatWindow{ private var context: Context? = null //'當(dāng)前窗口參數(shù)' var windowInfo: WindowInfo? = null //'把和Window布局有關(guān)的參數(shù)打包成一個(gè)內(nèi)部類' 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 //'創(chuàng)建窗口布局參數(shù)' 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) } } //'創(chuàng)建窗口布局參數(shù)' private fun createLayoutParam(x: Int, y: Int): WindowManager.LayoutParams { if (context == null) { return WindowManager.LayoutParams() } return WindowManager.LayoutParams().apply { //'該類型不需要申請(qǐng)權(quán)限' 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提供默認(rèn)值' fun Int?.value() = this ?: 0} 復(fù)制代碼
將FloatWindow
聲明成了單例,目的是在 app 整個(gè)生命周期,任何界面都可以方便地顯示浮窗。
為了方便統(tǒng)一管理窗口的參數(shù),抽象了內(nèi)部類WindowInfo
現(xiàn)在就可以像這樣在屏幕左上角顯示一個(gè)浮窗了:
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) } 復(fù)制代碼
產(chǎn)品要求當(dāng)浮窗顯示時(shí),屏幕變暗。設(shè)置WindowManager.LayoutParams.FLAG_DIM_BEHIND
標(biāo)簽配合dimAmount
就能輕松實(shí)現(xiàn):
object FloatWindow{ //當(dāng)前窗口參數(shù) 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 //'設(shè)置浮窗背景變暗' WindowManager.LayoutParams.FLAG_DIM_BEHIND //'設(shè)置默認(rèn)變暗程度為0,即不變暗,1表示全黑' dimAmount = 0f gravity = Gravity.START or Gravity.TOP width = windowInfo?.width.value() height = windowInfo?.height.value() this.x = x this.y = y } } //'供業(yè)務(wù)界面在需要的時(shí)候調(diào)整浮窗背景亮暗' fun setDimAmount(amount:Float){ windowInfo?.layoutParams?.let { it.dimAmount = amount } } } 復(fù)制代碼
為浮窗設(shè)置點(diǎn)擊事件等價(jià)于為浮窗視圖設(shè)置點(diǎn)擊事件,但如果直接對(duì)浮窗視圖使用setOnClickListener()
的話,浮窗的觸摸事件就不會(huì)被響應(yīng),那拖拽就無法實(shí)現(xiàn)。所以只能從更底層的觸摸事件著手:
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 //'為浮窗視圖設(shè)置觸摸監(jiān)聽器' 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 } } 復(fù)制代碼
在onTouch(v: View, event: MotionEvent)
中可以拿到更詳細(xì)的觸摸事件,比如ACTION_DOWN
,ACTION_MOVE
、ACTION_UP
。這方便了拖拽的實(shí)現(xiàn),但點(diǎn)擊事件的捕獲變得復(fù)雜,因?yàn)樾枰x上述三個(gè) ACTION 以怎樣的序列出現(xiàn)時(shí)才判定為點(diǎn)擊事件。幸好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); ... } } 復(fù)制代碼
構(gòu)建GestureDetector
實(shí)例并將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 //'為浮窗設(shè)置點(diǎn)擊監(jiān)聽器' fun setClickListener(listener: WindowClickListener) { clickListener = listener } override fun onTouch(v: View, event: MotionEvent): Boolean { //'將觸摸事件傳遞給 GestureDetector 解析' gestureDetector.onTouchEvent(event) return true } //'記憶起始觸摸點(diǎn)坐標(biāo)' private fun onActionDown(event: MotionEvent) { lastTouchX = event.rawX.toInt() lastTouchY = event.rawY.toInt() } private class GestureListener : GestureDetector.OnGestureListener { //'記憶起始觸摸點(diǎn)坐標(biāo)' override fun onDown(e: MotionEvent): Boolean { onActionDown(e) return false } override fun onSingleTapUp(e: MotionEvent): Boolean { //'點(diǎn)擊事件發(fā)生時(shí),調(diào)用監(jiān)聽器' return clickListener?.onWindowClick(windowInfo) ?: false } ... } //'浮窗點(diǎn)擊監(jiān)聽器' interface WindowClickListener { fun onWindowClick(windowInfo: WindowInfo?): Boolean } } 復(fù)制代碼
ViewManager
提供了updateViewLayout(View view, ViewGroup.LayoutParams params)
用于更新浮窗位置,所以只需監(jiān)聽ACTION_MOVE
事件并實(shí)時(shí)更新浮窗視圖位置就可實(shí)現(xiàn)拖拽。ACTION_MOVE
事件被GestureDetector
解析成OnGestureListener.onScroll()
回調(diào):
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 { //'響應(yīng)手指滾動(dòng)事件' onActionMove(e2) return true } } private fun onActionMove(event: MotionEvent) { //'獲取當(dāng)前手指坐標(biāo)' val currentX = event.rawX.toInt() val currentY = event.rawY.toInt() //'獲取手指移動(dòng)增量' val dx = currentX - lastTouchX val dy = currentY - lastTouchY //'將移動(dòng)增量應(yīng)用到窗口布局參數(shù)上' 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) //'將浮窗移動(dòng)區(qū)域限制在屏幕內(nèi)' 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 } } 復(fù)制代碼
新的需求來了,拖拽浮窗松手后,需要自動(dòng)貼邊。
把貼邊理解成一個(gè)水平位移動(dòng)畫。在松手時(shí)求出動(dòng)畫起點(diǎn)和終點(diǎn)橫坐標(biāo),利用動(dòng)畫值不斷更新浮窗位置::
object FloatWindow : View.OnTouchListener{ private var gestureDetector: GestureDetector = GestureDetector(context, GestureListener()) private var lastTouchX: Int = 0 private var lastTouchY: Int = 0 //'貼邊動(dòng)畫' 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 } //'記錄抬手橫坐標(biāo)' val upX = event.rawX.toInt() //'貼邊動(dòng)畫終點(diǎn)橫坐標(biāo)' val endX = if (upX > screenWidth / 2) { screenWidth - width } else { 0 } //'構(gòu)建貼邊動(dòng)畫' 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提供默認(rèn)值 fun Boolean?.value() = this ?: false} 復(fù)制代碼
GestureDetector
解析后ACTION_UP
事件被吞掉了,所以只能在onTouch()
中截獲它。
根據(jù)抬手橫坐標(biāo)和屏幕中點(diǎn)橫坐標(biāo)的大小關(guān)系,來決定浮窗貼向左邊還是右邊。
若 app 的不同業(yè)務(wù)界面同時(shí)需要顯示浮窗:進(jìn)入 界面A 時(shí)顯示 浮窗A,然后它被拖拽到右下角,退出 界面A 進(jìn)入 界面B,顯示浮窗B,當(dāng)再次進(jìn)入 界面A 時(shí),期望還原上次離開時(shí)的浮窗A的位置。
當(dāng)前FloatWindow
中用windowInfo
成員存儲(chǔ)單個(gè)浮窗參數(shù),為了同時(shí)管理多個(gè)浮窗,需要將所有浮窗參數(shù)保存在Map
結(jié)構(gòu)中用 tag 區(qū)分:
object FloatWindow : View.OnTouchListener { //'浮窗參數(shù)容器' private var windowInfoMap: HashMap<String, WindowInfo?> = HashMap() //'當(dāng)前浮窗參數(shù)' var windowInfo: WindowInfo? = null //'顯示浮窗' fun show( context: Context, //'浮窗標(biāo)簽' tag: String, //'若不提供浮窗參數(shù)則從參數(shù)容器中獲取該tag上次保存的參數(shù)' 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 } //'更新當(dāng)前浮窗參數(shù)' this.windowInfo = windowInfo //'將浮窗參數(shù)存入容器' 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) } } } 復(fù)制代碼
在顯示浮窗時(shí),增加tag
標(biāo)簽參數(shù)用以唯一標(biāo)識(shí)浮窗,并且為windowInfo
提供默認(rèn)參數(shù),當(dāng)恢復(fù)原有浮窗時(shí),可以不提供windowInfo
參數(shù),FloatWindow
就會(huì)去windowInfoMap
中根據(jù)給定tag
尋找對(duì)應(yīng)windowInfo
。
新的需求來了,點(diǎn)擊浮窗時(shí),貼邊的浮窗像抽屜一樣展示,點(diǎn)擊浮窗以外區(qū)域時(shí),抽屜收起。
剛開始接到這個(gè)新需求時(shí),沒什么思路。轉(zhuǎn)念一想PopupWindow
有一個(gè)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; } } 復(fù)制代碼
該函數(shù)用于設(shè)置是否允許 window 邊界外的觸摸事件傳遞給 window。跟蹤mOutsideTouchable
變量應(yīng)該就能找到更多線索:
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; } ... } } 復(fù)制代碼
繼續(xù)往上跟蹤computeFlags()
調(diào)用的地方:
public class PopupWindow { protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) { final WindowManager.LayoutParams p = new WindowManager.LayoutParams(); p.gravity = computeGravity(); //'計(jì)算窗口布局參數(shù)flag屬性并賦值' p.flags = computeFlags(p.flags); p.type = mWindowLayoutType; p.token = token; ... } } 復(fù)制代碼
而createPopupLayoutParams()
會(huì)在窗口顯示的時(shí)候被調(diào)用:
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; //'構(gòu)建窗口布局參數(shù)' final WindowManager.LayoutParams p = createPopupLayoutParams(token); preparePopup(p); p.x = x; p.y = y; invokePopup(p); } } 復(fù)制代碼
想在源碼中繼續(xù)搜索,但到FLAG_WATCH_OUTSIDE_TOUCH
,線索就斷了。現(xiàn)在只知道為了讓界外點(diǎn)擊事件傳遞給 window,必須為布局參數(shù)設(shè)置FLAG_WATCH_OUTSIDE_TOUCH
。但事件響應(yīng)邏輯應(yīng)該寫在哪里?
當(dāng)調(diào)用PopupWindow.setOutsideTouchable(true)
,在窗口界外點(diǎn)擊后,窗口會(huì)消失。這必然是調(diào)用了dismiss()
,沿著dismiss()
的調(diào)用鏈往上找一定能找到界外點(diǎn)擊事件的響應(yīng)邏輯:
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; //'如果發(fā)生了界外觸摸事件則解散窗口' } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { dismiss(); return true; } else { return super.onTouchEvent(event); } } } } 復(fù)制代碼
所以只需要在窗口根視圖的觸摸事件回調(diào)中捕獲ACTION_OUTSIDE
即可:
object FloatWindow : View.OnTouchListener { //'界外觸摸事件回調(diào)' private var onTouchOutside: (() -> Unit)? = null //'設(shè)置是否響應(yīng)界外點(diǎn)擊事件' 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 } //'點(diǎn)擊和拖拽事件處理' 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 } } 復(fù)制代碼
以上是“Android懸浮窗如何實(shí)現(xiàn)”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道!
網(wǎng)站名稱:Android懸浮窗如何實(shí)現(xiàn)
鏈接分享:http://www.chinadenli.net/article6/joisog.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站建設(shè)、App開發(fā)、ChatGPT、微信小程序、手機(jī)網(wǎng)站建設(shè)、微信公眾號(hào)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)