Android 从零开发一个简易的相机App
Camera
本文介绍了实现一个简易Android相机App过程中,遇到的一些问题,对Camera API的选型、通知相册更新、跳转相册、左右滑动界面切换拍照/录像,相机切换时候的高斯模糊虚化效果、相机切换的3D效果做了说明。
1. 技术选型
Android调用相机可以使用Camera1
、Camera2
和CameraX
1.1 Camera1
Camera1
的API
相对复杂,且Google
在Android 5.0
的时候,就已经停止维护了。
但由于种种原因,有时候不得不使用Camera1
的API。
如果必须要使用,建议参照 Camera1Java 这个Github库,写的还挺详细的。
同时,还有Camera1的官方文档 : Camera1 API
1.2 Camera2
Android5.0
以上支持Camera2
的API,如果使用Camera2
,可以看我的博客 :
十分钟实现 Android Camera2 相机预览
十分钟实现 Android Camera2 相机拍照
十分钟实现 Android Camera2 视频录制
还有官方的文档 : Android Developers | Camera2 overview
也可以直接使用Github
上的一个封装库 CameraView,使用起来比较简单,
它支持使用Camera1
或Camera2
作为引擎,进行图片的拍摄和视频的捕捉。
<com.otaliastudios.cameraview.CameraView
android:id="@+id/camera"
android:keepScreenOn="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
CameraView camera = findViewById(R.id.camera);
camera.setLifecycleOwner(this);
}
具体详见 官方文档 CameraView官方文档
Android5.0以上支持Camera2,但 Android 5.0 及更高版本的设备可能并不支持所有相机 API2 功能。
不是所有Android设备都支持完整的Camera2功能, 现在都2022了, Camera2出来都有8年左右了, Android车机上还有在使用低版本HAL的, 就会导致Camera2一些高级功能都没法使用。详见 Android Camera2 综述
1.3 Camera X
CameraX 是 Jetpack 的新增库。基于Camera2开发,向上提供更简洁的API接口,向下处理了各种厂商机型的兼容性问题,有助于在众多设备上打造一致的开发者体验。
Camera X 用起来也很简单
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
val preview = Preview.Builder().build()
val viewFinder: PreviewView = findViewById(R.id.previewView)
// The use case is bound to an Android Lifecycle with the following code
val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)
// PreviewView creates a surface provider and is the recommended provider
preview.setSurfaceProvider(viewFinder.getSurfaceProvider())
具体详见我的另一篇博客 Android 使用CameraX实现预览/拍照/录制视频/图片分析/对焦/切换摄像头等操作
这里,我选用了CameraX
来进行相机的开发。
1.4 扩展知识 : Android Camera HAL
HAL(Hardware Abstraction Layer),即Android 的Camera硬件抽象层。
HAL 位于相机驱动程序和更高级别的 Android 框架之间,它定义了必须实现的接口,以便应用可以正确地操作相机硬件。
HAL 可定义一个标准接口以供硬件供应商实现,可让Android忽略较低级别的驱动程序实现。HAL实现通常会内置在共享库模块(.so)中。
接下来来介绍下开发简易相机App的时候,遇到的问题
2. 通知相册更新
当我们拍摄了一张图片之后,如果不通知系统更新相册,那么在相册中是找不到这张图片的。
所以当我们拍摄了图片后,必须要通知系统,让相册更新这张图片。
首先,新建一个FileUtils
类,将需要保存的图片存储在该路径下
object FileUtils {
val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS"
val PHOTO_EXTENSION = ".jpg"
/** Helper function used to create a timestamped file */
fun createFile(baseFolder: File, format: String, extension: String) =
File(
baseFolder, SimpleDateFormat(format, Locale.US)
.format(System.currentTimeMillis()) + extension
)
/** Use external media if it is available, our app's file directory otherwise */
fun getOutputDirectory(context: Context): File {
val appContext = context.applicationContext
val mediaDir = context.externalMediaDirs.firstOrNull()?.let {
File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() }
}
return if (mediaDir != null && mediaDir.exists())
mediaDir else appContext.filesDir
}
fun getMoviesDirectory(context: Context): File {
var externalDirectory = Environment.getExternalStorageDirectory()
return File(externalDirectory, "Movies")
}
}
然后保存图片后
//这里是异步线程
File outputDirectory = FileUtils.INSTANCE.getOutputDirectory(context);
File myCaptureFile = FileUtils.INSTANCE.createFile(outputDirectory, FileUtils.INSTANCE.getFILENAME(), FileUtils.INSTANCE.getPHOTO_EXTENSION());
//写入文件
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(myCaptureFile));
bm.compress(Bitmap.CompressFormat.JPEG, 100, bos);
bos.flush();
bos.close();
最后,通知系统更新相册
//把图片保存后声明这个广播事件通知系统相册有新图片到来
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
Uri uri = Uri.fromFile(myCaptureFile);
Log.d(TAG, "Photo capture succeeded:" + myCaptureFile.getPath());
intent.setData(uri);
context.sendBroadcast(intent);
需要注意的是,这里没有启用分区存储,如果要适配分区存储的话,请看这几篇文章
Android 10 分区存储完全解析
Android Developer : 访问共享存储空间中的媒体文件
支持 Android 12,全版本保存图片到相册方案
3. 跳到相册
相机里还有一个跳转到全部相册的功能
首先,是去网上找到了一个跳转到相册的方法
3.1 使用Intent.ACTION_PICK
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
if (isVideo){
intent.type = "video/*"
}else{
intent.type = "image/*"
}
startActivity(intent)
但是这其实是一个选择图片的Intent,并不是跳转到真的相册。
后来想到,可以通过隐式Intent跳转到系统相册的App,这里以华为的相册为例
3.2 反编译获得隐式intent
3.2.1 查找包名并导出
首先,我们需要先查找到华为相册的包名,导出华为相册app
// 第一步 : 查看包名
adb shell am monitor
//第二步 : 查看该包名的存放路径
adb shell pm path com.huawei.photos
//第三步 : 导出到电脑上
adb pull 路径地址
具体详见 用adb导出某个app
3.2.2 使用dex2jar进行反编译
我们先解压apk,获取到dex文件,然后使用dex2jar
进行反编译
需要注意的是,需要把dex版本修改为036
,高版本已不支持反编译了。
d2j-dex2jar classes.dex
反编译成功后,我们使用jd-gui
进行打开
同时,通过apktool
我们可以反编译得到AndroidManifest.xml
查看Manifest可以得知,相册app的入口是GalleryMain
<activity android:configChanges="keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize" android:label="@string/app_name" android:launchMode="singleTop" android:name="com.huawei.gallery.app.GalleryMain" android:theme="@style/SplashTheme" android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="com.huawei.gallery.action.ACCESS_HWGALLERY"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:host="photosapp" android:path="/oneKeyDirect" android:scheme="huaweischeme"/>
</intent-filter>
<intent-filter>
<action android:name="com.huawei.gallery.action.ACCESS_HWGALLERY"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<intent-filter>
<action android:name="hwgallery.intent.action.GET_PHOTOSHARE_CONTENT"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.APP_GALLERY"/>
</intent-filter>
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
</activity>
可以看到,当tab为1的时候,会切换到albums
这个tab,这个就是我们想要的。
最终的itnent为
if (RomUtils.isHuawei()) {
val intent = Intent()
intent.setClassName("com.huawei.photos", "com.huawei.gallery.app.GalleryMain")
intent.putExtra("tab", 1)
startActivity(intent)
}
4. 左右滑动界面切换拍照/录像
一般相机左右滑动之后可以切换拍照/录像功能,我们对根view
进行touchEvent
监听即可
private var mPosX = 0F
private var mPosY = 0F
private var mCurPosX = 0F
private var mCurPosY = 0F
binding.rootView.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
mPosX = event.x
mPosY = event.y
}
MotionEvent.ACTION_MOVE -> {
mCurPosX = event.x
mCurPosY = event.y
}
MotionEvent.ACTION_UP ->
if (mCurPosX - mPosX > 0
&& Math.abs(mCurPosX - mPosX) > 120
) {
//往左滑
} else if (mCurPosX - mPosX < 0
&& Math.abs(mCurPosX - mPosX) > 120
) {
//往右滑
}
}
true
}
5. 相机切换时候虚化,实现高斯模糊效果
我这里使用到了AndroidUtilCode里的工具类ImageUtils
,使用ImageUtils.fastBlur
来进行高斯模糊效果的处理
val originBitmap = binding.previewView.bitmap
val blurBitmap = ImageUtils.fastBlur(originBitmap, 0.25F, 25F)
binding.imgBlur.setImageBitmap(blurBitmap)
这里需要注意的有两点
- 用户点击切换后,需要同步先处理好高斯模糊的效果,再进行摄像头的切换,这个处理时间大概在200ms,对于用户几乎是无感知的
- CameraX 默认使用的
implementationMode
为performance
,我是改为compatible
取到的图像角度才是正常的,关于implementationMode
可以看 CameraX 实现预览文档
效果如下
6. 相机切换 3D翻转效果
市面上主流的相机,切换前后摄像头的时候,会有3D翻转的效果。
网上找到了这篇文章 手把手教你实现Android开发中的3D卡片翻转效果!,自己按照文中步骤实现了一下
Rotate3dAnimation.kt
/**
* An animation that rotates the view on the Y axis between two specified angles.
* This animation also adds a translation on the Z axis (depth) to improve the effect.
*/
public class Rotate3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private final float mDepthZ;
private final boolean mReverse;
private Camera mCamera;
/**
* Creates a new 3D rotation on the Y axis. The rotation is defined by its
* start angle and its end angle. Both angles are in degrees. The rotation
* is performed around a center point on the 2D space, definied by a pair
* of X and Y coordinates, called centerX and centerY. When the animation
* starts, a translation on the Z axis (depth) is performed. The length
* of the translation can be specified, as well as whether the translation
* should be reversed in time.
*
* @param fromDegrees the start angle of the 3D rotation //起始角度
* @param toDegrees the end angle of the 3D rotation //结束角度
* @param centerX the X center of the 3D rotation //x中轴线
* @param centerY the Y center of the 3D rotation //y中轴线
* @param reverse true if the translation should be reversed, false otherwise//是否反转
*/
public Rotate3dAnimation(float fromDegrees, float toDegrees,
float centerX, float centerY, float depthZ, boolean reverse) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;//Z轴移动的距离,这个来影响视觉效果,可以解决flip animation那个给人看似放大的效果
mReverse = reverse;
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;
final Matrix matrix = t.getMatrix();
Log.i("interpolatedTime", interpolatedTime+"");
camera.save();
if (mReverse) {
camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
} else {
camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
}
camera.rotateX(degrees);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}
Rotate3dManager.kt
class Rotate3dManager(val photo1: View) {
private var centerX = 0
private var centerY = 0
private val depthZ = 400
private val duration = 300
private var closeAnimation: Rotate3dAnimation? = null
/**
* 卡牌文本介绍关闭效果:旋转角度与打开时逆行即可
*/
private fun initCloseAnim() {
closeAnimation = Rotate3dAnimation(
360F, 270F, centerX.toFloat(), centerY.toFloat(),
depthZ.toFloat(), true
)
closeAnimation!!.setDuration(duration.toLong())
closeAnimation!!.setFillAfter(true)
closeAnimation!!.setInterpolator(AccelerateInterpolator())
closeAnimation!!.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
}
override fun onAnimationRepeat(animation: Animation) {}
override fun onAnimationEnd(animation: Animation) {
val rotateAnimation =
Rotate3dAnimation(
90F, 0F,
centerX.toFloat(), centerY.toFloat(), depthZ.toFloat(), false
)
rotateAnimation.duration = duration.toLong()
rotateAnimation.fillAfter = true
rotateAnimation.interpolator = DecelerateInterpolator()
photo1!!.startAnimation(rotateAnimation)
}
})
}
fun operate() {
if (photo1.width <= 0) {
photo1.post {
operate()
}
return
}
//以旋转对象的中心点为旋转中心点,这里主要不要再onCreate方法中获取,因为视图初始绘制时,获取的宽高为0
centerX = photo1.width / 2
centerY = photo1.height / 2
if (closeAnimation == null) {
initCloseAnim()
}
//用作判断当前点击事件发生时动画是否正在执行
if (closeAnimation!!.hasStarted() && !closeAnimation!!.hasEnded()) {
return
}
photo1!!.startAnimation(closeAnimation)
}
}
然后,对想要翻转的View进行使用即可
val rotate3dManager = Rotate3dManager(targetView)
rotate3dManager.operate()
效果如下
github地址 : DialogFlipTest
转载:https://blog.csdn.net/EthanCo/article/details/126260794
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)