真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

Android中怎么實現(xiàn)懸浮窗功能

今天就跟大家聊聊有關(guān)Android中怎么實現(xiàn)懸浮窗功能,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

成都創(chuàng)新互聯(lián)公司堅持“要么做到,要么別承諾”的工作理念,服務(wù)領(lǐng)域包括:成都網(wǎng)站建設(shè)、網(wǎng)站設(shè)計、企業(yè)官網(wǎng)、英文網(wǎng)站、手機端網(wǎng)站、網(wǎng)站推廣等服務(wù),滿足客戶于互聯(lián)網(wǎng)時代的黎平網(wǎng)站設(shè)計、移動媒體設(shè)計的需求,幫助企業(yè)找到有效的互聯(lián)網(wǎng)解決方案。努力成為您成熟可靠的網(wǎng)絡(luò)建設(shè)合作伙伴!

業(yè)務(wù)場景

以微信視頻通話為例,在視頻通話時,我們打開其他應(yīng)用或點擊Home鍵退出時或點擊縮放圖標,懸浮窗會顯示在其他應(yīng)用之上,給人的假象是通話頁面變小了,點擊懸浮窗回到通過頁面,懸浮窗消失。退出通話頁面懸浮窗消失。

業(yè)務(wù)場景技術(shù)分析

在編碼之前,我們必須將流程整理好,這樣更有利于編碼的實現(xiàn)。實現(xiàn)一個功能如果需要10分鐘,思考的時間是7分鐘,編碼占用的時間只是三分鐘。

1.懸浮窗可以顯示在其他應(yīng)用或launchers之上,這個肯定需要懸浮窗權(quán)限,而懸浮窗權(quán)限屬于特殊權(quán)限,所以只能通過引導(dǎo)用戶去打開無法像危險權(quán)限那樣直接申請??梢宰龅胶笈_顯示則說明懸浮窗是一個Service。

2.通話頁面隱藏時懸浮窗顯示,通話頁面顯示時懸浮窗隱藏,可以看出懸浮窗和Activity的生命周期相關(guān)聯(lián),所以懸浮窗的Service和通話頁面的Activity是通過bind去綁定的。

3.既然Service和Activity是通過bind去綁定的,說明當(dāng)懸浮窗顯示的時候,通話Activity雖然不可見但仍在運行。

結(jié)合上述技術(shù)問題分析,我們倒敘一一通過編碼實現(xiàn)

懸浮窗實現(xiàn)方案

實現(xiàn)效果

準備工作

首先我們新建一個項目,項目中有兩個Activity,我們在第二個Activity編寫通話模擬頁面。在第二個頁面的原因我們后面會講到。

如何將acitivity置于后臺

其實很簡單,我們調(diào)用一個方法即可

moveTaskToBack(true);

這個方法的含義就是將當(dāng)前的任務(wù)戰(zhàn)置于后臺,so,為什么我要在第二個Activity中實現(xiàn)的原因之一,因為默認的Activity的啟動模式是標準模式,而上面方法會將任務(wù)棧置于后臺而不是一個單獨的Activity,所以我們?yōu)榱孙@示懸浮窗時不影響操作軟件的其他功能,我們要將通話頁面的Activity設(shè)置為singleInstance,這樣當(dāng)調(diào)用上面方法的時候只是將通話頁面所在的Activity棧置于后臺,如果你還不了解啟動模式可以移步至上一篇文章:Activity的啟動模式。

我們現(xiàn)在在右上方的點擊事件中添加上述代碼,可以看到通話頁面的Activity的已經(jīng)在后臺運行了。

判斷是否有懸浮窗權(quán)限

點擊左上角圖標時,我們要先判斷當(dāng)前app是否有懸浮窗權(quán)限,首先我們在配置文件中添加,懸浮窗的權(quán)限。

(很多文章標題都是懸浮窗如何繞過權(quán)限,什么設(shè)置類型為TOAST或者PHONE,我想說不可能的事,TOAST類型的雖然部分機型可以顯示但是就是一個普通的TOSAT會自動消失)

