Android懸浮窗的學習
本文參考了董小蟲的Android懸浮窗的實現
和Android Demo : 懸浮窗(支援Android7.0)
但是本文的程式碼是來自董小蟲的demo,可以支援到android 8.0。內容僅用於學習
更高階的部分功能使用看下面
Android 8.0 懸浮窗變動與用法
專案實戰:WindowManager中removeView的那些坑-隨心所欲removeView
懸浮窗的實現需要用到service,這樣按下Home退出到桌面的時候,懸浮窗還可以作為程式後臺,繼續顯示在螢幕上,如果不清後臺,它會一直在那裡哈。注意要在AndroidManifest.xml裡註冊service哈
我們藉助WindowManager來生成懸浮窗
WindowManager的三個最常用方法為:
addView 新增View
addView(View view, WindowManager.LayoutParams params);
View就是要新增到windowmanager中的物件,而params是視窗的設定引數,這個我們講到程式碼階段再說。
removeView 移除View removeView(View view); 從windowmanager中移除物件。
updateViewLayout重新整理View updateViewLayout(View view,
ViewGroup.LayoutParams params); 也是兩個引數,一個View一個params,參考addView。
懸浮窗的佈局就是通過addView新增、懸浮窗更改位置通過updateViewLayout進行重新整理、關閉懸浮窗時呼叫removeView。
我們需要申請許可權
懸浮窗需要在別的應用之上顯示控制元件,很顯然,這需要某些許可權才可以。
在API Level >= 23的時候,需要在AndroidManefest.xml檔案中宣告許可權SYSTEM_ALERT_WINDOW才能在其他應用上繪製控制元件。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
除了這個許可權外,我們還需要在系統設定裡面對本應用進行設定懸浮窗許可權。該許可權在應用中需要啟動Settings.ACTION_MANAGE_OVERLAY_PERMISSION來讓使用者手動設定許可權。
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), REQUEST_CODE);
視窗型別很重要
LayoutParam裡的type變數。這個變數是用來指定視窗型別的。在設定這個變數時,需要注意一個坑,那就是需要對不同版本的Android系統進行適配。
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之前,懸浮視窗設定可以為TYPE_PHONE,這種型別是用於提供使用者互動操作的非應用視窗。
而Android 8.0對系統和API行為做了修改,如果需要實現在其他應用和視窗上方顯示提醒視窗,那麼必須為TYPE_APPLICATION_OVERLAY的型別。
具體程式碼分析
layout
這裡實現3種懸浮窗:懸浮Button(java程式碼直接實現,不寫入layout),懸浮imagView,懸浮surfaceView
activity_main.xml就在主活動排3個按鈕,點選按鈕開啟對應的懸浮窗
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="start floating button"
android:onClick="startFloatingButtonService" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="start floating image display"
android:onClick="startFloatingImageDisplayService"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="start floating video player"
android:onClick="startFloatingVideoService"/>
</LinearLayout>
image_display.xml是imgeView型別的懸浮窗
<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>
video_display.xml是surfaceView型別的懸浮窗
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/video_display_surfaceview"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
如果使用FrameLayout就可以在懸浮窗上面加控制元件了Ho~
在啟動服務之前,需要在活動裡先判斷一下當前是否允許開啟懸浮窗。
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 0) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "授權失敗", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "授權成功", Toast.LENGTH_SHORT).show();
startService(new Intent(MainActivity.this, FloatingButtonService.class));
}
} else if (requestCode == 1) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "授權失敗", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "授權成功", Toast.LENGTH_SHORT).show();
startService(new Intent(MainActivity.this, FloatingImageDisplayService.class));
}
} else if (requestCode == 2) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "授權失敗", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "授權成功", Toast.LENGTH_SHORT).show();
startService(new Intent(MainActivity.this, FloatingVideoService.class));
}
}
}
public void startFloatingButtonService(View view) {
if (FloatingButtonService.isStarted) {
return;
}
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "當前無許可權,請授權", Toast.LENGTH_SHORT);
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 0);
} else {
startService(new Intent(MainActivity.this, FloatingButtonService.class));
}
}
public void startFloatingImageDisplayService(View view) {
if (FloatingImageDisplayService.isStarted) {
return;
}
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "當前無許可權,請授權", Toast.LENGTH_SHORT);
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 1);
} else {
startService(new Intent(MainActivity.this, FloatingImageDisplayService.class));
}
}
public void startFloatingVideoService(View view) {
if (FloatingVideoService.isStarted) {
return;
}
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "當前無許可權,請授權", Toast.LENGTH_SHORT);
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 2);
} else {
startService(new Intent(MainActivity.this, FloatingVideoService.class));
}
}
}
懸浮Button的service
FloatingButtonService.java
public class FloatingButtonService extends Service {
public static boolean isStarted = false;
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private Button button;
@Override
public void onCreate() {
super.onCreate();
isStarted = true;
// 獲取WindowManager服務
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
// 設定LayoutParam
layoutParams = new WindowManager.LayoutParams();
//設定type
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.gravity = Gravity.LEFT | Gravity.TOP;
//設定flags.不可聚焦及不可使用按鈕對懸浮窗進行操控.
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
//設定懸浮視窗長寬資料.
//注意,這裡的width和height均使用px而非dp
//如果你想完全對應佈局設定,需要先獲取到機器的dpi
//px與dp的換算為px = dp * (dpi / 160).
layoutParams.width = 500;
layoutParams.height = 100;
layoutParams.x = 300;
layoutParams.y = 300;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
showFloatingWindow();
return super.onStartCommand(intent, flags, startId);
}
private void showFloatingWindow() {
if (Settings.canDrawOverlays(this)) {
// 新建懸浮窗控制元件
button = new Button(getApplicationContext());
button.setText("Floating Window");
button.setBackgroundColor(Color.BLUE);
// 將懸浮窗控制元件新增到WindowManager
windowManager.addView(button, layoutParams);
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;
}
}
}
format 用於設定顯示的格式。RGBA_8888是透明型,也是最常用到的。
flags 這是很重要的一個設定。FLAG_NOT_FOCUSABLE設定了不可聚焦。同時經常用的還有FLAG_WATCH_OUTSIDE_TOUCH,這個設定可以讓懸浮窗接收到外部點選事件,如果你想在之後做小懸浮窗點選變大,再點選懸浮窗之外又變回小懸浮窗。這個可以用到。多個FLAG的話可以用|來連線,如
params.flags = FLAG_NOT_FOCUSABLE | FLAG_WATCH_OUTSIDE_TOUCH
gravity 用於設定視窗的初始停靠位置。我們設定的是讓它初始在最左&最上方生成,後面兩句是定義這裡的xy值都為0。
width&height用於設定懸浮視窗的大小,建議設定成和佈局一樣大最好。小於佈局會把裡面的元件進行擠壓。
接下來是懸浮ImagView的service了,它還可以每隔兩秒更換一張圖片
FloatingImageDisplayService.java
public class FloatingImageDisplayService extends Service {
public static boolean isStarted = false;
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private View displayView;
private int[] images;
private int imageIndex = 0;
private Handler changeImageHandler;
@Override
public void onCreate() {//建立服務
super.onCreate();
isStarted = true;
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
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.gravity = Gravity.LEFT | Gravity.TOP;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.width = 500;
layoutParams.height = 500;
layoutParams.x = 300;
layoutParams.y = 300;
images = new int[] {
R.drawable.image_01,
R.drawable.image_02,
R.drawable.image_03,
R.drawable.image_04,
R.drawable.image_05,
};
//藉助Handler定時傳遞訊息機制來實現定時切換圖片的機制
changeImageHandler = new Handler(this.getMainLooper(),changeImageCallback);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
showFloatingWindow();//啟動服務
return super.onStartCommand(intent, flags, startId);
}
private void showFloatingWindow() {
if (Settings.canDrawOverlays(this)) { //如果允許了懸浮窗
LayoutInflater layoutInflater = LayoutInflater.from(this);//獲取LayoutInflater物件
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);
changeImageHandler.sendEmptyMessageDelayed(0, 2000);//第一次延遲2秒傳送空訊息,編號為0
}
}
private Handler.Callback changeImageCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == 0) {//Handler接收訊息判斷
imageIndex++;
if (imageIndex >= 5) {
imageIndex = 0;
}
if (displayView != null) {
((ImageView) displayView.findViewById(R.id.image_display_imageview)).setImageResource(images[imageIndex]);//更新UI圖
}
changeImageHandler.sendEmptyMessageDelayed(0, 2000);//圖換完了,再發一條訊息
}
return false;//否則沒有接受訊息
}
};
//懸浮窗拖動操作部分程式碼完全不變
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;
}
}
}
最後是SurfaceView的service了,我們用它來實現播放小視訊
大同小異
public class FloatingVideoService extends Service {
public static boolean isStarted = false;
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private MediaPlayer mediaPlayer;
private View displayView;
@Override
public void onCreate() {
super.onCreate();
isStarted = true;
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
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.gravity = Gravity.LEFT | Gravity.TOP;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.width = 800;
layoutParams.height = 450;
layoutParams.x = 300;
layoutParams.y = 300;
mediaPlayer = new MediaPlayer();//建立媒體播放器物件
}
@Nullable//表示可以為空
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
showFloatingWindow();
return super.onStartCommand(intent, flags, startId);
}
private void showFloatingWindow() {
if (Settings.canDrawOverlays(this)) {
LayoutInflater layoutInflater = LayoutInflater.from(this);
displayView = layoutInflater.inflate(R.layout.video_display, null);
displayView.setOnTouchListener(new FloatingOnTouchListener());
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);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
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, "無法開啟視訊源", Toast.LENGTH_LONG).show();
}
windowManager.addView(displayView, layoutParams);
}
}
//拖動懸浮窗部分程式碼 完全不變
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 true;
}
}
}
相關文章
- Android 懸浮窗Android
- 懸浮窗的一種實現 | Android懸浮窗Window應用Android
- Android 懸浮窗 System Alert WindowAndroid
- Android仿微信文章懸浮窗效果Android
- Android 攝像頭預覽懸浮窗Android
- 【轉載】使用WindowManage實現Android懸浮窗Android
- Android 輔助許可權與懸浮窗Android
- Android中的懸浮框Android
- Android懸浮框的實現Android
- 下沉式通知的一種實現 | Android懸浮窗Window應用Android
- Android:會呼吸的懸浮氣泡Android
- QPM 之懸浮窗設定資訊
- QPM 之懸浮窗助力效能優化優化
- 懸浮窗開發設計實踐
- 小米 TYPE_TOAST 懸浮窗無效的原因AST
- Android懸浮框的適配問題Android
- 直播平臺製作,Android 懸浮窗延時5秒返回APP問題AndroidAPP
- Android通過WindowManager實現懸浮框Android
- 百度地圖新增懸浮窗搜尋功能地圖
- 直播原始碼,懸浮窗滾動漸變色效果原始碼
- Android懸浮窗怎麼簡單實現?這樣用 kotlin編寫輕鬆搞定!AndroidKotlin
- Android 攝像頭預覽懸浮窗,可拖動,可顯示在其他app上方AndroidAPP
- [轉]Android輕鬆實現RecyclerView懸浮條AndroidView
- 記一次懸浮窗的上線以及坑點總結
- 如何獲取Vivo系統的懸浮窗許可權狀態
- Android RecyclerView實現頭部懸浮吸頂效果AndroidView
- 非侵入式無許可權應用內懸浮窗的實現
- ALaunch 0.8 := 懸浮提示
- 直播軟體原始碼,設定懸浮窗並可進行任意位置的移動原始碼
- Fluid Browser for Mac(多工懸浮透明視窗瀏覽器)1.6啟用版UIMac瀏覽器
- 電商直播主圖設計素材,輕鬆搞定直播懸浮窗設計!
- 短視訊系統原始碼,關於懸浮窗的縮放、拖動等應用原始碼
- 直播app開發,推出語音聊天室時保持懸浮窗存在狀態APP
- 底部導航欄懸浮效果
- css做個波浪懸浮球?CSS
- 短視訊app原始碼,連麥時最小化出現可移動懸浮窗APP原始碼
- jQuery滑鼠懸浮翻牌效果詳解jQuery
- jQuery滑鼠懸浮聚焦效果詳解jQuery