Android编程权威指南(第二版)学习笔记(十六)—— 第16章 使用 intent 拍照

本章主要讲了如何使用 intent 拍照,存储照片和展示照片

GitHub 地址:
完成16章,但未完成挑战
完成16章挑战1
完成16章挑战2

1. 外部存储

相机照片动辄几 MB 大小,直接保存在数据库中肯定不现实。很自然,大家会想到直接使用设备的文件系统。
一般来讲,应用都应该使用私有存储空间保存各类文件。还记得吗?在前面章节中,我们在私有存储空间保存过 SQLite 数据文件。使用类似 Context.getFileStreamPath(String)和 Context.getFilesDir()这样的方法,我们也可以实现这样的存储目标,下表所示:

Context 类提供的方法使用目的
File getFilesDir()获取/data/data//files 目录
FileInputStream openFileInput(String name)打开现有文件进行读取
FileOutputStream openFileOutput(String name, int mode)打开文件进行写入,如不存在就创建它
File getDir(String name, int mode)获取/data/data//目录的子目录(如不存在就先创建它)
String[] fileList()获取/data/data//files 目录下的文件列表。可与其他方法配合使用,例如 openFileInput(String)
File getCacheDir()获取/data/data//cache 目录。应注意及时清理该目录,并节约使用空间

如果想存储的文件仅供应用内部使用,使用上表中的各类方法就可以了。而如果想共享文件给其他应用或是接收其他应用的文件(如相机应用拍摄的照片)时,路只有一条:使用外部存储保存文件。
外部存储有两类:主外部存储和其他各类存储介质。所有的 Android 设备至少应有一个主外部存储地。使用Environment.getExternalStorageDirectory()可以返回这个外部存储目录。 以前,这个存储地通常是指 SD 卡,但现在都已基本整合至了设备内部。即使现在还有设备使用扩展外部存储,也应算作其他各类存储介质这一类了。
Context 也提供了一些访问外部存储空间要用到的方法,如下表所示。

方法使用目的
File getExternalCacheDir()获取主外部存储上的缓存文件目录。用法类似 getCacheDir()方法,但要注意,Android 一般不会自动清理该目录
File[] getExternalCacheDirs()获取多个外部存储上的缓存文件目录
File getExternalFilesDir(String)获取主外部存储上存放常规文件的文件目录。通过 String 参数,可访问特定内容类型的子目录。内容类型常量以 DIRECTORY_为前缀,定义在 Environment 中 。 例如 , 用于 图像 文件 的 Environment.DIRECTORY_ PICTURES
File[] getExternalFilesDirs(String)类似 getExternalFilesDir(String)方法,但该方法可获取指定类型的所有文件目录
File[] getExternalMediaDirs()获取 Android 存储图片、视频和音乐文件的所有外部文件目录。和 getExternalFilesDir(Environment.DIRECTORY_PICTURES) 方法 区别 在于,调用该方法,多媒体扫描器会自动扫描目标目录,并将存放的多媒体文件暴露给能够播放音乐、浏览视频和图片的应用。也就是说, getExternalMediaDirs()方法返回目录中存放的任何文件都会自动出现在多媒体应用中

1.1 指定照片存放位置

首先,一张照片的文件名我们用一个 Crime 的 ID 来标识,所以在 Crime.java 中加入了获取文件名的方法:

public String getPhotoFileName() {
    return "IMG_" + getId().toString() + ".jpg";
}

然后在 CrimeLab.java 中加入获取路径文件的函数:

public File getPhotoFile(Crime crime) {
    File externalFilesDir = mContext
            .getExternalFilesDir(Environment.DIRECTORY_PICTURES);

    if (externalFilesDir == null) {
        return null;
    }

    return new File(externalFilesDir, crime.getPhotoFileName());
}

1.2 外部存储使用权限

读写外部存储需要获得权限,一般在AndroidManifest.xml中使用<uses-permission>标签来使用。而对于 API 19(Android 4.4)及以后的新版系统来说,应用不需要再申请 Context.getExternalFilesDir(String) 所需要的权限了,所以这个权限申请是这么写的:

<uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="18"/>

2. 使用相机 intent

