Android 摄像头预览悬浮窗

CameraX打开摄像头预览,显示在界面上。结合悬浮窗的功能。实现一个可拖动悬浮窗,实时预览摄像头的例子。

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

操作摄像头,打开预览。这部分代码与Android CameraX 打开摄像头预览相同。
悬浮窗相关代码与可拖动悬浮窗相同。在此基础上增加了对拖动范围的限制。

引入依赖

模块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版本

权限

需要动态申请android.permission.CAMERA权限

<uses-permission android:name="android.permission.CAMERA" />

本文略过动态申请权限的地方

layout

CameraX提供了androidx.camera.view.PreviewView

把它放在一个FrameLayout里,如下的me_act_simple_preivew_x_scale.xml

<?xml version="1.0" encoding="utf-8"?>
<layout>

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/root"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent">

        <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:id="@+id/func_field"
            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="停止摄像头" />

            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:orientation="horizontal">

                <Button
                    android:id="@+id/enable_ana"
                    style="@style/NormalBtn"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="setAnalyzer" />

                <Button
                    android:id="@+id/clr_ana"
                    style="@style/NormalBtn"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="4dp"
                    android:text="clearAnalyzer" />

                <Button
                    android:id="@+id/take_one_analyse"
                    style="@style/NormalBtn"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="4dp"
                    android:text="截取" />

            </LinearLayout>
        </LinearLayout>

        <View
            android:id="@+id/touch_move"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:visibility="gone" />

        <ImageView
            android:id="@+id/zoom_iv"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_margin="12dp"
            android:src="@drawable/me_ic_to_small" />

        <TextView
            android:id="@+id/tip"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:text="rustfisher.com" />

    </RelativeLayout>
</layout>

func_field装着一些按钮。缩小和还原界面用zoom_iv

style

准备一个style

<style name="MeTranslucentAct" parent="AppTheme">
    <item name="android:windowBackground">#80000000</item>
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
</style>

manifest里注册

<activity
    android:name=".camera.MeSimplePreviewXFloatingAct"
    android:exported="true"
    android:theme="@style/MeTranslucentAct" />

activity

开启摄像头

新建MeSimplePreviewXFloatingAct,继承androidx.appcompat.app.AppCompatActivity

// onCreate中获取mCameraProvider
mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
mCameraProviderFuture.addListener(() -> {
    try {
        mCameraProvider = mCameraProviderFuture.get();
        Log.d(TAG, "获取到了 cameraProvider");
        } catch (ExecutionException | InterruptedException e) {
        // 这里不用处理
    }
}, ContextCompat.getMainExecutor(this));

为了获得ProcessCameraProvider,用ProcessCameraProvider.getInstance方法拿到一个cameraProviderFuture
cameraProviderFuture完成后取出ProcessCameraProvidercameraProvider)。

开启摄像头的方法bindPreview

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, mImageAnalysis);
    mRunning = true;
}

要开启预览,通过Preview.Builder构建一个Preview。用CameraSelector来选择后置摄像头。
PreviewSurfaceProvider由layout中的androidx.camera.view.PreviewView提供。

cameraProvider.bindToLifecycle绑定上后,启动摄像头预览

悬浮窗

setContentView之前设置一下window的flag

WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
layoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
缩小和放大窗口

缩小放大窗口需要用android.view.WindowManager.LayoutParams

private void toSmallWindow() {
    mBinding.funcField.setVisibility(View.GONE);
    mIsSmallWindow = true;
    mBinding.zoomIv.setImageResource(R.drawable.me_to_big);

    android.view.WindowManager.LayoutParams p = getWindow().getAttributes();
    p.height = 480; // 悬浮窗大小可以自己定
    p.width = 360;
    p.dimAmount = 0.0f;
    getWindow().setAttributes(p);
}

private void toBigWindow() {
    WindowManager.LayoutParams lp = getWindow().getAttributes();
    lp.x = 0;
    lp.y = 0;
    getWindow().setAttributes(lp);

    getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    mBinding.funcField.setVisibility(View.VISIBLE);
    mIsSmallWindow = false;
    mBinding.zoomIv.setImageResource(R.drawable.me_ic_to_small);
}

按钮的图片资源请自备

限制拖动范围

先拿到一个参考范围

mBinding.container.post(() -> {
    mBigWid = mBinding.container.getWidth();
    mBigHeight = mBinding.container.getHeight();
    Log.d(TAG, "container size: " + mBigWid + ", " + mBigHeight);
});