那么我們?nèi)绾闻袛嗍欠裼袘腋〈皺?quán)限呢,這一塊不同廠商處理方案可能不一樣,這里我們用一種通用的處理方案,測試表明除了(vivo部分)無效,其他多數(shù)機型都ok。并且vivo部分機型微信通話也不會彈出提示(這我就放心了~)

fun zoom(v: View) {  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {    if (!Settings.canDrawOverlays(this)) {      Toast.makeText(this, "當(dāng)前無權(quán)限,請授權(quán)", Toast.LENGTH_SHORT)      GlobalDialogSingle(this, "", "當(dāng)前未獲取懸浮窗權(quán)限", "去開啟", DialogInterface.OnClickListener { dialog, which ->        dialog.dismiss()        startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + packageName)), 0)      }).show()     } else {      moveTaskToBack(true)      val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)      hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)    }  }}

我們通過Settings.canDrawOverlays(this)來判斷當(dāng)前應(yīng)用是否有懸浮窗權(quán)限,如果沒有,我們彈窗提示,通過

startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + packageName)), 0)

跳轉(zhuǎn)到開啟懸浮窗權(quán)限頁面。如果懸浮窗權(quán)限已開啟,直接將當(dāng)前任務(wù)棧置于后臺,開啟服務(wù)即可。

其實回調(diào)方法,并沒有直接告訴我們是否授權(quán)成功,所以我們需要在回調(diào)中再次判斷

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {  if (requestCode == 0) {    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {      if (!Settings.canDrawOverlays(this)) {        Toast.makeText(this, "授權(quán)失敗", Toast.LENGTH_SHORT).show()      } else {        Handler().postDelayed({          val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)          intent.putExtra("rangeTime", rangeTime)          hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)          moveTaskToBack(true)        }, 1000)       }    }  }}

這里我們可以看到回調(diào)中延遲了1秒,因為測試發(fā)現(xiàn)某些機型反應(yīng)“過快”,收到回調(diào)的時候還以為沒有授權(quán)成功,其實已經(jīng)成功了。

綁定Service我們需要一個ServiceConnection對象