实现拍照功能只需要使用一个隐式 intent,分为下面几步:

  • 获取保存图片的文件存储位置
  • 处理拍照按钮,实现触发拍照,其实就是发送一个带有 MediaStore.ACTION_IMAGE_CAPTURE的 intent 即可。

对于 intent 的操作,我们需要定义在 MediaStore 类中的ACTION_CAPTURE_IMAGE。MediaStore 类定义了一些公共接口,可用于处理图像、视频以及音乐这些常见的多媒体任务。当然,这也包括触发相机应用的拍照 intent。

如果只用ACTION_IMAGE_CAPTURE打开相机应用,默认只能拍摄缩略图这样的低分辨率照片,而且照片会保存在 onActivityResult(…)返回的 Intent 对象里。要想获得全尺寸照片,就要让它使用文件系统存储照片。这可以通过传入保存在 MediaStore.EXTRA_OUTPUT 中的指向存储路径的 Uri 来完成。
编写用于拍照的隐式 intent,拍摄的照片应该保存在 mPhotoFile 指定的地方。同时,别忘了检查设备上是否安装有相机应用,以及是否有地方存储照片。

mPhotoButton = (ImageButton) v.findViewById(R.id.crime_camera);
// 首先创建一个用于拍照的 Intent 对象
final Intent captureImage = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// 检查是否有可拍照的应用
boolean canTakePhoto = mPhotoFile != null &&
        captureImage.resolveActivity(packageManager) != null;
mPhotoButton.setEnabled(canTakePhoto);

if (canTakePhoto) {
    // 建立访问照片目录的 Uri
    Uri uri = Uri.fromFile(mPhotoFile);
    // 将该 Uri 放入 intent 对象中
    captureImage.putExtra(MediaStore.EXTRA_OUTPUT, uri);
}

mPhotoButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        // 使用 startActivityForResult 是为了拍完照后刷新视图
        startActivityForResult(captureImage, REQUEST_PHOTO);
    }
});

3. 缩放和显示位图

有了照片,接下来就是找到并加载它,然后展示给用户看。在技术实现上,这需要加载照片到大小合适的 Bitmap 对象中。而要从文件生成 Bitmap 对象,我们需要 BitmapFactory 类:
Bitmap bitmap = BitmapFactory.decodeFile(mPhotoFile.getPath());

Bitmap 是个简单对象,它只存储实际像素数据。也就是说,即使原始照片已压缩过,但存入 Bitmap 对象时,文件并不会同样压缩。因此,如果有一个16万像素24位已压缩为5Mb 大小的 JPG 照片文件,一旦载入 Bitmap 对象,就会立即膨胀至48Mb 大小!
这个问题可以设法解决,但需要手工缩放位图照片。具体做法就是,首先确认文件到底有多大,然后考虑按照给定区域大小合理缩放文件。最后,重新读取缩放后的文件,创建 Bitmap 对象。
既然需要处理图像文件,我们建立一个通用的工具类,名为 PictureUtils.java。在其中添加 getScaledBitmap(String, int, int)缩放方法,

public class PictureUtils {
    public static Bitmap getScaledBitmap(String path, int destWidth, int destHeight) {
        // Read in the dimensions of the image on disk
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path, options);

        float srcWidth = options.outWidth;
        float srcHeight = options.outHeight;

        // Figure out how much to scale down by
        int inSampleSize = 1;
        if (srcHeight > destHeight || srcWidth > destWidth) {
            if (srcWidth > srcHeight) {
                inSampleSize = Math.round(srcHeight / destHeight);
            } else {
                inSampleSize = Math.round(srcWidth / destWidth);
            }
        }

        options = new BitmapFactory.Options();
        options.inSampleSize = inSampleSize;

        // Read in and create final bitmap
        return BitmapFactory.decodeFile(path, options);
    }
}

上述方法中,inSampleSize 值很关键。它决定着缩略图像素的大小。假设这个值是1的话,就表明缩略图和原始照片的水平像素大小一样。如果是2的话,它们的水平像素比就是1∶2。因此,inSampleSize 值为2时,缩略图的像素数就是原始文件的四分之一。
问题总是接踵而来。解决了缩放问题,又冒出了新问题:fragment 刚启动时,PhotoView 究竟有多大无人知道。onCreate(…)、onStart()和 onResume()方法启动后,才会有首个实例化布局出现。也就在此时,显示在屏幕上的视图才会有大小尺寸。这也是出现新问题的原因。
解决方案有两个:要么等布局实例化完成并显示,要么干脆使用保守估算值。特定条件下, 尽管估算比较主观,但确实是一个切实可行的办法。再添加一个 getScaledBitmap(String, Activity)静态 Bitmap 估算方法。

