市面上常见的摄像头悬浮窗,如微信、手机QQ的视频通话功能,有如下特点:

  • 整屏页面能切换到一个小的悬浮窗
  • 悬浮窗能运行在其他app上方
  • 悬浮窗能跳回整屏页面,并且悬浮窗消失

我们探讨过用CameraX打开摄像头预览,结合可改变大小和浮动的activity,实现了应用内摄像头预览悬浮Activity。这个悬浮Activity是在应用内使用的。要让悬浮窗在其他app上,需要结合悬浮窗 System Alert Window

本文用CameraX实现摄像头预览悬浮窗,能显示在其他app上方,可拖动,可跳回activity。

这个例子的相关代码放进了单独的模块。使用时注意gradle里的细微差别。

引入依赖

模块gradle的一些配置,使用的Android SDK版本为31,启用databinding

plugins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-android-extensions'
id 'kotlin-kapt'
} android {
compileSdk 31 defaultConfig {
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
dataBinding {
enabled = true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
} dependencies {
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'com.google.android.material:material:1.4.0'
implementation project(path: ':baselib')
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' implementation "androidx.camera:camera-core:1.1.0-alpha11"
implementation "androidx.camera:camera-camera2:1.1.0-alpha11"
implementation "androidx.camera:camera-lifecycle:1.1.0-alpha11"
implementation "androidx.camera:camera-view:1.0.0-alpha31"
implementation "androidx.camera:camera-extensions:1.0.0-alpha31"
}

引入CameraX依赖(CameraX 核心库是用camera2实现的),目前主要用1.1.0-alpha11版本

权限

manifest中申请权限

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.any" /> <!-- 悬浮窗的权限 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

动态申请相机权限 Manifest.permission.CAMERA

private static final int REQ_CAMERA = 2;

    if (PackageManager.PERMISSION_GRANTED != ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQ_CAMERA);
}

后面需要MeFloatingCameraXActFloatingCameraXService配合使用。

manifest中注册Activity和Service

<activity
android:name=".camera.MeFloatingCameraXAct"
android:exported="true"
android:launchMode="singleTop" /> <service android:name=".camera.FloatingCameraXService" />

MeFloatingCameraXAct

这个activity提供一个简单的摄像头预览界面,并且提供入口启动悬浮窗。

layout me_act_floating_preivew_x.xml

<?xml version="1.0" encoding="utf-8"?>
<layout> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"> <androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center"
android:orientation="vertical"
android:padding="4dp"> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"> <Button
android:id="@+id/start"
style="@style/NormalBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="打开摄像头" /> <Button
android:id="@+id/end"
style="@style/NormalBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="停止摄像头" /> <Button
android:id="@+id/go_floating"
style="@style/NormalBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="切换到悬浮窗" /> <Button
android:id="@+id/close_act"
style="@style/NormalBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="关闭" /> </LinearLayout> </LinearLayout> </RelativeLayout>
</layout>

完整代码如下

// package com.rustfisher.mediasamples.camera;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
import com.google.common.util.concurrent.ListenableFuture;
import com.rustfisher.mediasamples.R;
import com.rustfisher.mediasamples.databinding.MeActFloatingPreivewXBinding; import java.util.concurrent.ExecutionException; /**
* @author an.rustfisher.com
* @date 2022-1-06 23:53
*/
public class MeFloatingCameraXAct extends AppCompatActivity {
private static final String TAG = "rfDevX";
private static final int REQ_CAMERA = 2;
public static final String K_START_CAMERA = "start_camera"; // 直接启动摄像头 private MeActFloatingPreivewXBinding mBinding;
private ListenableFuture<ProcessCameraProvider> mCameraProviderFuture;
private ProcessCameraProvider mCameraProvider;
private boolean mRunning = false; @Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.me_act_floating_preivew_x);
final boolean startNow = getIntent().getBooleanExtra(K_START_CAMERA, false);
mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
mCameraProviderFuture.addListener(() -> {
try {
mCameraProvider = mCameraProviderFuture.get();
Log.d(TAG, "获取到了 cameraProvider");
if (startNow) {
bindPreview(mCameraProvider);
}
} catch (ExecutionException | InterruptedException e) {
// 这里不用处理
}
}, ContextCompat.getMainExecutor(this));
mBinding.start.setOnClickListener(v -> {
if (mCameraProvider != null && !mRunning) {
bindPreview(mCameraProvider);
}
});
mBinding.end.setOnClickListener(v -> {
mCameraProvider.unbindAll();
mRunning = false;
});
mBinding.goFloating.setOnClickListener(v -> {
startService(new Intent(getApplicationContext(), FloatingCameraXService.class));
finish();
});
mBinding.closeAct.setOnClickListener(v -> {
Toast.makeText(getApplicationContext(), "关闭摄像头示例", Toast.LENGTH_SHORT).show();
stopService(new Intent(getApplicationContext(), FloatingCameraXService.class));
finish();
}); if (PackageManager.PERMISSION_GRANTED != ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQ_CAMERA);
}
} @Override
public void onBackPressed() {
Toast.makeText(getApplicationContext(), "请点击关闭按钮", Toast.LENGTH_SHORT).show();
} @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQ_CAMERA) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(getApplicationContext(), "请允许相机权限", Toast.LENGTH_SHORT).show();
}
}
} private void bindPreview(ProcessCameraProvider cameraProvider) {
if (cameraProvider == null) {
Toast.makeText(getApplicationContext(), "没获取到相机", Toast.LENGTH_SHORT).show();
return;
}
Toast.makeText(getApplicationContext(), "相机启动", Toast.LENGTH_SHORT).show();
Preview preview = new Preview.Builder().build(); CameraSelector cameraSelector = new CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build(); preview.setSurfaceProvider(mBinding.previewView.getSurfaceProvider()); cameraProvider.bindToLifecycle(this, cameraSelector, preview);
mRunning = true;
}
}

