我們原有的APP中有視頻播放以及投屏的功能,但是投屏只在當前頁面起效,一旦退出,投屏就自動失效了。偏偏產品喜歡研究別人家的app,研究了一波之後,對我發出了直擊靈魂的疑問:“爲什麼人家騰訊視頻在投屏的時候有個懸浮按鈕?”,"爲什麼人家優酷在投屏的時候有全局懸浮按鈕?"
產品指着騰訊視頻,終於露出了獠牙:“啊,我不管,我要這個!你要給我做!”。我的內心毫無波動,甚至還有點。。。
哎,好吧,做。
那我們就先來研究一下這個懸浮按鈕吧。
如果要在單個Activity內實現一個懸浮按鈕,只要你是個Android開發就會做了,那全局的懸浮按鈕是不是就是要在左右的Activity中都來做一個這樣的按鈕呢?
這種思路不是說不可以,但是,太累了,不僅要在左右的activity里加,還要再fragment裏面加。
於是,我們就直接把view添加到widnow上,這樣就可以掙脫activity和fragment的束縛。那要怎麼來添加呢?這就要藉助windowmanager了,從名字上就可以看出,windowmanage時window的管理類,它本身包含3個方法,分別對應着增加view,刪除view,更新view。
那具體要怎麼使用呢?
1.自定義懸浮view 主要用來處理拖動事件,我們的view裏面只放了一個圖片
public class FloatView extends LinearLayout {
/**
* 系統狀態欄高度
*/
private static int statusBarHeight;
/**
* 窗口管理
*/
private WindowManager windowManager;
private WindowManager.LayoutParams layoutParams;
/**
* 點擊事件
* @param context
*/
private OnFloatViewClickListener clickListener;
public void setClickListener(OnFloatViewClickListener clickListener) {
this.clickListener = clickListener;
}
public FloatView(Context context) {
this(context, null);
}
public FloatView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public FloatView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
LayoutInflater.from(context).inflate(R.layout.window_float_layout, this);
}
public void setLayoutParams(WindowManager.LayoutParams layoutParams) {
this.layoutParams = layoutParams;
}
private void updateWindow() {
layoutParams.x = (int) (xInScreen-xViewScreen);
layoutParams.y = (int) (yInScreen-yViewScreen);
windowManager.updateViewLayout(this,layoutParams);
}
/**
* 點擊事件
*/
public void onClick(){
if (clickListener!=null){
clickListener.onClick();
}
}
/**
* 獲取狀態欄高度
*
* @return
*/
private int getStatusBarHeight() {
if (statusBarHeight == 0) {
try {
Class<?> c = Class.forName("com.android.internal.R$dimen");
Object o = c.newInstance();
Field field = c.getField("status_bar_height");
int x = (Integer) field.get(o);
statusBarHeight = getResources().getDimensionPixelSize(x);
} catch (Exception e) {
e.printStackTrace();
}
}
return statusBarHeight;
}
佈局文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/dp_100"
android:layout_height="@dimen/dp_100">
<ImageView
android:id="@+id/iv_float"
android:layout_width="@dimen/dp_50"
android:layout_gravity="center"
android:layout_height="@dimen/dp_50"
android:src="@drawable/ic_launcher_background" />
</FrameLayout>
2.定義懸浮按鈕的管理類,來統一管理懸浮按鈕。
public class FloatWindowManager {
public static FloatView floatView;
public static LayoutParams floatParams;
private static WindowManager windowManager;
private OnFloatViewClickListener clickListener;
public static void creatFloatWindow(Context context, OnFloatViewClickListener listener) {
windowManager = getWindowManager(context);
if (floatView == null) {
floatView = new FloatView(context);
if (floatParams == null) {
floatParams = new LayoutParams();
floatParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
floatParams.gravity = Gravity.RIGHT | Gravity.BOTTOM;
floatParams.x = 15;
floatParams.y = 170;
floatParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL
| LayoutParams.FLAG_NOT_FOCUSABLE;
// 不加這一句,會出現懸浮按鈕有黑邊框
floatParams.format = PixelFormat.RGBA_8888;
floatParams.width = LayoutParams.WRAP_CONTENT;
floatParams.height = LayoutParams.WRAP_CONTENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
floatParams.type = LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
floatParams.type = LayoutParams.TYPE_SYSTEM_ALERT;
}
}
floatView.setLayoutParams(floatParams);
floatView.setClickListener(listener);
windowManager.addView(floatView, floatParams);
}
}
/**
* 移除懸浮窗
*
* @param context
*/
public static void removeFloatView(Context context) {
if (floatView != null) {
WindowManager windowManager = getWindowManager(context);
windowManager.removeView(floatView);
floatView = null;
}
}
/**
* 如果WindowManager還未創建,則創建一個新的WindowManager返回。否則返回當前已創建的WindowManager。
*
* @param context 必須爲應用程序的Context.
* @return WindowManager的實例,用於控制在屏幕上添加或移除懸浮窗。
*/
private static WindowManager getWindowManager(Context context) {
if (windowManager == null) {
windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
}
return windowManager;
}
/**
* 是否有懸浮框
*
* @return
*/
public static boolean hasFloatWindow() {
return floatView == null ? false : true;
}
}
請注意代碼中的註釋,因爲我在開發中就因爲這一句話頭疼了好久。
3.寫一個Service,當啓動service時,開啓懸浮按鈕,關閉服務時,關閉懸浮按鈕。
public class FloatScreenService extends Service {
public FloatScreenService() {
}
private Handler mHandler = new Handler();
private Timer timer;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (timer == null) {
timer = new Timer();
timer.scheduleAtFixedRate(new ResreshWindow(), 0, 500);
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
timer.cancel();
timer = null;
// 關閉服務時,銷燬懸浮框
// mHandler.post(new Runnable() {
// @Override
// public void run() {
// FloatWindowManager.removeFloatView(getApplicationContext());
// }
// });
}
class ResreshWindow extends TimerTask {
@Override
public void run() {
/**
* 根據不同頁面來判斷是否顯示懸浮按鈕
*
* 桌面,投屏頁 不顯示
*
*/
if (!isShowWindow() && FloatWindowManager.hasFloatWindow()) {//不應該展示懸浮按鈕,但是有了懸浮按鈕 需要隱藏按鈕
mHandler.post(new Runnable() {
@Override
public void run() {
FloatWindowManager.removeFloatView(getApplicationContext());
}
});
} else if (isShowWindow() && !FloatWindowManager.hasFloatWindow()) {//應該展示懸浮窗,但無懸浮窗。需要增加
mHandler.post(new Runnable() {
@Override
public void run() {
FloatWindowManager.creatFloatWindow(getApplicationContext(), new OnFloatViewClickListener() {
@Override
public void onClick() {
Intent it = new Intent(FloatScreenService.this, SecondActivity.class);
it.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(it);
}
});
}
});
}
}
}
/**
* 判斷當前界面是否是桌面
*/
private boolean isHome() {
ActivityManager mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> rti = mActivityManager.getRunningTasks(1);
return getHomes().contains(rti.get(0).topActivity.getPackageName());
}
/**
* 判斷是否要顯示懸浮按鈕
*
* @return true 要顯示 false 隱藏
*/
private boolean isShowWindow() {
if (isHome()) {
return false;
} else {
ActivityManager mActivityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> rti = mActivityManager.getRunningTasks(1);
if (rti.get(0).topActivity.getPackageName().contains("你的應用包名")) {
return true;
} else {
return false;
}
}
}
/**
* 獲得屬於桌面的應用的應用包名稱
*
* @return 返回包含所有包名的字符串列表
*/
private List<String> getHomes() {
List<String> names = new ArrayList<String>();
PackageManager packageManager = this.getPackageManager();
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo ri : resolveInfo) {
names.add(ri.activityInfo.packageName);
}
return names;
}
}
關於服務,我們在這裏開啓了一個計時器,每過一段時間都來檢測一下當前頁面是否需要展示/隱藏懸浮按鈕,我們的邏輯是當回退到桌面,或者不在當前應用內時隱藏懸浮按鈕。
好了,接下來我們就需要在activity中開啓服務了。但是在開啓服務之前,我們還要添加一下懸浮窗的權限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
這個權限屬於危險權限,6.0以上需要來動態申請。然而有意思的是,這個權限的判斷有個特別的API來判斷,而6.0以下則直接開啓服務即可:
fun stopFloat() {
//有權限,開啓服務
var ser: Intent = Intent()
ser.setClass(this, FloatScreenService::class.java)
stopService(ser)
}
fun startFloat() {
// 1 判斷權限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// var b = Settings.canDrawOverlays(this)
var b = commonROMPermissionCheck(this)
if (b) {
startWindowService()
} else {
toSettingPage()
}
} else {
startWindowService()
}
}
var mHandler:Handler = Handler()
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 1) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mHandler.postDelayed(Runnable {
if (commonROMPermissionCheck(this))
startWindowService()
},500)
}
}
}
fun startWindowService() {
//有權限,開啓服務
var ser: Intent = Intent()
ser.setClass(this, FloatScreenService::class.java)
startService(ser)
}
/**
* 進入設置頁面,獲取權限
*/
fun toSettingPage() {
var settings: Intent = Intent()
settings.setAction(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
settings.setData(Uri.parse("package:" + packageName))
startActivityForResult(settings, 1)
}
/**
* 判斷懸浮窗權限
*/
private fun commonROMPermissionCheck(context: Context): Boolean {
var result: Boolean? = true
if (Build.VERSION.SDK_INT >= 23) {
try {
val clazz = Settings::class.java
val canDrawOverlays = clazz.getDeclaredMethod("canDrawOverlays", Context::class.java)
// Settings.canDrawOverlays(context)
result = canDrawOverlays.invoke(null, context) as Boolean
} catch (e: Exception) {
}
}
return result!!
}
好了,到這裏爲止,我們的懸浮按鈕已經成型了。
終於,產品露出了欣慰的笑容。
---------------------------------------
非常感謝郭神的文章 救我狗命!