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

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

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

1. 外部存储

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

Context 类提供的方法 使用目的
File getFilesDir() 获取/data/data/<packagename>/files 目录
FileInputStream openFileInput(String name) 打开现有文件进行读取
FileOutputStream openFileOutput(String name, int mode) 打开文件进行写入,如不存在就创建它
File getDir(String name, int mode) 获取/data/data/<packagename>/目录的子目录(如不存在就先创建它)
String[] fileList() 获取/data/data/<packagename>/files 目录下的文件列表。可与其他方法配合使用,例如 openFileInput(String)
File getCacheDir() 获取/data/data/<packagename>/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");
    }
});

GitHub Page: kniost.github.io
简书:http://www.jianshu.com/u/723da691aa42

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,820评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,648评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,324评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,714评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,724评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,328评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,897评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,804评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,345评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,431评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,561评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,238评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,928评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,417评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,528评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,983评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,573评论 2 359

推荐阅读更多精彩内容