CameraX启动预览的代码可以参考 https://an.rustfisher.com/android/jetpack/camerax/simple-preview/

悬浮窗

layout

先给悬浮窗准备一个layout

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000"> <androidx.camera.view.PreviewView
android:id="@+id/preview_view"
android:layout_width="match_parent"
android:layout_height="match_parent" /> <ImageView
android:id="@+id/to_big"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="4dp"
android:src="@drawable/me_to_big"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>

图片请自备

FloatingCameraXService

FloatingCameraXService实现LifecycleOwner接口,为了方便CameraX绑定生命周期组件。

// package com.rustfisher.mediasamples.camera;
import android.app.Service;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.IBinder;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Toast; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry; import com.google.common.util.concurrent.ListenableFuture;
import com.rustfisher.mediasamples.R; import java.util.concurrent.ExecutionException; /**
* 摄像头预览悬浮窗的服务
*
* @author an.rustfisher.com
* @date 2022-01-06 23:53
*/
public class FloatingCameraXService extends Service implements LifecycleOwner {
private static final String TAG = "rfDevFloatingCameraX"; private WindowManager mWM;
private View mFloatView;
private LifecycleRegistry mLifecycleRegistry; private ListenableFuture<ProcessCameraProvider> mCameraProviderFuture;
private ProcessCameraProvider mCameraProvider; @Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
} @Override
public void onCreate() {
super.onCreate();
mLifecycleRegistry = new LifecycleRegistry(this);
mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
} @Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand , " + startId);
if (mFloatView == null) {
Log.d(TAG, "onStartCommand: 创建悬浮窗");
initUi();
}
mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
return super.onStartCommand(intent, flags, startId);
} @Override
public void onDestroy() {
Log.d(TAG, "onDestroy");
if (mFloatView != null) {
mWM.removeView(mFloatView);
}
mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
super.onDestroy();
} private void initUi() {
DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
int width = metrics.widthPixels;
int height = metrics.heightPixels; mWM = (WindowManager) getSystemService(WINDOW_SERVICE);
LayoutInflater inflater = (LayoutInflater) getBaseContext().getSystemService(LAYOUT_INFLATER_SERVICE);
mFloatView = inflater.inflate(R.layout.me_floating_camerax, null); int layoutType;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutType = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
layoutType = WindowManager.LayoutParams.TYPE_TOAST;
} WindowManager.LayoutParams floatLp = new WindowManager.LayoutParams(
(int) (width * (0.4f)), // 这里可以定小一点
(int) (height * (0.3f)),// 为方便展示 这里给的尺寸比较大
layoutType,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
); floatLp.gravity = Gravity.CENTER;
floatLp.x = 0;
floatLp.y = 0; mFloatView.findViewById(R.id.to_big).setOnClickListener(v -> {
stopSelf();
Intent intent = new Intent(getApplicationContext(), MeFloatingCameraXAct.class);
intent.putExtra(MeFloatingCameraXAct.K_START_CAMERA, true);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}); mWM.addView(mFloatView, floatLp); mFloatView.setOnTouchListener(new View.OnTouchListener() {
final WindowManager.LayoutParams floatWindowLayoutUpdateParam = floatLp;
double x;
double y;
double px;
double py; @Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x = floatWindowLayoutUpdateParam.x;
y = floatWindowLayoutUpdateParam.y;
px = event.getRawX();
py = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
floatWindowLayoutUpdateParam.x = (int) ((x + event.getRawX()) - px);
floatWindowLayoutUpdateParam.y = (int) ((y + event.getRawY()) - py);
mWM.updateViewLayout(mFloatView, floatWindowLayoutUpdateParam);
break;
}
return false;
}
}); mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
mCameraProviderFuture.addListener(() -> {
try {
mCameraProvider = mCameraProviderFuture.get();
Log.d(TAG, "[service]获取到了cameraProvider");
bindPreview(mCameraProvider, mFloatView.findViewById(R.id.preview_view));
} catch (ExecutionException | InterruptedException e) {
// 这里不用处理
}
}, ContextCompat.getMainExecutor(this));
} @NonNull
@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
} private void bindPreview(ProcessCameraProvider cameraProvider, PreviewView previewView) {
if (cameraProvider == null) {
Toast.makeText(getApplicationContext(), "没获取到相机", Toast.LENGTH_SHORT).show();
return;
}
Toast.makeText(getApplicationContext(), "相机启动", Toast.LENGTH_SHORT).show();
Preview preview = new Preview.Builder().build();
CameraSelector cameraSelector = new CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build();
preview.setSurfaceProvider(previewView.getSurfaceProvider());
cameraProvider.bindToLifecycle(this, cameraSelector, preview);
}
}

