Android懸浮窗的實(shí)現(xiàn)(易錯(cuò)點(diǎn))
0. 前言
現(xiàn)在很多應(yīng)用都使用到懸浮窗,例如微信在視頻的時(shí)候,點(diǎn)擊Home鍵,視頻小窗口仍然會(huì)在屏幕上顯示。這個(gè)功能在很多情況下都非常有用。那么今天我們就來(lái)實(shí)現(xiàn)一下Android懸浮窗,以及探索一下實(shí)現(xiàn)懸浮窗時(shí)的易錯(cuò)點(diǎn)。
1. 實(shí)現(xiàn)原理
1.1 懸浮窗插入接口
在實(shí)現(xiàn)懸浮窗之前,我們需要知道通過(guò)什么接口,能夠?qū)⒁粋€(gè)控件放入到屏幕中去。
Android的界面繪制,都是通過(guò)WindowMananger的服務(wù)來(lái)實(shí)現(xiàn)的。那么,既然要實(shí)現(xiàn)一個(gè)能夠在自身應(yīng)用以外的界面上的懸浮窗,我們就要利用WindowManager來(lái)“做手腳”。
(frameworks/base/core/java/android/view/WindowMananger.java)
@SystemService(Context.WINDOW_SERVICE)
public interface WindowManager extends ViewManager {
...
}
WindowManager實(shí)現(xiàn)了ViewManager接口,可以通過(guò)獲取WINDOW_SERVICE系統(tǒng)服務(wù)得到。而ViewManager接口有addView方法,我們就是通過(guò)這個(gè)方法將懸浮窗控件加入到屏幕中去。
1.2 權(quán)限設(shè)置及請(qǐng)求
懸浮窗需要在別的應(yīng)用之上顯示控件,很顯然,這需要某些權(quán)限才可以。
在API Level >= 23的時(shí)候,需要在AndroidManefest.xml文件中聲明權(quán)限SYSTEM_ALERT_WINDOW才能在其他應(yīng)用上繪制控件。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
除了這個(gè)權(quán)限外,我們還需要在系統(tǒng)設(shè)置里面對(duì)本應(yīng)用進(jìn)行設(shè)置懸浮窗權(quán)限。該權(quán)限在應(yīng)用中需要啟動(dòng)Settings.ACTION_MANAGE_OVERLAY_PERMISSION來(lái)讓用戶(hù)手動(dòng)設(shè)置權(quán)限。
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), REQUEST_CODE);
1.3 LayoutParam設(shè)置
WindowManager的addView方法有兩個(gè)參數(shù),一個(gè)是需要加入的控件對(duì)象,另一個(gè)參數(shù)是WindowManager.LayoutParam對(duì)象。
這里需要著重說(shuō)明的是LayoutParam里的type變量。這個(gè)變量是用來(lái)指定窗口類(lèi)型的。在設(shè)置這個(gè)變量時(shí),需要注意一個(gè)坑,那就是需要對(duì)不同版本的Android系統(tǒng)進(jìn)行適配。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
在Android 8.0之前,懸浮窗口設(shè)置可以為T(mén)YPE_PHONE,這種類(lèi)型是用于提供用戶(hù)交互操作的非應(yīng)用窗口。
而Android 8.0對(duì)系統(tǒng)和API行為做了修改,包括使用SYSTEM_ALERT_WINDOW權(quán)限的應(yīng)用無(wú)法再使用一下窗口類(lèi)型來(lái)在其他應(yīng)用和窗口上方顯示提醒窗口:
- TYPE_PHONE
- TYPE_PRIORITY_PHONE
- TYPE_SYSTEM_ALERT
- TYPE_SYSTEM_OVERLAY
- TYPE_SYSTEM_ERROR
如果需要實(shí)現(xiàn)在其他應(yīng)用和窗口上方顯示提醒窗口,那么必須該為T(mén)YPE_APPLICATION_OVERLAY的新類(lèi)型。
如果在Android 8.0以上版本仍然使用TYPE_PHONE類(lèi)型的懸浮窗口,則會(huì)出現(xiàn)如下異常信息:
android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@f8ec928 -- permission denied for window type 2002
2. 具體實(shí)現(xiàn)
下面來(lái)講解一下懸浮窗的具體實(shí)現(xiàn)方式。
完整的源碼地址:https://github.com/dongzhong/TestForFloatingWindow
為了讓?xiě)腋〈芭cActivity脫離,使其在應(yīng)用處于后臺(tái)時(shí)懸浮窗仍然可以正常運(yùn)行,這里使用Service來(lái)啟動(dòng)懸浮窗并做為其背后邏輯支撐。
在啟動(dòng)服務(wù)之前,需要先判斷一下當(dāng)前是否允許開(kāi)啟懸浮窗。
(MainActivity.java)
public void startFloatingService(View view) {
...
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "當(dāng)前無(wú)權(quán)限,請(qǐng)授權(quán)", Toast.LENGTH_SHORT);
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 0);
} else {
startService(new Intent(MainActivity.this, FloatingService.class));
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 0) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "授權(quán)失敗", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "授權(quán)成功", Toast.LENGTH_SHORT).show();
startService(new Intent(MainActivity.this, FloatingService.class));
}
}
}
懸浮窗控件可以是任意的View的子類(lèi)類(lèi)型。這里先以一個(gè)最簡(jiǎn)單的Button來(lái)做示例。
(FloatingService.java)
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
showFloatingWindow();
return super.onStartCommand(intent, flags, startId);
}
private void showFloatingWindow() {
if (Settings.canDrawOverlays(this)) {
// 獲取WindowManager服務(wù)
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
// 新建懸浮窗控件
Button button = new Button(getApplicationContext());
button.setText("Floating Window");
button.setBackgroundColor(Color.BLUE);
// 設(shè)置LayoutParam
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
layoutParams.format = PixelFormat.RGBA_8888;
layoutParams.width = 500;
layoutParams.height = 100;
layoutParams.x = 300;
layoutParams.y = 300;
// 將懸浮窗控件添加到WindowManager
windowManager.addView(button, layoutParams);
}
}
好了,完成了!
對(duì),沒(méi)看錯(cuò),最簡(jiǎn)單的懸浮窗這就實(shí)現(xiàn)了。是不是很簡(jiǎn)單?來(lái)看看效果吧。