相关阅读:获取view的宽高

activity完整代码

// package com.rustfisher.mediasamples.camera;
import android.os.Bundle;
import android.util.Log;
import android.util.Size;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
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.MeActSimplePreivewXScaleBinding;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


/**
 * 预览照相机  悬浮窗
 *
 * @author an.rustfisher.com
 * @date 2021-12-31 15:53
 */
public class MeSimplePreviewXFloatingAct extends AppCompatActivity {
    private static final String TAG = "rfDevX";
    private MeActSimplePreivewXScaleBinding mBinding;
    private ListenableFuture<ProcessCameraProvider> mCameraProviderFuture;
    private ProcessCameraProvider mCameraProvider;
    private boolean mRunning = false;

    private boolean mIsSmallWindow = false;
    private boolean mLimitArea = true;

    private boolean mTakeOneYuv = false; // 获取一帧 实际工程中不要这么做

    private final ImageAnalysis mImageAnalysis =
            new ImageAnalysis.Builder()
                    //.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
                    .setTargetResolution(new Size(720, 1280)) // 图片的建议尺寸
                    .setOutputImageRotationEnabled(true) // 是否旋转分析器中得到的图片
                    .setTargetRotation(Surface.ROTATION_0) // 允许旋转后 得到图片的旋转设置
                    .setBackpressureStrategy(ImageAnalysis.STRATEGY_BLOCK_PRODUCER)
                    .build();

    private float mLastTx = 0; // 手指的上一个位置
    private float mLastTy = 0;

    private int mBigHeight = 0;
    private int mBigWid = 0;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
        layoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;

        mBinding = DataBindingUtil.setContentView(this, R.layout.me_act_simple_preivew_x_scale);
        mCameraProviderFuture = ProcessCameraProvider.getInstance(this);
        mCameraProviderFuture.addListener(() -> {
            try {
                mCameraProvider = mCameraProviderFuture.get();
                Log.d(TAG, "获取到了 cameraProvider");
//                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.takeOneAnalyse.setOnClickListener(v -> {
            mTakeOneYuv = true;
            Log.d(TAG, "获取一帧, 输出图片旋转: " + mImageAnalysis.isOutputImageRotationEnabled());
        });

        final ExecutorService executorService = Executors.newFixedThreadPool(2);
        mBinding.enableAna.setOnClickListener(v -> {
            Toast.makeText(getApplicationContext(), "启用分析器", Toast.LENGTH_SHORT).show();
            mImageAnalysis.setAnalyzer(executorService, imageProxy -> {
                // 下面处理数据
                if (mTakeOneYuv) {
                    mTakeOneYuv = false;
                    Log.d(TAG, "旋转角度: " + imageProxy.getImageInfo().getRotationDegrees());
                    ImgHelper.useYuvImgSaveFile(imageProxy, true); // 存储这一帧为文件
                    runOnUiThread(() -> Toast.makeText(getApplicationContext(), "截取一帧", Toast.LENGTH_SHORT).show());
                }
                imageProxy.close(); // 最后要关闭这个
            });
        });
        mBinding.clrAna.setOnClickListener(v -> {
            mImageAnalysis.clearAnalyzer();
            Toast.makeText(getApplicationContext(), "clearAnalyzer", Toast.LENGTH_SHORT).show();
        });
        mBinding.zoomIv.setOnClickListener(v -> {
            if (mIsSmallWindow) {
                toBigWindow();
            } else {
                toSmallWindow();
            }
        });

        mBinding.root.setOnTouchListener((v, event) -> {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    Log.d(TAG, "down " + event);
                    mLastTx = event.getRawX();
                    mLastTy = event.getRawY();
                    return true;
                case MotionEvent.ACTION_MOVE:
                    Log.d(TAG, "move " + event);
                    float dx = event.getRawX() - mLastTx;
                    float dy = event.getRawY() - mLastTy;
                    mLastTx = event.getRawX();
                    mLastTy = event.getRawY();
                    Log.d(TAG, "  dx: " + dx + ", dy: " + dy);
                    if (mIsSmallWindow) {
                        WindowManager.LayoutParams lp = getWindow().getAttributes();
                        int tx = (int) (lp.x + dx);
                        int ty = (int) (lp.y + dy);
                        Log.d(TAG, "move to " + tx + ", " + ty);
                        if (mLimitArea) {
                            tx = Math.max(lp.width / 2 - mBigWid / 2, tx);
                            tx = Math.min(mBigWid / 2 - lp.width / 2, tx);
                            ty = Math.max(lp.height / 2 - mBigHeight / 2, ty);
                            ty = Math.min(mBigHeight / 2 - lp.height / 2, ty);
                        }
                        lp.x = tx;
                        lp.y = ty;
                        getWindow().setAttributes(lp);
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    Log.d(TAG, "up " + event);
                    return true;
                case MotionEvent.ACTION_CANCEL:
                    Log.d(TAG, "cancel " + event);
                    return true;
            }
            return false;
        });
        mBinding.container.post(() -> {
            mBigWid = mBinding.container.getWidth();
            mBigHeight = mBinding.container.getHeight();
            Log.d(TAG, "container size: " + mBigWid + ", " + mBigHeight);
        });
    }

    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, mImageAnalysis);
        mRunning = true;
    }

    private void toSmallWindow() {
        mBinding.funcField.setVisibility(View.GONE);
        mIsSmallWindow = true;
        mBinding.zoomIv.setImageResource(R.drawable.me_to_big);

        android.view.WindowManager.LayoutParams p = getWindow().getAttributes();
        p.height = 480;
        p.width = 360;
        p.dimAmount = 0.0f;
        getWindow().setAttributes(p);
    }

    private void toBigWindow() {
        WindowManager.LayoutParams lp = getWindow().getAttributes();
        lp.x = 0;
        lp.y = 0;
        getWindow().setAttributes(lp);

        getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        mBinding.funcField.setVisibility(View.VISIBLE);
        mIsSmallWindow = false;
        mBinding.zoomIv.setImageResource(R.drawable.me_ic_to_small);
    }
}