这里悬浮窗view创建完毕后,再去请求摄像头和打开预览。

LifeCycle

生命周期请参考LifeCycle

WindowManager

创建悬浮窗view,并且添加到窗口中。主要使用WindowManager提供的方法。

创建WindowManager.LayoutParams的时候,指定宽高,根据API版本选择layoutType

启动预览

启动预览部分bindPreview代码与前面的类似。用bindToLifecycle方法。

运行测试

运行到手机上,打开这个Activity就可以看到摄像头预览。图像宽高比正常,没有拉伸现象。

缩小成悬浮窗后,可以拖动。可从悬浮窗跳回Activity。

  • 荣耀 EMUI 3.1 Lite,Android 5.1 运行正常
  • Redmi 9A,MIUI 12.5.1稳定版,Android 10 运行正常

小结

结合悬浮窗功能与摄像头得到的预览悬浮窗,可以运行在其他app上方。

注意引导用户开启悬浮窗权限和摄像头权限。

参考

Android 摄像头预览悬浮窗,可拖动,可显示在其他app上方的更多相关文章

  1. Android 摄像头预览悬浮窗

    用CameraX打开摄像头预览,显示在界面上.结合悬浮窗的功能.实现一个可拖动悬浮窗,实时预览摄像头的例子. 这个例子放进了单独的模块里.使用时注意gradle里的细微差别. 操作摄像头,打开预览.这 ...

  2. Android CameraX 打开摄像头预览

    目标很简单,用CameraX打开摄像头预览,实时显示在界面上.看看CameraX有没有Google说的那么好用.先按最简单的来,把预览显示出来. 引入依赖 模块gradle的一些配置,使用的Andro ...

  3. 【腾讯优测干货分享】Android 相机预览方向及其适配探索

    本文来自于腾讯bugly开发者社区,未经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/583ba1df25d735cd2797004d 由于Android系统的开放策略 ...

  4. 基于开源的GOCW和Directshow.net,实现摄像头预览、采集、录像等操作

    本文基于开源的GOCW和Directshow.net,实现图像采集等操作.最为关键的部分在于可以实现摄像头的控制,同时关于视频采集进行了实现. 具体的内容请关注首发于51CTO的课程<基于Csh ...

  5. [转帖]Windows 10新预览版上线:可直接运行任意安卓APP了

    Windows 10新预览版上线:可直接运行任意安卓APP了 http://www.pcbeta.com/viewnews-80316-1.html 今晨(3月13日),微软面向Fast Ring(快 ...

  6. 关于降低android手机摄像头预览分辨率

    假设现在有这样一个需求需要一直开着手机摄像头 但是不做任何拍照动作 但是每个手机的相机分辨率都不同 而默认预览的时候参数是最大分辨率 这样有时候就回导致电量损耗的加快 所以我们可以采取降低相机分辨率的 ...

  7. Android悬浮窗及其拖动事件

    主页面布局很简单,只有一个RelativelyLayout <?xml version="1.0" encoding="utf-8"?> <R ...

  8. android camera 摄像头预览画面变形

    问题:最近在处理一下camera的问题,发现在竖屏时预览图像会变形,而横屏时正常.但有的手机则是横竖屏都会变形. 结果:解决了预览变形的问题,同时支持前后摄像头,预览无变形,拍照生成的jpg照片方向正 ...

  9. Android开发:实时处理摄像头预览帧视频------浅析PreviewCallback,onPreviewFrame,AsyncTask的综合应用(转)

    原文地址:http://blog.csdn.net/yanzi1225627/article/details/8605061# 很多时候,android摄像头模块不仅预览,拍照这么简单,而是需要在预览 ...