當(dāng)然了,這個(gè)懸浮窗的效果僅僅是顯示出來(lái),離真正想要的效果還相差甚遠(yuǎn)。不過(guò)基礎(chǔ)的原理是已經(jīng)實(shí)現(xiàn)了,剩下的就是要在這上面一點(diǎn)點(diǎn)的添加功能啦。
3. 增加小功能
3.1 拖動(dòng)功能
首先想要增加的功能就是能夠拖動(dòng)這個(gè)懸浮窗。因?yàn)閼腋〈帮@示的位置也許會(huì)擋住背后我們想要看到的信息,如果能夠把懸浮窗拖走那就最好了。
在Android中,觸摸事件的處理算是一個(gè)最基本操作了,直接上代碼。
(FloatingService.java)
private void showFloatingWindow() {
...
button.setOnTouchListener(new FloatingOnTouchListener());
...
}
private class FloatingOnTouchListener implements View.OnTouchListener {
private int x;
private int y;
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x = (int) event.getRawX();
y = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int nowX = (int) event.getRawX();
int nowY = (int) event.getRawY();
int movedX = nowX - x;
int movedY = nowY - y;
x = nowX;
y = nowY;
layoutParams.x = layoutParams.x + movedX;
layoutParams.y = layoutParams.y + movedY;
// 更新懸浮窗控件布局
windowManager.updateViewLayout(view, layoutParams);
break;
default:
break;
}
return false;
}
}
這里需要注意的是,在代碼注釋處的更新懸浮窗控件布局的方法。只有調(diào)用了這個(gè)方法,懸浮窗的位置才會(huì)發(fā)生改變??纯葱Ч伞?/p>

