Android 从零开发一个简易的相机App

本文介绍了实现一个简易Android相机App过程中,遇到的一些问题,对Camera API的选型、通知相册更新、跳转相册、左右滑动界面切换拍照/录像,相机切换时候的高斯模糊虚化效果、相机切换的3D效果做了说明。

1. 技术选型

Android调用相机可以使用Camera1Camera2CameraX

1.1 Camera1

Camera1API相对复杂,且GoogleAndroid 5.0的时候,就已经停止维护了。
但由于种种原因,有时候不得不使用Camera1的API。
如果必须要使用,建议参照 Camera1Java 这个Github库,写的还挺详细的。
同时,还有Camera1的官方文档 : Camera1 API

1.2 Camera2

Android5.0以上支持Camera2的API,如果使用Camera2,可以看官方的demo : Camera2Basic
还有官方的文档 : Android Developers | Camera2 overview

也可以直接使用Github上的一个封装库 CameraView,使用起来比较简单,
它支持使用Camera1Camera2作为引擎,进行图片的拍摄和视频的捕捉。

<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)中。

Android Developers | HAL介绍

接下来来介绍下开发简易相机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)

详见 android如何调系统用相册并处理返回

但是这其实是一个选择图片的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)

这里需要注意的有两点

  1. 用户点击切换后,需要同步先处理好高斯模糊的效果,再进行摄像头的切换,这个处理时间大概在200ms,对于用户几乎是无感知的
  2. CameraX 默认使用的implementationModeperformance,我是改为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

猜你喜欢

转载自blog.csdn.net/EthanCo/article/details/126260794
今日推荐