随机推荐

  1. File类及常用操作方法

    import java.io.File; import java.io.IOException; public class file { public static void main(String[ ...

  2. linux网络相关命令之脚本和centos启动流程

    nice 功用:设置优先权,可以改变程序执行的优先权等级.等级的范围从-19(最高优先级)到20(最低优先级).优先级为操作系统决定cpu分配的参数,优先级越高,所可能获得的 cpu时间越长. 语法: ...

  3. shell 截取字符串实例教程

    本节内容:shell字符串截取方法 1,去掉字符串最左边的字符 [root@jbxue ~]$ vi test.sh 1 STR="abcd" 2 STR=${STR#" ...

  4. 使用Booststrap布局网页页面

    <!DOCTYPE html><html lang="zh-CN"><head> <meta charset="utf-8&qu ...

  5. [BUUCTF]PWN——babyheap_0ctf_2017

    [BUUCTF]PWN--babyheap_0ctf_2017 附件 步骤: 例行检查,64位,保护全开 试运行一下程序,看到这个布局菜单,知道了这是一道堆的题目,第一次接触堆的小伙伴可以去看一下这个 ...

  6. 拖动条形图设置任务关联(Project)

    <Project2016 企业项目管理实践>张会斌 董方好 编著 仅仅是知悉了四种任务关联,那只是纸上谈兵,要把这四种关联真正用到Project上才行,所以我们就要来设置任务关联了. 这是 ...

  7. Tornado WEB服务器框架 Epoll-- 【模板】

    4.2 使用模板 1. 路径与渲染 使用模板,需要仿照静态文件路径设置一样,向web.Application类的构造函数传递一个名为template_path的参数来告诉Tornado从文件系统的一个 ...

  8. Vue3 全家桶,从 0 到 1 实战项目,新手有福了

    前端发展百花放,一技未熟百技出.未知何处去下手,关注小编胜百书. 我是前端人,专注分享前端内容! 本篇文章主要是,使用 vite 创建一个vue3 项目,实践 vie-router4 vuex4 结合 ...

  9. 通过css实现表格的斜线

    效果图 实现思路 编辑一个svg文件,可以自定义线条颜色和粗细. 将svg文件转为base64格式,作为背景图属性设置. svg转base64的网址:https://www.sojson.com/im ...

  10. 【LeetCode】913. Cat and Mouse 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 参考资料 日期 题目地址:https://leetc ...