3.2 圖片自動(dòng)播放
下面我們對(duì)懸浮窗做一些小變動(dòng),來(lái)演示一下略微復(fù)雜一丟丟的界面。
這里的懸浮窗界面我們不再單純的使用一個(gè)Button控件,而是在一個(gè)LinearLayout內(nèi)加一個(gè)ImageView,布局文件如下。
(image_display.xml)
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ImageView android:id="@+id/image_display_imageview" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
在創(chuàng)建懸浮窗布局的地方做一些修改。
(FloatingService.java)
private void showFloatingWindow() {
...
LayoutInflater layoutInflater = LayoutInflater.from(this);
displayView = layoutInflater.inflate(R.layout.image_display, null);
displayView.setOnTouchListener(new FloatingOnTouchListener());
ImageView imageView = displayView.findViewById(R.id.image_display_imageview);
imageView.setImageResource(images[imageIndex]);
windowManager.addView(displayView, layoutParams);
...
}
我們還想讓圖片隔兩秒就切換一張,那么就再做一個(gè)定時(shí)切換圖片的機(jī)制吧。
(FloatingService.java)
@Override
public void onCreate() {
...
changeImageHandler = new Handler(this.getMainLooper(), changeImageCallback);
}
private Handler.Callback changeImageCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == 0) {
imageIndex++;
if (imageIndex >= 5) {
imageIndex = 0;
}
if (displayView != null) {
((ImageView) displayView.findViewById(R.id.image_display_imageview)).setImageResource(images[imageIndex]);
}
changeImageHandler.sendEmptyMessageDelayed(0, 2000);
}
return false;
}
};
private void showFloatingWindow() {
...
windowManager.addView(displayView, layoutParams);
changeImageHandler.sendEmptyMessageDelayed(0, 2000);
}
來(lái)看一下懸浮窗自動(dòng)播放圖片的效果吧。
3.3 視頻小窗口
下面我們就來(lái)看看懸浮窗最常用的功能:視頻小窗口。例如微信在視頻過(guò)程中退出界面,就會(huì)以小窗口的形式來(lái)顯示視頻。在這里,我先以MediaPlay和SurfaceView播放一個(gè)網(wǎng)絡(luò)視頻來(lái)模擬一下效果。實(shí)現(xiàn)起來(lái)與上面的圖片播放器基本相同,只是改變了控件和相應(yīng)的播放邏輯。
布局文件類(lèi)似上面的圖片播放器,只是把ImageView替換成了SurfaceView。
創(chuàng)建懸浮窗控件。
(FloatingService.java)
private void showFloatingWindow() {
...
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
SurfaceView surfaceView = displayView.findViewById(R.id.video_display_surfaceview);
final SurfaceHolder surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
mediaPlayer.setDisplay(surfaceHolder);
}
...
);
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mediaPlayer.start();
}
});
try {
mediaPlayer.setDataSource(this, Uri.parse("https://raw.githubusercontent.com/dongzhong/ImageAndVideoStore/master/Bruno%20Mars%20-%20Treasure.mp4"));
mediaPlayer.prepareAsync();
}
catch (IOException e) {
Toast.makeText(this, "無(wú)法打開(kāi)視頻源", Toast.LENGTH_LONG).show();
}
windowManager.addView(displayView, layoutParams);
}
好啦,下面就來(lái)一曲火星哥騷氣的《Treasure》吧。
4. 總結(jié)
以上就是Android懸浮窗的實(shí)現(xiàn)方式,以及一些小小的簡(jiǎn)單應(yīng)用。
可以總結(jié)為以下幾個(gè)步驟:
1. 聲明及申請(qǐng)權(quán)限
2. 構(gòu)建懸浮窗需要的控件
3. 將控件添加到`WindowManager`
4. 必要時(shí)更新`WindowManager`的布局
需要注意的容易掉的坑就是 LayoutParams.type的版本適配問(wèn)題。
Demo源碼地址:https://github.com/dongzhong/TestForFloatingWindow
以上所述是小編給大家介紹的Android懸浮窗的實(shí)現(xiàn),希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
如果你覺(jué)得本文對(duì)你有幫助,歡迎轉(zhuǎn)載,煩請(qǐng)注明出處,謝謝!
- Android 8.0如何完美適配全局dialog懸浮窗彈出
- Android利用WindowManager實(shí)現(xiàn)懸浮窗
- Android實(shí)現(xiàn)類(lèi)似qq微信消息懸浮窗通知功能
- Android應(yīng)用內(nèi)懸浮窗的實(shí)現(xiàn)方案示例
- Android 懸浮窗權(quán)限各機(jī)型各系統(tǒng)適配大全(總結(jié))
- Android懸浮窗屏蔽懸浮窗外部所有的點(diǎn)擊事件的實(shí)例代碼
- 不依賴(lài)于Activity的Android全局懸浮窗的實(shí)現(xiàn)
- android編程實(shí)現(xiàn)懸浮窗體的方法
- Android實(shí)現(xiàn)類(lèi)似360,QQ管家那樣的懸浮窗
- android 添加隨意拖動(dòng)的桌面懸浮窗口
相關(guān)文章
Android listview動(dòng)態(tài)加載列表項(xiàng)實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Android listview動(dòng)態(tài)加載列表項(xiàng)實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-06-06
詳解Android Automotive車(chē)載應(yīng)用對(duì)駕駛模式Safe Drive Mode的適配
這篇文章主要介紹了詳解Android Automotive車(chē)載應(yīng)用對(duì)駕駛模式(Safe Drive Mode)的適配,對(duì)車(chē)載應(yīng)用感興趣的同學(xué)可以參考下2021-04-04
Android布局加載之LayoutInflater示例詳解
這篇文章主要介紹了Android布局加載之LayoutInflater的相關(guān)資料,文中介紹的非常詳細(xì),對(duì)大家具有一定的參考借鑒價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-03-03
Android實(shí)現(xiàn)下拉放大圖片松手自動(dòng)反彈效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)下拉放大圖片松手自動(dòng)反彈效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03
Android獲取當(dāng)前位置的經(jīng)緯度數(shù)據(jù)
這篇文章主要介紹了Android獲取當(dāng)前位置的經(jīng)緯度數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2016-02-02
Android圖表庫(kù)HelloChart繪制多折線(xiàn)圖
這篇文章主要為大家詳細(xì)介紹了Android圖表庫(kù)HelloChart繪制多折線(xiàn)圖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02
Android使用Jni實(shí)現(xiàn)壓力鍋數(shù)據(jù)檢測(cè)效果示例
這篇文章主要介紹了Android使用Jni實(shí)現(xiàn)壓力鍋數(shù)據(jù)檢測(cè)效果,涉及Android結(jié)合Jni實(shí)現(xiàn)進(jìn)度條模擬壓力鍋數(shù)據(jù)監(jiān)測(cè)效果的相關(guān)操作技巧,需要的朋友可以參考下2017-12-12
Android仿淘寶詳情頁(yè)面viewPager滑動(dòng)到最后一張圖片跳轉(zhuǎn)的功能
需要做一個(gè)仿淘寶客戶(hù)端ViewPager滑動(dòng)到最后一頁(yè),再拖動(dòng)的時(shí)候跳到詳情的功能,剛開(kāi)始我也迷糊了,通過(guò)查閱相關(guān)資料發(fā)現(xiàn)有好多種實(shí)現(xiàn)方法,下面小編給大家分享實(shí)例代碼,感興趣的朋友一起看看吧2017-03-03

