笔记 - Android10 拍照/从相册选择图片并展示出来 (Java版本)

代码是根据<<第一行Android代码>>中 “调用摄像头和相册” 部分改写的 Java 实例. 文字部分摘自书中内容.

准备工作

在现有的project中, 新建一个module, 命名为CameraAlbumTest.
在这里插入图片描述

Empty Activity 是不支持java的, 选择其他的就可以:
在这里插入图片描述

这里要勾选上 “generate a layout file”, 名称可以默认, 或者改成其他的:
在这里插入图片描述

点击 finish, 然后等待一会儿
在这里插入图片描述

生成的项目目录结构:
在这里插入图片描述

代码部分

在res => layout => activity_main.xml 文件中写入页面代码.
ImageView 是用于将拍到的图片显示出来.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/takePhoto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="拍照" />

    <Button
        android:id="@+id/fromAlbumBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="从相册选择照片" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal" />

</LinearLayout>

主文件 mainActivity 中, 写入如下代码:

package com.example.cameraalbumtest;

import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Uri imageUri = null;
    private File outputImage = null;
    private ActivityResultLauncher<Intent> takePhoto;
    private ActivityResultLauncher<Intent> fromAlumn;

    private ImageView imageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // launch要在onCreate里初始化, 否则会闪退
        initLaunch();

        Button takephoto = findViewById(R.id.takePhoto);
        takephoto.setOnClickListener(this);

        Button fromAlbumBtn = findViewById(R.id.fromAlbumBtn);
        fromAlbumBtn.setOnClickListener(this);

        imageView = findViewById(R.id.imageView);
    }

    private void initLaunch() {
        takePhoto = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
            @Override
            public void onActivityResult(ActivityResult result) {
                Bitmap bitmap = null;
                try {
                    if(result.getResultCode() == Activity.RESULT_OK) {
                        bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
                    }
                } catch (FileNotFoundException e) {
                    throw new RuntimeException(e);
                }
                try {
                    imageView.setImageBitmap(rotateIfRequired(bitmap));
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        fromAlumn = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
            @Override
            public void onActivityResult(ActivityResult result) {
                Intent data = result.getData();
                if(result.getResultCode() == Activity.RESULT_OK && data != null) {
                    // 将选择的图片显示
                    Bitmap bitmap = null;
                    try {
                        bitmap = getBitmapFromUri(data.getData());
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    imageView.setImageBitmap(bitmap);
                }
            }
        });
    }

    private Bitmap rotateIfRequired(Bitmap bitmap) throws IOException {
        ExifInterface exif = new ExifInterface(outputImage.getPath());
        int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
        Bitmap returnedBitmap = null;
        switch (orientation) {
            case ExifInterface.ORIENTATION_ROTATE_90:
                returnedBitmap = rotateBitmap(bitmap,90);
                break;
            case ExifInterface.ORIENTATION_ROTATE_180:
                returnedBitmap = rotateBitmap(bitmap,180);
                break;
            case ExifInterface.ORIENTATION_ROTATE_270:
                returnedBitmap = rotateBitmap(bitmap,270);
                break;
            default:
                returnedBitmap = bitmap;
                break;
        }
        return returnedBitmap;
    }

    private Bitmap rotateBitmap(Bitmap bitmap,int degree) {
        Matrix matrix = new Matrix();
        matrix.postRotate((float) degree);
        Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        bitmap.recycle(); // 将不需要的bitmap对象回收
        return rotatedBitmap;
    }

    private Bitmap getBitmapFromUri(Uri uri) throws IOException {
        ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(uri,"r");
        Bitmap image = null;
        if (parcelFileDescriptor != null){
            FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
            image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
        }
        return image;
    }

    @Override
    public void onClick(View v) {
        if(v.getId() == R.id.takePhoto) {
            // 相机
            // 创建File对象,用于存储拍照后的图片
            outputImage = new File(getExternalCacheDir(),"output_image.jpg");
            if(outputImage.exists()) {
                outputImage.delete();
            }
            try {
                if (outputImage.createNewFile()){
                    Log.i("create", "新建文件成功 ");
                } else {
                    Log.i("create", "新建文件失败 ");
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            // 判断Android版本, 7.0以上和以下处理方式不同
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                // 高于7.0
                imageUri = FileProvider.getUriForFile(this,"com.example.cameraalbumtest.fileprovider",outputImage);
            } else {
                imageUri = Uri.fromFile(outputImage);
            }
            // 启动相机程序
            Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
            intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
            takePhoto.launch( intent );
        } else if (v.getId() == R.id.fromAlbumBtn) {
            // 从相册选择图片
            // 打开文件选择器
            Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            // 指定显示图片
            intent.setType("image/*");
            fromAlumn.launch(intent);
        }
    }

}

主文件解析

拍照部分

我们首先这里创建了一个 File 对象,用于存放摄像头拍下的图片,这里我们把图片命名为output_image.jpg,并存放在手机 SD卡的应用关联缓存目录下。什么叫作应用关联缓存目录呢?就是指 SD 卡中专门用于存放当前应用缓存数据的位置,调用 getExternalCacheDir()方法可以得到这个目录,具体的路径是/sdcard/Android/data//cache。那么为什么要使用应用关联缓存目录来存放图片呢?因为从 Android 6.0 系统开始,读写SD卡被列为了危险权限,如果将图片存放在 SD 卡的任何其他目录,都要进行运行时权限处理才行,而使用应用关联目录则可以跳过这一步。另外,从 Android 10.0系统开始,公有的 SD卡目录已经不再允许被应用程序直接访问了,而是要使用作用域存储才行。

接着会进行一个判断, 如果运行设备的系统版本低于 Android 7.0,就调用Uri 的fromFile()方法将 File 对象转换成 Uri 对象,这个 Uri 对象标识着 utput image.jpg 这张图片的本地真实路径。否则,就调用 FileProvider 的 getUriForFile()方法将 File 对象转换成一个封装过的Uri对象。

getUriForFile()方法接收 3 个参数:第一个参数要求传入 Context 对象,第二个参数可以是任意唯一的字符串,第三个参数则是我们刚刚创建的 File 对象。之所以要进行这样一层转换,是因为从 Android 7.0 系统开始,直接使用本地真实路径的 Uri 被认为是不安全的,会抛出一个 FileUriExposedException 异常。而 FileProvider 则是一种特殊的 ContentProvider,它使用和 ContentProvider 类似的机制来对数据进行保护,可以选择性地将封装过的 Uri 共享给外部从而提高了应用的安全性。

接下来构建了一个Intent对象, 并将这个Intent的action 指定为android.media.actionIMAGE CAPTURE,再调用 Intent 的 putExtra()方法指定图片的输出地址,这里填入刚刚得到的Uri 对象,最后调用 startActivityForResult()启动 Activity。由于我们使用的是一个隐式Intent,系统会找出能够响应这个 Intent的Activity 去启动,这样照相机程序就会被打开,拍下的照片将会输出到 outpu_ image.jpg中。

这里有个问题, 书上提到的是使用startActivityForResult()来获取返回数据的, 但是现在startActivityForResult()这个方法已经被废弃了, 编译器里也会有提示. 所以我们这里采用的是 registerForActivityResult 这个API

如果发现拍照成功,就可以调用 BitmapFactory 的decodeStream()方法将 output image,jpg这张照片解析成 Bitmap 对象,然后把它设置到 ImageView中显示出来。

需要注意的是,调用照相机程序去拍照有可能会在一些手机上发生照片旋转的情况。这是因为这些手机认为打开摄像头进行拍摄时手机就应该是横屏的,因此回到竖屏的情况下就会发生90 度的旋转。为此,这里我们又加上了判断图片方向的代码,如果发现图片需要进行旋转,那么就先将图片旋转相应的角度,然后再显示到界面上。

从相册获取图片部分

在“FromAlbum”按钮的点击事件里,我们先构建了一个Intent对象,并将它的 action 指定为Intent.ACTION_OPEN_DOCUMENT,表示打开系统的文件选择器。接着给这个Intent对象设置一些条件过滤,只允许可打开的图片文件显示出来,然后调用 startActivityForResult()方法即可。

接下来的部分就很简单了,我们调用了返回 Intent的 getData()方法来获取选中图片的Uri.然后再调用 getBitmapFromUri()方法将 Uri 转换成 Bitmap 对象,最终将图片显示到界面上

其他配置

AndroidManifest.xml

刚刚提到了ContentProvider, 下面我们要在AndroidManifest.xml中注册ContentProvider (新增 <provider> 的这一块内容).

此外, 还需要配置一下存储的读写权限, 不然会闪退.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <!--外部存储的写权限-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!--外部存储的读权限-->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MainActivity">
        <provider
            android:authorities="com.example.cameraalbumtest.fileprovider"
            android:name="androidx.core.content.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true"
            tools:ignore="WrongManifestParent">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

android:name 属性的值是固定的,而 android:authorities 属性的值必须和刚才FileProvider.getUriForFile()方法中的第二个参数一致。另外,这里还在标签的内部使用指定 Uri 的共享路径,并引用了一个@xml/file_paths 资源。当然这个资源现在还是不存在的,下面我们就来创建它。

res目录

右击 res 目录, 新建一个 xml目录:
在这里插入图片描述

在这里插入图片描述

在xml文件夹中新建一个 xml 文件:

在这里插入图片描述
在这里插入图片描述

写入如下代码:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="my_images"
        path="/" />
</paths>

external-path 就是用来指定 Uri 共享路径的,name 属性的值可以随便填、path 属性的值表示共享的具体路径。这里使用一个单斜线表示将整个 SD卡进行共享,当然你也可以仅共存放output_image.jpg这张图片的路径。

补充

调用摄像头拍照以及从相册中选择图片是很多 Android应用都会带有的功能,现在你已经将这两种技术都学会了,如果将来在工作中需要开发类似的功能,相信你一定能轻松完成的。

不过,目前我们的实现还不算完美,因为如果某些图片的像素很高,直接加载到内存中就有可能会导致程序崩溃。更好的做法是根据项目的需求先对图片进行适当的压缩,然后再加载到内存中。至于如何对图片进行压缩,就要考验你查阅资料的能力了,这里就不再展开进行讲解了。

-------文章内容摘自<<第一行Android代码(第三版)>>

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值