我们原有的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;
}
布局文件:
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 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 rti = mActivityManager.getRunningTasks(1);
if (rti.get(0).topActivity.getPackageName().contains("你的应用包名")) {
return true;
} else {
return false;
}
}
}
/**
* 获得属于桌面的应用的应用包名称
*
* @return 返回包含所有包名的字符串列表
*/
private List getHomes() {
List names = new ArrayList();
PackageManager packageManager = this.getPackageManager();
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
List resolveInfo = packageManager.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo ri : resolveInfo) {
names.add(ri.activityInfo.packageName);
}
return names;
}
}
关于服务,我们在这里开启了一个计时器,每过一段时间都来检测一下当前页面是否需要展示/隐藏悬浮按钮,我们的逻辑是当回退到桌面,或者不在当前应用内时隐藏悬浮按钮。
好了,接下来我们就需要在activity中开启服务了。但是在开启服务之前,我们还要添加一下悬浮窗的权限:
这个权限属于危险权限,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!!
}
好了,到这里为止,我们的悬浮按钮已经成型了。
终于,产品露出了欣慰的笑容。
---------------------------------------
非常感谢郭神的文章 救我狗命!
附上项目github传送门