internal var mVideoServiceConnection: ServiceConnection = object : ServiceConnection {  override fun onServiceConnected(name: ComponentName, service: IBinder) {    // 獲取服務(wù)的操作對象    val binder = service as FloatWinfowServices.MyBinder    binder.service  }  override fun onServiceDisconnected(name: ComponentName) {}}

Main2Activity的完整代碼如下所示:

/** * @author Huanglinqing */class Main2Activity : AppCompatActivity() {  private val chronometer: Chronometer? = null  private var hasBind = false  private val rangeTime: Long = 0  override fun onCreate(savedInstanceState: Bundle?) {    super.onCreate(savedInstanceState)    setContentView(R.layout.activity_main2)  }  fun zoom(v: View) {    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {      if (!Settings.canDrawOverlays(this)) {        Toast.makeText(this, "當(dāng)前無權(quán)限,請授權(quán)", Toast.LENGTH_SHORT)        GlobalDialogSingle(this, "", "當(dāng)前未獲取懸浮窗權(quán)限", "去開啟", DialogInterface.OnClickListener { dialog, which ->          dialog.dismiss()          startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + packageName)), 0)        }).show()      } else {        moveTaskToBack(true)        val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)        hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)      }    }  }  internal var mVideoServiceConnection: ServiceConnection = object : ServiceConnection {    override fun onServiceConnected(name: ComponentName, service: IBinder) {      // 獲取服務(wù)的操作對象      val binder = service as FloatWinfowServices.MyBinder      binder.service    }    override fun onServiceDisconnected(name: ComponentName) {}  }  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {    if (requestCode == 0) {      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {        if (!Settings.canDrawOverlays(this)) {          Toast.makeText(this, "授權(quán)失敗", Toast.LENGTH_SHORT).show()        } else {          Handler().postDelayed({            val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)            intent.putExtra("rangeTime", rangeTime)            hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)            moveTaskToBack(true)          }, 1000)        }      }    }  }  override fun onRestart() {    super.onRestart()    Log.d("RemoteView", "重新顯示了")    //不顯示懸浮框    if (hasBind) {      unbindService(mVideoServiceConnection)      hasBind = false    }  }  override fun onNewIntent(intent: Intent) {    super.onNewIntent(intent)  }  override fun onDestroy() {    super.onDestroy()  }}

新建懸浮窗Service

新建懸浮窗Service FloatWinfowServices,因為我們使用的BindService,我們在onBind方法中初始化service中的布局

override fun onBind(intent: Intent): IBinder? {  initWindow()  //懸浮框點擊事件的處理  initFloating()  return MyBinder()}

service中我們通過WindowManager來添加一個布局顯示。

/** * 初始化窗口 */private fun initWindow() {  winManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager  //設(shè)置好懸浮窗的參數(shù)  wmParams = params  // 懸浮窗默認顯示以左上角為起始坐標  wmParams!!.gravity = Gravity.LEFT or Gravity.TOP  //懸浮窗的開始位置,因為設(shè)置的是從左上角開始,所以屏幕左上角是x=0;y=0  wmParams!!.x = winManager!!.defaultDisplay.width  wmParams!!.y = 210  //得到容器,通過這個inflater來獲得懸浮窗控件  inflater = LayoutInflater.from(applicationContext)  // 獲取浮動窗口視圖所在布局  mFloatingLayout = inflater!!.inflate(R.layout.remoteview, null)  // 添加懸浮窗的視圖  winManager!!.addView(mFloatingLayout, wmParams)}

懸浮窗的參數(shù)主要設(shè)置懸浮窗的類型為

WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY

8.0 以下可設(shè)置為:

wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE

代碼如下所示:

private //設(shè)置window type 下面變量2002是在屏幕區(qū)域顯示,2003則可以顯示在狀態(tài)欄之上    //設(shè)置可以顯示在狀態(tài)欄上    //設(shè)置懸浮窗口長寬數(shù)據(jù)val params: WindowManager.LayoutParams  get() {    wmParams = WindowManager.LayoutParams()    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {      wmParams!!.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY    } else {      wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE    }    wmParams!!.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or        WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or        WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH    wmParams!!.width = WindowManager.LayoutParams.WRAP_CONTENT    wmParams!!.height = WindowManager.LayoutParams.WRAP_CONTENT    return wmParams  }

當(dāng)點擊懸浮窗的時候回到Activity2頁面,并且懸浮窗消失,所以我們只需要給懸浮窗添加點擊事件

linearLayout!!.setOnClickListener { startActivity(Intent(this@FloatWinfowServices, Main2Activity::class.java)) }

當(dāng)Service走到onDestory的時候?qū)iew移除,對于Activity2頁面來說 當(dāng)onResume的時候 解綁Service,當(dāng)onstop的時候 綁定Service。

從效果圖中我們可以看到懸浮窗可以拖拽的,所以還要設(shè)置觸摸事件,當(dāng)移動距離超過某個值的時候讓onTouch消費事件,這樣就不會觸發(fā)點擊事件了。這個算是view比較基礎(chǔ)的知識,相信大家都明白了。

//開始觸控的坐標,移動時的坐標(相對于屏幕左上角的坐標)private var mTouchStartX: Int = 0private var mTouchStartY: Int = 0private var mTouchCurrentX: Int = 0private var mTouchCurrentY: Int = 0//開始時的坐標和結(jié)束時的坐標(相對于自身控件的坐標)private var mStartX: Int = 0private var mStartY: Int = 0private var mStopX: Int = 0private var mStopY: Int = 0//判斷懸浮窗口是否移動,這里做個標記,防止移動后松手觸發(fā)了點擊事件private var isMove: Boolean = falseprivate inner class FloatingListener : View.OnTouchListener {  override fun onTouch(v: View, event: MotionEvent): Boolean {    val action = event.action    when (action) {      MotionEvent.ACTION_DOWN -> {        isMove = false        mTouchStartX = event.rawX.toInt()        mTouchStartY = event.rawY.toInt()        mStartX = event.x.toInt()        mStartY = event.y.toInt()      }      MotionEvent.ACTION_MOVE -> {        mTouchCurrentX = event.rawX.toInt()        mTouchCurrentY = event.rawY.toInt()        wmParams!!.x += mTouchCurrentX - mTouchStartX        wmParams!!.y += mTouchCurrentY - mTouchStartY        winManager!!.updateViewLayout(mFloatingLayout, wmParams)        mTouchStartX = mTouchCurrentX        mTouchStartY = mTouchCurrentY      }      MotionEvent.ACTION_UP -> {        mStopX = event.x.toInt()        mStopY = event.y.toInt()        if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) {          isMove = true        }      }      else -> {      }    }    //如果是移動事件不觸發(fā)OnClick事件,防止移動的時候一放手形成點擊事件    return isMove  }}

FloatWinfowServices所有代碼如下所示:

class FloatWinfowServices : Service() {   private var winManager: WindowManager? = null  private var wmParams: WindowManager.LayoutParams? = null  private var inflater: LayoutInflater? = null  //浮動布局  private var mFloatingLayout: View? = null  private var linearLayout: LinearLayout? = null  private var chronometer: Chronometer? = null  override fun onBind(intent: Intent): IBinder? {    initWindow()    //懸浮框點擊事件的處理    initFloating()    return MyBinder()  }  inner class MyBinder : Binder() {    val service: FloatWinfowServices      get() = this@FloatWinfowServices  }  override fun onCreate() {    super.onCreate()  }  /**   * 懸浮窗點擊事件   */  private fun initFloating() {    linearLayout = mFloatingLayout!!.findViewById(R.id.line1)    linearLayout!!.setOnClickListener { startActivity(Intent(this@FloatWinfowServices, Main2Activity::class.java)) }    //懸浮框觸摸事件,設(shè)置懸浮框可拖動    linearLayout!!.setOnTouchListener(FloatingListener())  }  //開始觸控的坐標,移動時的坐標(相對于屏幕左上角的坐標)  private var mTouchStartX: Int = 0  private var mTouchStartY: Int = 0  private var mTouchCurrentX: Int = 0  private var mTouchCurrentY: Int = 0  //開始時的坐標和結(jié)束時的坐標(相對于自身控件的坐標)  private var mStartX: Int = 0  private var mStartY: Int = 0  private var mStopX: Int = 0  private var mStopY: Int = 0  //判斷懸浮窗口是否移動,這里做個標記,防止移動后松手觸發(fā)了點擊事件  private var isMove: Boolean = false  private inner class FloatingListener : View.OnTouchListener {    override fun onTouch(v: View, event: MotionEvent): Boolean {      val action = event.action      when (action) {        MotionEvent.ACTION_DOWN -> {          isMove = false          mTouchStartX = event.rawX.toInt()          mTouchStartY = event.rawY.toInt()          mStartX = event.x.toInt()          mStartY = event.y.toInt()        }        MotionEvent.ACTION_MOVE -> {          mTouchCurrentX = event.rawX.toInt()          mTouchCurrentY = event.rawY.toInt()          wmParams!!.x += mTouchCurrentX - mTouchStartX          wmParams!!.y += mTouchCurrentY - mTouchStartY          winManager!!.updateViewLayout(mFloatingLayout, wmParams)          mTouchStartX = mTouchCurrentX          mTouchStartY = mTouchCurrentY        }        MotionEvent.ACTION_UP -> {          mStopX = event.x.toInt()          mStopY = event.y.toInt()          if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) {            isMove = true          }        }        else -> {        }      }      //如果是移動事件不觸發(fā)OnClick事件,防止移動的時候一放手形成點擊事件      return isMove    }  }  /**   * 初始化窗口   */  private fun initWindow() {    winManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager    //設(shè)置好懸浮窗的參數(shù)    wmParams = params    // 懸浮窗默認顯示以左上角為起始坐標    wmParams!!.gravity = Gravity.LEFT or Gravity.TOP    //懸浮窗的開始位置,因為設(shè)置的是從左上角開始,所以屏幕左上角是x=0;y=0    wmParams!!.x = winManager!!.defaultDisplay.width    wmParams!!.y = 210    //得到容器,通過這個inflater來獲得懸浮窗控件    inflater = LayoutInflater.from(applicationContext)    // 獲取浮動窗口視圖所在布局    mFloatingLayout = inflater!!.inflate(R.layout.remoteview, null)    chronometer = mFloatingLayout!!.findViewById(R.id.chronometer)    chronometer!!.start()    // 添加懸浮窗的視圖    winManager!!.addView(mFloatingLayout, wmParams)  }  private //設(shè)置window type 下面變量2002是在屏幕區(qū)域顯示,2003則可以顯示在狀態(tài)欄之上      //設(shè)置可以顯示在狀態(tài)欄上      //設(shè)置懸浮窗口長寬數(shù)據(jù)  val params: WindowManager.LayoutParams    get() {      wmParams = WindowManager.LayoutParams()      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {        wmParams!!.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY      } else {        wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE      }      wmParams!!.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or          WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or          WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH      wmParams!!.width = WindowManager.LayoutParams.WRAP_CONTENT      wmParams!!.height = WindowManager.LayoutParams.WRAP_CONTENT      return wmParams    }  override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {    return super.onStartCommand(intent, flags, startId)  }  override fun onDestroy() {    super.onDestroy()    winManager!!.removeView(mFloatingLayout)  }}

實際應(yīng)用中需要考慮的一些其他問題

在使用使用的過程中,我們肯定會遇到其他問題:

1.用戶使用過程中,可能會直接按Home鍵,這個時候如何提示呢?

產(chǎn)生問題原因:因為用戶按Home鍵之后,開發(fā)者無法重寫Home鍵邏輯,此時應(yīng)用不在前臺運行,無法彈窗提醒,此時用戶點擊APP圖標進入的是第一個棧,這個時候用戶就沒有進入通話頁面的入口了。

解決方案:

第一種解決方案 我們可以仿照微信那樣去做,就是在整個通話過程中開啟一個前臺通知,用戶點擊通知時進入通話頁面。

第二種解決方案 就是檢測應(yīng)用是否在前臺,當(dāng)通話頁面在運行的時候,并且應(yīng)用重新回到前臺,我們廣播到其他頁面,提示權(quán)限引導(dǎo)即可。

2.用戶在通話頁面(singleInstance模式),點擊Home鍵

應(yīng)用在后臺運行的時候,通話結(jié)束,Activity被finish,此時從任務(wù)程序中切回應(yīng)用你會發(fā)現(xiàn)打開的竟然是通話頁面!

這個問題簡單的說就是,如果你在通話頁面呼叫某人,通話過程中按Home鍵,然后電話掛斷,此時你從任務(wù)程序中切回應(yīng)用,會再次呼叫這個人,也就是這種狀態(tài)下重新回到了onCreate方法。

問題產(chǎn)生原因:

1.因為通話頁面是singleInstance模式,此時有兩個任務(wù)棧,按Home鍵后再從任務(wù)程序中切回,此時應(yīng)用只保留了第二個任務(wù)棧,已經(jīng)失去了和第一個任務(wù)棧的關(guān)系,finish之后無法在回到第一個任務(wù)棧。

解決方案:

1.(不推薦)通話頁面不使用singleInstance模式,這種情況下,在通話過程中無法操作軟件的其他功能,一般都不采取。

2.(我目前的解決方案)設(shè)置一個標記位,標記當(dāng)前是否在通話,在onCreate中如果通話已經(jīng)結(jié)束了,跳轉(zhuǎn)到一個過渡頁面(標準模式),過渡頁面中finish,就可以了,添加過渡頁面的原因是我們不知道上一個頁面是哪里,因為我們收到來電可能是任意頁面,我們我們在過渡頁面finsh之后,就再次回到了第一個任務(wù)棧。

看完上述內(nèi)容,你們對Android中怎么實現(xiàn)懸浮窗功能有進一步的了解嗎?如果還想了解更多知識或者相關(guān)內(nèi)容,請關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝大家的支持。


名稱欄目:Android中怎么實現(xiàn)懸浮窗功能
文章位置:http://weahome.cn/article/ihgpcd.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部