Android悬浮窗视频
类似微信视频通话需求。
思路:
1.申请悬浮窗权限
2.windowManager实现悬浮窗;
3.moveToback退出全屏,显示悬浮窗;
当用户正在NewsActivity看文字,视频通话来了,接听(VideoActivity),然后缩至悬浮窗,此时应回到NewsActivity,悬浮窗出现时该如何回到电话前的页面?finish掉VideoActivity吗?finish后自然回退到栈内上一个Activity,页面逻辑上符合需求,但是VideoActivity中写了很多通话逻辑,销毁不太好,怎么办?(有人说,为何会在页面写逻辑,明显是没抽象好嘛,这都是后话,不解决当前版本问题)
于是想到干脆起两个任务栈,一个单独放VideoActivity,另一个放剩余的Activity,然后点击要悬浮时,直接后置VideoActivity所在任务栈即可。
实现两个任务栈也很简单,只要给VideoActivity设置android:launchMode="singleInstance"即可。
想要退后该任务栈直接调用 moveTaskToBack(true);
然而singleInstance的坑不是一般的多。
一 . 首先 singleInstance会导致onRequestPermissionsResult( ),onActivityResult( )不回调。可是视频必须要申请RECORD_AUDIO,CAMERA,WRITE_EXTERNAL_STORAGE权限,以及悬浮窗ACTION_MANAGE_OVERLAY_PERMISSION。
无奈写个视频的前置页面标准启动模式,专门用来申请相机,录音,读写等权限,全部授权后 再真正进入singleInstance页面。
问题一算是迎刃而解。(躲过去了)
二. 长按home或者菜单键 查看Android最近任务列表时,一个APP竟然两个最近任务,解决方案:需要添加一个属性 保证 不在最近任务列表中显示当前activity所在的应用
android:launchMode= "singleInstance" //开启新的应用任务栈
android:excludeFromRecents= "true" //不在最近任务列表中显示当前activity所在的应用
三 选择TYPE_TOAST,如果期间有toast弹出,在android7.1.1会崩溃,加上版本判断吧。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
下面是悬浮窗的实现
public class FloatVideoWindowService extends Service {
private WindowManager mWindowManager;
private WindowManager.LayoutParams wmParams;
private View mFloatingLayout;
private RelativeLayout mPreviewLayout;
private TutorialsManager mInstance = null;
private SurfaceView remoteView = null;
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}
public class MyBinder extends Binder {
// public FloatVideoWindowService getService() {
// return FloatVideoWindowService.this;
// }
}
@Override
public void onCreate() {
super.onCreate();
initWindow();//设置悬浮窗基本参数(位置、宽高等)
initFloating();//悬浮框点击事件的处理
mInstance = TutorialsManager.getInstance(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
int uid = intent.getIntExtra(VideoChatViewActivity.KEY_C_USER_ID, -1);
if (uid != -1) {
remoteView = mInstance.getRemoteVideo(uid);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
remoteView.setLayoutParams(lp);
mPreviewLayout.addView(remoteView);
} else {
stopSelf();
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
if (null != mPreviewLayout) {
mPreviewLayout.removeAllViews();
}
if (mFloatingLayout != null) {
// 移除悬浮窗口
mWindowManager.removeView(mFloatingLayout);
}
}
private void initWindow() {
mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
wmParams = getParams();
// 悬浮窗默认显示以左上角为起始坐标
wmParams.gravity = Gravity.RIGHT | Gravity.TOP;
//悬浮窗的开始位置,因为设置的是从左上角开始,所以屏幕左上角是x=0;y=0
wmParams.x = DensityUtil.dip2px(10);
wmParams.y = 110;
// 获取浮动窗口视图所在布局
mFloatingLayout = LayoutInflater.from(getApplicationContext()).inflate(R.layout.view_videochat_services_float_layout, null);
// 添加悬浮窗的视图
mWindowManager.addView(mFloatingLayout, wmParams);
}
private WindowManager.LayoutParams getParams() {
wmParams = new WindowManager.LayoutParams();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
//设置可以显示在状态栏上
wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams
.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR |
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
int fullCropWidth = DeviceUtils.getScreenWidth();
int cropHeight = fullCropWidth * 16 / 9;
//设置悬浮窗口长宽数据
wmParams.width = (int) (fullCropWidth * 0.26);
wmParams.height = (int) (cropHeight * 0.26);
return wmParams;
}
private void initFloating() {
mPreviewLayout = mFloatingLayout.findViewById(R.id.small_size_preview);
//悬浮框点击事件
mPreviewLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FloatVideoWindowService.this, VideoChatViewActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
mPreviewLayout.removeView(remoteView);
stopSelf();
}
});
//悬浮框触摸事件,设置悬浮框可拖动
mPreviewLayout.setOnTouchListener(new FloatingListener());
}
private int mTouchStartX;
private int mTouchStartY;
private int mStartX;
private int mStartY;
private boolean isMove;
private class FloatingListener implements View.OnTouchListener {
int slop = 1;//滑动距离,区分点击
public FloatingListener() {
slop = ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop();
}
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
isMove = false;
mTouchStartX = (int) event.getRawX();
mTouchStartY = (int) event.getRawY();
mStartX = (int) event.getX();
mStartY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
int mTouchCurrentX = (int) event.getRawX();
int mTouchCurrentY = (int) event.getRawY();
wmParams.x -= mTouchCurrentX - mTouchStartX;
wmParams.y += mTouchCurrentY - mTouchStartY;
mWindowManager.updateViewLayout(mFloatingLayout, wmParams);
mTouchStartX = mTouchCurrentX;
mTouchStartY = mTouchCurrentY;
break;
case MotionEvent.ACTION_UP:
int mStopX = (int) event.getX();
int mStopY = (int) event.getY();
if (Math.abs(mStartX - mStopX) >= slop || Math.abs(mStartY - mStopY) >= slop) {
isMove = true;
}
break;
default:
break;
}
//如果是移动事件不触发OnClick事件,防止移动的时候一放手形成点击事件
return isMove;
}
}
}
其中VideoActivity中代码:
private int PermissionRequestCode = 10;
private void onClickFloatBtn() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), PermissionRequestCode);
} else {
showFloatView();
}
}
private void showFloatView() {
if (mUid != -1) {
moveTaskToBack(true);
Intent intent = new Intent(this, FloatVideoWindowService.class);
intent.putExtra(KEY_C_USER_ID, mUid);
startService(intent);
}
}
@Override
protected void onActivityResult(final int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PermissionRequestCode) {
if (mHandler == null) {
mHandler = new Handler(Looper.getMainLooper());
}
//此处特意延时500ms,否则回调值不准确, https://blog.csdn.net/qq_24179679/article/details/84139408
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Settings.canDrawOverlays(mContext)) {//开启
showFloatView();
} else {//关闭
ToastUtil.show(mContext, "您未授权悬浮窗");
}
}
}
}, 500);
}
}
等等,你刚才坑一里不是说,onActivityResult不回调吗?咋还在singleInstance的页面重写这个方法。经测试,onRequestPermissionsResult的确不回调,但是onActivityResult还是有回调的,延时500ms,回调值更准确哦。
是不是很开森,万事大吉了。人生就像登山,往上走即使一小步也有新高度;然而编程就像玩俄罗斯套娃,打破一小层,还有更大层等着你。走不完的套路,爬不完的坑。
问题描述:当singleInstance页面在onResume时,按下home键,再次点击app的桌面icon,进入后,发现不是刚才的singleInstance页面。我太难啦!!!
有人说可以设置:alwaysRetainTaskState = true
咦~ 这是什么属性?来看看谷歌官方文档怎么说的:
android:alwaysRetainTaskState
系统是否始终保持 Activity 所在任务的状态 —“true”表示是,“false”表示允许系统在特定情况下将任务重置到其初始状态。
默认值为“false”。该属性只对任务的根Activity 有意义;所有其他 Activity 均可忽略该属性。
正常情况下,当用户从主屏幕重新选择某个任务时,系统会在特定情况下清除该任务(从根 Activity 上的堆栈中移除所有Activity)。通常,如果用户在一段时间(如 30 分钟)内未访问任务,系统会执行此操作。
不过,如果该属性的值是“true”,则无论用户如何返回任务,该任务始终会显示最后一次的状态。例如,该属性非常适用于网络浏览器这类应用,因为其中存在大量用户不愿丢失的状态(如多个打开的标签)。
看完最后一句,“该属性非常适用于网络浏览器这类应用,因为其中存在大量用户不愿丢失的状态(如多个打开的标签)”,好像有点那个意思哦,赶紧加上试试,RUN。。。。。满怀期待。。。。虔诚等待。。。。
等了那么久,然并卵。显然是singleInstance在作祟,烦屎了。。。。
剩下我知识储量的最后一招了,纯属无奈之举!
监听app从后台切回前台时机,切回前台时,如果VideoActivity尚在且在通话中,二话不说,直接startActivity之。
怎么监听APP从后台切到前台了呢?注册Application.ActivityLifecycleCallbacks
记得在MyApplication的onCreate中调用注册方法
registerActivityLifecycleCallback(new AppActivityLifecycleCallbacks());
看看AppActivityLifecycleCallbacks实现类的代码:
@Override
public void onActivityStarted(Activity activity) {
//视频面试时APP切换至后台,然后由后台切换至前台,视频面试中,且不是悬浮窗显示时 要回到视频面试页面
if (onActivityStoppedFlag && BackgroundUtils.isForeground(activity)
&& ActivityStackHelper.isActivityRunning(VideoChatViewActivity.class)
&& !SystemUtils.isServiceRunning(activity, FloatVideoWindowService.class.getName())) {
activity.startActivity(new Intent(activity, VideoChatViewActivity.class));
}
onActivityStoppedFlag = !BackgroundUtils.isForeground(activity);
}
private boolean onActivityStoppedFlag = false;
@Override
public void onActivityStopped(Activity activity) {
onActivityStoppedFlag = !BackgroundUtils.isForeground(activity);
}
功能算是“顺利”实现了,但是监听app后台切前台纯属临时方案;恳请有爱的大神帮忙支招,在我的知识盲区指津。
参考文献:
https://www.jianshu.com/p/3786653f9c9b
https://blog.csdn.net/qq_24179679/article/details/84139408
作者:iblade
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341