zoukankan      html  css  js  c++  java
  • 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键回桌面,或者锁屏,然后再回来。

    参考

    一个软件工程师的记录
  • 相关阅读:
    md5编码的两个程序
    DotNetNuke 5 User's Guide Get Your Website Up and Running读书摘录3
    纪念今天DNN密码破解
    DotNetNuke 5 User's Guide Get Your Website Up and Running读书摘录4
    文件与目录的默认权限与隐藏权限(转)
    EXT2 文件系统
    磁盘与目录的容量(转)
    文件的搜寻(转)
    权限与命令间的关系(转)
    账户切换(转)
  • 原文地址:https://www.cnblogs.com/rustfisher/p/15765777.html
Copyright © 2011-2022 走看看