Android 摄像头预览悬浮窗

116 阅读4分钟

用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权限

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

layout CameraX提供了androidx.camera.view.PreviewView

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

<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>
func_field装着一些按钮。缩小和还原界面用zoom_iv

style 准备一个style

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

manifest里注册

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完成后取出ProcessCameraProvider(cameraProvider)。

开启摄像头的方法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来选择后置摄像头。 Preview的SurfaceProvider由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); }); !!! tip “”

获取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 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键回桌面,或者锁屏,然后再回来。