zoukankan      html  css  js  c++  java
  • Android 摄像头预览悬浮窗,可拖动,可显示在其他app上方

    市面上常见的摄像头悬浮窗,如微信、手机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上方。
    注意引导用户开启悬浮窗权限和摄像头权限。

    参考

    一个软件工程师的记录
  • 相关阅读:
    1-4个人博客
    大二上学期软件工程概论学习进度表(第十六周)
    软件工程概论个人总结
    python+selenium 定位元素的主要方法
    python+selenium 元素定位--iframe
    返回字符串中出现最多的字符
    TestNG中 ITestListener 的使用
    对数组对象按某些属性排序方法
    OSX 10.11 cocoapods安装命令: sudo gem install -n /usr/local/bin cocoapods
    IOS启动页设置适应ios8/9
  • 原文地址:https://www.cnblogs.com/rustfisher/p/15773979.html
Copyright © 2011-2022 走看看