运行测试

运行到手机上,打开这个Activity就可以看到摄像头预览。图像宽高比正常,没有拉伸现象。
缩小成悬浮窗后,可以拖动。

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

小结

从简单的打开相机预览来看,CameraX简化了开发者的工作。提供了PreviewView,开发者不需要自定义SurfaceView或者TextureView。实时预览中,相机能够自动对焦。可以试试按home键回桌面,或者锁屏,然后再回来。

参考

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在 Android 应用中实现后置摄像头悬浮窗,你需要进行以下步骤: 1. 添加权限:在 AndroidManifest.xml 文件中添加摄像头权限。 ```xml <uses-permission android:name="android.permission.CAMERA" /> ``` 2. 创建后置摄像头预览界面:在你的 Activity 或 Fragment 中创建一个 SurfaceView,并在 `surfaceCreated` 回调中打开相机并设置预览界面。 ```java public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { private SurfaceHolder holder; private Camera camera; public CameraPreview(Context context) { super(context); // Install a SurfaceHolder.Callback so we get notified when the // underlying surface is created and destroyed. holder = getHolder(); holder.addCallback(this); } public void surfaceCreated(SurfaceHolder holder) { // Open the camera and set the preview display camera = Camera.open(); try { camera.setPreviewDisplay(holder); } catch (IOException e) { e.printStackTrace(); } } public void surfaceDestroyed(SurfaceHolder holder) { // Release the camera when the surface is destroyed camera.stopPreview(); camera.release(); camera = null; } public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { // Start the camera preview after the surface is changed camera.startPreview(); } } ``` 3. 创建悬浮窗:在你的 Activity 或 Service 中创建一个悬浮窗,并将预览界面添加到悬浮窗中。 ```java public class FloatingCameraService extends Service { private WindowManager windowManager; private CameraPreview cameraPreview; public void onCreate() { super.onCreate(); // Create a new SurfaceView and add it to the window manager cameraPreview = new CameraPreview(this); WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_PHONE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); windowManager.addView(cameraPreview, layoutParams); } public void onDestroy() { super.onDestroy(); // Remove the SurfaceView from the window manager when the service is destroyed windowManager.removeView(cameraPreview); } public IBinder onBind(Intent intent) { // Not used return null; } } ``` 4. 启动悬浮窗:在你的 Activity 中启动悬浮窗服务。 ```java Intent intent = new Intent(this, FloatingCameraService.class); startService(intent); ``` 注意:在 Android 6.0 及以上版本中,你需要动态请求摄像头权限。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值