Android develops a simple camera App from scratch

This article introduces some problems encountered in the process of implementing a simple Android camera App, including the selection of Camera API, notification of album updates, jumping to albums, sliding the interface left and right to switch photo/video, and the Gaussian blur effect when switching cameras. , the 3D effect of camera switching is explained.

1. Technology selection

Android can use Camera1, Camera2 and CameraX to call the camera

1.1 Camera1

Camera1‘s API is relatively complex, and Google has stopped maintaining it since Android 5.0.
But due to various reasons, sometimes you have to use the API of Camera1.
If you must use it, it is recommended to refer to the Github library Camera1Java, which is written in quite detail.
At the same time, there is also the official documentation of Camera1: Camera1 API

1.2 Camera2

Android5.0 and above support the API of Camera2. If you use Camera2, you can read my blog:
Implement Android Camera2 camera preview in ten minutes
Take pictures with Android Camera2 in ten minutes
Implement Android Camera2 video recording in ten minutes
There are also official documents: Android Developers | Camera2 overview

You can also directly use CameraView, a package library on Github, which is relatively simple to use.
It supports using Camera1 or Camera2 as the engine for picture shooting and video capture.

<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);
}

For details, please see the official documentation CameraView official documentation

Android 5.0 and above support Camera2, but devices with Android 5.0 and above may not support all camera API2 functions.
Not all Android devices support complete Camera2 functions. It is now 2022, and Camera2 has been out for about 8 years. Some Android cars are still using lower versions of HAL, which will cause some advanced functions of Camera2 to be unavailable. See Android Camera2 review for details

1.3 Camera X

CameraX is a new library for Jetpack. Developed based on Camera2, it provides a simpler API interface upwards and handles the compatibility issues of various manufacturers’ models downwards, helping to create a consistent developer experience on many devices.

Camera X is also very easy to use

<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())

For details, please see my other blog. Android uses CameraX to implement preview/photography/video recording/picture analysis/focus/switching cameras and other operations.

Here, I chose CameraX for camera development.

1.4 Extended knowledge: Android Camera HAL

HAL (Hardware Abstraction Layer), the Camera hardware abstraction layer of Android.
The HAL sits between the camera driver and the higher-level Android framework, and defines the interfaces that must be implemented so that applications can operate the camera hardware correctly.
HAL defines a standard interface for hardware vendors to implement, allowing Android to ignore lower-level driver implementations. HAL implementations are usually built into shared library modules (.so).

Android Developers | HAL Introduction

Next, let’s introduce the problems encountered when developing a simple camera App.

2. Notification of album updates

After we take a picture, if we do not notify the system to update the album, the picture will not be found in the album.
So when we take a picture, we must notify the system to let the album update the picture.

First, create a new FileUtils class and store the pictures that need to be saved in this path.

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 & amp; & amp; mediaDir.exists())
            mediaDir else appContext.filesDir
    }

    fun getMoviesDirectory(context: Context): File {<!-- -->
        var externalDirectory = Environment.getExternalStorageDirectory()
        return File(externalDirectory, "Movies")
    }
}

Then save the picture

//This is an asynchronous thread
File outputDirectory = FileUtils.INSTANCE.getOutputDirectory(context);
File myCaptureFile = FileUtils.INSTANCE.createFile(outputDirectory, FileUtils.INSTANCE.getFILENAME(), FileUtils.INSTANCE.getPHOTO_EXTENSION());
//Write to file
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(myCaptureFile));
bm.compress(Bitmap.CompressFormat.JPEG, 100, bos);
bos.flush();
bos.close();

Finally, notify the system to update the album

//After saving the picture, declare this broadcast event to notify the system that new pictures have arrived in the album.
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);

It should be noted that partition storage is not enabled here. If you want to adapt partition storage, please read these articles.
Android 10 partition storage complete analysis
Android Developer : Access media files in shared storage
Support Android 12, full version save pictures to album solution

3. Jump to photo album

There is also a function in the camera to jump to all albums
First, I went online and found a way to jump to the photo album.

3.1 Using 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)

For details, see How Android calls the system album and handles returns

But this is actually an Intent to select a picture, not a jump to a real photo album.
Later I thought that I could jump to the app of the system photo album through implicit Intent. Here I take Huawei’s photo album as an example.

3.2 Decompile to obtain implicit intent
3.2.1 Find the package name and export

