目錄
概述
原理
Android的界面繪制,都是通過 WindowManager 的服務來實現的。 WindowManager 實現了 ViewManager 接口,可以通過獲取 WINDOW_SERVICE 系統服務得到。而 ViewManager 接口有 addView 方法,我們就是通過這個方法將懸浮窗控件加入到屏幕中去。
為了讓懸浮窗與Activity
脫離,使其在應用處於后台時懸浮窗仍然可以正常運行,使用Service
來啟動懸浮窗並做為其背后邏輯支撐。
權限
在 API Level >= 23 的時候,需要在AndroidManefest.xml文件中聲明權限 SYSTEM_ALERT_WINDOW 才能在其他應用上繪制控件。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
除了這個權限外,我們還需要在系統設置里面對本應用進行設置懸浮窗權限。該權限在應用中需要啟動 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 來讓用戶手動設置權限。
if (!Settings.canDrawOverlays(this)) {
showError("當前無權限,請授權");
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)) {
showError("授權失敗");
} else {
showMsg("授權成功");
startService(new Intent(MainActivity.this, FloatingService.class));
}
}
}
LayoutParam
WindowManager 的 addView 方法有兩個參數,一個是需要加入的控件對象,另一個參數是 WindowManager.LayoutParam 對象。
這里需要着重說明的是 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;
}
實例
AndroidManifest.xml
添加權限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
添加Service組件
<service android:name=".MediaFloatService"/>
MyApplication
public class PubApplication extends Application {
//設置一個全局變量來判斷懸浮窗是否已經開啟
public static Boolean isMediaFloatShow = false;
}
MediaFloatService
public class MediaFloatService extends Service {
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private View mView;
private ViewFloatMediaBinding floatView;
private ArrayList<String> lstFilePaths;
private int currentIndex;
private int screenWidth;
private int screenHeight;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//加載窗口布局
initWindow(intent);
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
//移除窗口布局
windowManager.removeView(mView);
super.onDestroy();
}
}
加載窗口布局
private void initWindow(Intent intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(this)) {
//獲取WindowManager服務
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
//取得屏幕尺寸
DisplayMetrics dm = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(dm);
screenWidth = dm.widthPixels;
screenHeight = dm.heightPixels;
//設置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.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_FULLSCREEN
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
layoutParams.format = PixelFormat.RGBA_8888; //背景透明效果
layoutParams.width = 512; //懸浮窗口長寬值,單位為 px 而非 dp
layoutParams.height = 450;
layoutParams.gravity = 51; //想要x,y生效,一定要指定Gravity為top和left //Gravity.TOP | Gravity.LEFT
layoutParams.x = 100; //啟動位置
layoutParams.y = 100;
//加載懸浮窗布局
floatView = ViewFloatMediaBinding.inflate(LayoutInflater.from(MediaFloatService.this));
mView = floatView.getRoot();
//mView.setAlpha((float) 0.9);
//設定懸浮窗控件
floatView.ivFloatMediaClose.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
MyApplication.isMediaFloatShow = false;
//切記添加關閉服務按鈕事件,調用 stopSelf() 方法以關閉懸浮窗。
stopSelf();
}
});
//接收傳值
Bundle bundle = intent.getExtras();
lstFilePaths = bundle.getStringArrayList("lstFilePaths");
currentIndex = bundle.getInt("currentIndex");
floatView.ivFloatMediaPrev.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//判斷是否是第一個,如果是則跳到最后循環播放
if (currentIndex == 0) currentIndex = lstFilePaths.size() - 1;
else currentIndex = currentIndex - 1;
showImage();
//Glide.with(MediaFloatService.this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
}
});
floatView.ivFloatMediaNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//判斷是否是最后一個,如果是則跳到最前循環播放
if (currentIndex == lstFilePaths.size() - 1) currentIndex = 0;
else currentIndex = currentIndex + 1;
showImage();
//Glide.with(MediaFloatService.this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
}
});
BitmapFactory.Options options = getBitmapOptions(lstFilePaths.get(currentIndex));
layoutParams.width = Math.min(options.outWidth, screenWidth); //Math.min取得兩個數據中的最小值
layoutParams.height = Math.min(options.outHeight, screenHeight);
Glide.with(this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
//單擊事件是否顯示控制按鈕
floatView.ivFloatMediaShow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (floatView.grpFloatMediaControl.getVisibility() == View.INVISIBLE)
floatView.grpFloatMediaControl.setVisibility(View.VISIBLE);
else floatView.grpFloatMediaControl.setVisibility(View.INVISIBLE);
}
});
//提交布局
windowManager.addView(mView, layoutParams);
}
}
}
取得屏幕尺寸
DisplayMetrics dm = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(dm);
screenWidth = dm.widthPixels;
screenHeight = dm.heightPixels;
根據路徑取得圖片尺寸
private BitmapFactory.Options getBitmapOptions(String filepath) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filepath, options);
return options;
}
變更顯示圖片
變更圖片時懸浮窗口也要根據變更后的圖片再做調整,不然窗口大小還是上一圖片的尺寸比例
private void showImage() {
BitmapFactory.Options options = getBitmapOptions(lstFilePaths.get(currentIndex));
layoutParams.width = Math.min(options.outWidth, screenWidth); //Math.min取得兩個數據中的最小值
layoutParams.height = Math.min(options.outHeight, screenHeight);
Glide.with(this).load(lstFilePaths.get(currentIndex)).into(floatView.ivFloatMediaShow);
windowManager.updateViewLayout(mView, layoutParams);
}
窗口拖動與縮放
窗口拖動
floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
private int dX;
private int dY;
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
dX = (int) motionEvent.getRawX();
dY = (int) motionEvent.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int nX = (int) motionEvent.getRawX();
int nY = (int) motionEvent.getRawY();
int cW = nX - dX;
int cH = nY - dY;
dX = nX;
dY = nY;
layoutParams.x = layoutParams.x + cW;
layoutParams.y = layoutParams.y + cH;
windowManager.updateViewLayout(mView, layoutParams);
break;
default:
break;
}
return true;
}
});
單擊雙擊
floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
private int sX;
private int sY;
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
sX = (int) motionEvent.getRawX();
sY = (int) motionEvent.getRawY();
break;
case MotionEvent.ACTION_UP:
//如果抬起時的位置和按下時的位置大致相同視作單擊事件
int nX2 = (int) motionEvent.getRawX();
int nY2 = (int) motionEvent.getRawY();
int cW2 = nX2 - sX;
int cH2 = nY2 - sY;
//間隔值可能為負值,所以要取絕對值進行比較
if (Math.abs(cW2) < 3 && Math.abs(cH2) < 3) view.performClick();
break;
default:
break;
}
return true;
}
});
雙指縮放
floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
private ScaleGestureDetector scaleGestureDetector = new ScaleGestureDetector(MediaFloatService.this, new myScale());
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
isP2Down = true; //雙指按下
break;
case MotionEvent.ACTION_MOVE:
if (motionEvent.getPointerCount() == 2) {
//雙指縮放
scaleGestureDetector.onTouchEvent(motionEvent);
}
break;
case MotionEvent.ACTION_UP:
isP2Down = false; //雙指抬起
break;
default:
break;
}
return true;
}
});
ScaleGestureDetector
private float initSapcing = 0;
private int initWidth;
private int initHeight;
private int initX;
private int initY;
private boolean isP2Down = false;
private class myScale extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
// detector.getCurrentSpan();//兩點間的距離跨度
// detector.getCurrentSpanX();//兩點間的x距離
// detector.getCurrentSpanY();//兩點間的y距離
// detector.getFocusX(); //
// detector.getFocusY(); //
// detector.getPreviousSpan(); //上次
// detector.getPreviousSpanX();//上次
// detector.getPreviousSpanY();//上次
// detector.getEventTime(); //當前事件的事件
// detector.getTimeDelta(); //兩次事件間的時間差
// detector.getScaleFactor(); //與上次事件相比,得到的比例因子
if (isP2Down) {
//雙指按下時最初間距
initSapcing = detector.getCurrentSpan();
//雙指按下時窗口大小
initWidth = layoutParams.width;
initHeight = layoutParams.height;
//雙指按下時窗口左頂點位置
initX = layoutParams.x;
initY = layoutParams.y;
isP2Down = false;
}
float scale = detector.getCurrentSpan() / initSapcing; //取得縮放比
int newWidth = (int) (initWidth * scale);
int newHeight = (int) (initHeight * scale);
//判斷窗口縮放后是否超出屏幕大小
if (newWidth < screenWidth && newHeight < screenHeight) {
layoutParams.width = newWidth;
layoutParams.height = newHeight;
layoutParams.x = initX;
layoutParams.y = initY;
//縮放后圖片會失真重新載入圖片
Glide.with(MediaFloatService.this)
.load(lstFilePaths.get(currentIndex))
.into(floatView.ivFloatMediaShow);
//提交更新布局
windowManager.updateViewLayout(mView, layoutParams);
}
return true;
//return super.onScale(detector);
}
}
完整代碼
floatView.ivFloatMediaShow.setOnTouchListener(new View.OnTouchListener() {
private int dX;
private int dY;
private int sX;
private int sY;
private ScaleGestureDetector scaleGestureDetector = new ScaleGestureDetector(MediaFloatService.this, new myScale());
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
dX = (int) motionEvent.getRawX();
dY = (int) motionEvent.getRawY();
sX = (int) motionEvent.getRawX();
sY = (int) motionEvent.getRawY();
isP2Down = true; //雙指按下
break;
case MotionEvent.ACTION_MOVE:
if (motionEvent.getPointerCount() == 1) {
//單指拖動
int nX = (int) motionEvent.getRawX();
int nY = (int) motionEvent.getRawY();
int cW = nX - dX;
int cH = nY - dY;
dX = nX;
dY = nY;
layoutParams.x = layoutParams.x + cW;
layoutParams.y = layoutParams.y + cH;
windowManager.updateViewLayout(mView, layoutParams);
} else if (motionEvent.getPointerCount() == 2) {
//雙指縮放
scaleGestureDetector.onTouchEvent(motionEvent);
}
break;
case MotionEvent.ACTION_UP:
isP2Down = false; //雙指抬起
//如果抬起時的位置和按下時的位置大致相同視作單擊事件
int nX2 = (int) motionEvent.getRawX();
int nY2 = (int) motionEvent.getRawY();
int cW2 = nX2 - sX;
int cH2 = nY2 - sY;
//間隔值可能為負值,所以要取絕對值進行比較
if (Math.abs(cW2) < 3 && Math.abs(cH2) < 3) view.performClick();
break;
default:
break;
}
return true;
}
});
實例2
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
private View floatView;
if(Settings.canDrawOverlays(this)){
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.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.format = PixelFormat.RGBA_8888; //背景透明效果
layoutParams.width = 512; //懸浮窗口長寬值,單位為 px 而非 dp
layoutParams.height = 450;
layoutParams.gravity = Gravity.TOP | Gravity.LEFT; //啟動位置
layoutParams.x = 100;
layoutParams.y = 100;
floatView = LayoutInflater.from(this).inflate(R.layout.note_float_view, null);
floatView.setAlpha((float) 0.9);
windowManager.addView(floatView, layoutParams);
}
常見問題
起始位置設置無效
想要x,y生效,一定要指定Gravity為top和left,想要居中就設置為 Center
layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
layoutParams.x = 100; //啟動位置
layoutParams.y = 100;
獲取狀態欄高度
private int statusBarHeight = -1;
//獲取狀態欄高度
private void getStatusBarHeight(){
mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if(resourceId > 0){
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
}
}