代码是根据<<第一行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 对象,最终将图片显示到界面上
刚刚提到了ContentProvider, 下面我们要在AndroidManifest.xml中注册ContentProvider (新增
的这一块内容).
此外, 还需要配置一下存储的读写权限, 不然会闪退.
<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 资源。当然这个资源现在还是不存在的,下面我们就来创建它。
在xml文件夹中新建一个 xml 文件:
写入如下代码:
<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代码(第三版)>>