First, we need to find the package name of Huawei Photo Album and export it to Huawei Photo Album app

//The first step: Check the package name
adb shell am monitor

//Step 2: Check the storage path of the package name
adb shell pm path com.huawei.photos

//Step 3: Export to computer
adb pull path address

For details, see Export an app using adb

3.2.2 Decompile using dex2jar

We first decompress the apk, obtain the dex file, and then use dex2jar to decompile
It should be noted that the dex version needs to be modified to 036. Higher versions no longer support decompilation.

d2j-dex2jar classes.dex

After decompilation is successful, we use jd-gui to open it
At the same time, through apktool we can decompile and get AndroidManifest.xml
Looking at the Manifest, we can see that the entrance to the photo album app is 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>


As you can see, when the tab is 1, it will switch to the albums tab, which is what we want.

The final itnent is

if (RomUtils.isHuawei()) {<!-- -->
val intent = Intent()
intent.setClassName("com.huawei.photos", "com.huawei.gallery.app.GalleryMain")
intent.putExtra("tab", 1)
startActivity(intent)
 }

4. Swipe the interface left or right to switch between taking pictures and recording video

Generally, you can switch the photo/video function by sliding the camera left or right. We can monitor the root view with 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
& amp; & amp; Math.abs(mCurPosX - mPosX) > 120
) {<!-- -->
//Swipe left
} else if (mCurPosX - mPosX < 0
& amp; & amp; Math.abs(mCurPosX - mPosX) > 120
) {<!-- -->
//Swipe right
}
}
true
}

5. Blur when switching cameras to achieve Gaussian blur effect

I used the tool class ImageUtils in AndroidUtilCode here, and used ImageUtils.fastBlur to process the Gaussian blur effect.

 val originBitmap = binding.previewView.bitmap
 val blurBitmap = ImageUtils.fastBlur(originBitmap, 0.25F, 25F)
 binding.imgBlur.setImageBitmap(blurBitmap)

There are two points to note here

  1. After the user clicks to switch, the Gaussian blur effect needs to be processed synchronously before switching the camera. This processing time is about 200ms, which is almost imperceptible to the user.
  2. The default implementationMode used by CameraX is performance. I changed it to compatible to get a normal image angle. About implementationModeYou can see the CameraX implementation preview document

The effect is as follows

6. Camera switching 3D flip effect

Mainstream cameras on the market will have a 3D flip effect when switching between front and rear cameras.
I found this article online. It teaches you step by step how to implement the 3D card flipping effect in Android development! , I implemented it by following the steps in the article.

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, defined 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 //Start angle
     * @param toDegrees the end angle of the 3D rotation //End angle
     * @param centerX the X center of the 3D rotation //x central axis
     * @param centerY the Y center of the 3D rotation //y central axis
     * @param reverse true if the translation should be reversed, false otherwise//Whether to reverse
     */
    public Rotate3dAnimation(float fromDegrees, float toDegrees,
            float centerX, float centerY, float depthZ, boolean reverse) {<!-- -->
        mFromDegrees = fromDegrees;
        mToDegrees = toDegrees;
        mCenterX = centerX;
        mCenterY = centerY;
        mDepthZ = depthZ;//The distance the Z axis moves, this affects the visual effect and can solve the seemingly amplified effect of 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
    privateval duration=300
    private var closeAnimation: Rotate3dAnimation? = null

    /**
     *Card text introduces the closing effect: the rotation angle is the same as the reverse direction when opening
     */
    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
        }

        //Take the center point of the rotating object as the rotation center point. Don't get it in the onCreate method because when the view is initially drawn, the width and height obtained are 0.
        centerX = photo1.width / 2
        centerY = photo1.height / 2

        if (closeAnimation == null) {<!-- -->
            initCloseAnim()
        }

        //Used to determine whether the animation is executing when the current click event occurs
        if (closeAnimation!!.hasStarted() & amp; & amp; !closeAnimation!!.hasEnded()) {<!-- -->
            return
        }

        photo1!!.startAnimation(closeAnimation)
    }
}

Then, just use the View you want to flip

val rotate3dManager = Rotate3dManager(targetView)
rotate3dManager.operate()

The effect is as follows

github address: DialogFlipTest

Reprint: https://blog.csdn.net/EthanCo/article/details/126260794