用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
完成後取出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);
});
相關閱讀:獲取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鍵回桌面,或者鎖屏,然後再回來。