public static Bitmap getScaledBitmap(String path, Activity activity) {
    Point size = new Point();
    activity.getWindowManager().getDefaultDisplay()
            .getSize(size);

    return getScaledBitmap(path, size.x, size.y);
}

4. 功能声明

应用的拍照功能用起来不错,但还有件事情要做:告诉目标用户应用具有拍照功能。

假如应用要用到诸如相机、NFC,或者任何其他的随设备走的功能时,都应该要让 Android 系统知道。否则,假如设备缺少这样的功能,类似 Google Play 商店的安装程序就会拒绝安装应用。
为声明需要使用相机,在 AndroidManifest.xml 中加入<uses-feature>标签:

<uses-feature
    android:name="android.hardware.camera2"
    android:required="false"/>

5. 布局文件中的 <include> 标签

如果有重复的布局可以使用,那么可以采用 include 标签,直接在不同的 layout 中引用。
然而,经验表明,布局文件的优点是可靠又好用。例如,直接查看布局文件内容,就可以快速准确地知道应用视图是如何构建的。然而,一旦用了 include 标签,一切就不好说了。还想明白视图构成的话,就得仔细翻看布局主文件以及所有 include 的布局文件。这种非直观的感觉,极易让人失去耐心。
用户界面是应用改动相对频繁的部分。既然这样,不顾一切地追求复用原则很可能会适得其反。因此,在视图层开发时,我们一定要多多考量,尽量做到审慎、合理地使用 include 标签。

6. 挑战练习

6.1 优化照片显示

新建一个 GlancePictureFragment,继承自 DialogFragment,代码如下:

public class GlancePictureFragment extends DialogFragment {

    private static final String ARG_PATH = "path";

    private ImageView mImage;

    // 由于文件比较大,所以将文件路径传入即可
    public static GlancePictureFragment newInstance(String path) {
        Bundle args = new Bundle();
        args.putString(ARG_PATH, path);
        GlancePictureFragment fragment = new GlancePictureFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // 使用 getArguments() 方法取出照片文件路径
        String path = getArguments().getString(ARG_PATH);

        // 这个新的 style 其实就做了一件事,那就是使窗口全屏
        // 注意如果继承了 @android:Theme.Dialog 的话,窗口
        // 大小就限定了,所以我没有继承
        final Dialog dialog = new Dialog(getActivity(), R.style.CustomDialogTheme);
        // 这个 layout 中只有一个 ImageView
        dialog.setContentView(R.layout.dialog_image_glance);

        mImage = (ImageView) dialog.findViewById(R.id.glance_image);
        // 仍然使用 PictureUtils 类的工具来获得缩放的 Bitmap
        mImage.setImageBitmap(
                PictureUtils.getScaledBitmap(path, getActivity()));
        mImage.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            // 点击图片则退出该 dialog
                dialog.dismiss();
            }
        });
        return dialog;
    }
}

然后在图片的点击事件中声明即可

6.2 优化缩略图加载

首先修改更新视图的函数,接受高宽的指定像素:

private void updatePhotoView(int width, int height) {
    if (mPhotoFile == null || !mPhotoFile.exists()) {
        mPhotoView.setImageDrawable(null);
    } else {
        Bitmap bitmap = PictureUtils.getScaledBitmap(
                mPhotoFile.getPath(), width, height);
        mPhotoView.setImageBitmap(bitmap);
    }
}

之后,先获取 mPhotoView 的 ViewTreeObserver,然后设置 OnGlobalLayoutListener 监听器,在监听器中即可获取视图的高度和宽度,然后进行图片显示。

mPhotoObserver = mPhotoView.getViewTreeObserver();
mPhotoObserver.addOnGlobalLayoutListener(
        new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        updatePhotoView(
                mPhotoView.getWidth(),
                mPhotoView.getHeight());
        Log.i("CrimeFragment", "onGlobalLayout: Observed");
    }
});
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值