众所周知,调用相机拍照和图库中获取图片的功能,基本上是每个程序App必备的。

实现适配Android每个版本,国内手机,要处理的问题却也不少。例如:Android6.0权限问题,Android7.0 FileProvider问题,华为手机图库获取不到图片的问题。

本篇内容概述

  • 调用系统相机拍照

  • 图库选取图片

  • 处理华为图库获取不到图片问题

  • 处理部分手机拍照后,图片旋转角度问题

  • RxJava加载图片,向上取整计算合适比例。

  • EasyPermission库处理去读写权限( 适配Android6.0系统及其以上)

  • FileProvider访问文件(适配Android7.0系统及其以上)

  • 跳转其他程序,Activity被系统因内存不足回收,处理数据保存问题。

项目前期配置

依赖库添加

dependencies {    compile fileTree(include: ['*.jar'], dir: 'libs')    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {        exclude group: 'com.android.support', module: 'support-annotations'    })    compile 'com.android.support:appcompat-v7:26.+'    compile 'com.android.support.constraint:constraint-layout:1.0.2'    testCompile 'junit:junit:4.12'    //谷歌官方权限库    compile 'pub.devrel:easypermissions:1.0.1'    //异步消息通知库    compile 'io.reactivex:rxjava:1.3.3'    compile 'io.reactivex:rxandroid:1.2.1'}

编码方式:Java+retrolambda库实现Java8特性

Android拍照功能


1. 赋予读写权限

从Android6.0开始,需要动态赋予权限,而不是安装时候赋予权限。拍照功能需要用到写入磁盘的权限。

在AndroidManifest.xml中注册读写权限:

 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE">

第一步:检查权限和申请读写权限。 这里,使用EasyPermission库处理权限问题。

public class MainActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks{    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        checkWritePermission();    }    /**     * 检查读写权限权限     */    private void checkWritePermission() {        boolean result = PermissionManager.checkPermission(this, Constance.PERMS_WRITE);        if (!result) {            PermissionManager.requestPermission(this, Constance.WRITE_PERMISSION_TIP                 , Constance.WRITE_PERMISSION_CODE, Constance.PERMS_WRITE);        }    }    /**     * 重写onRequestPermissionsResult,用于接受请求结果     *     * @param requestCode     * @param permissions     * @param grantResults     */    @Override    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {        super.onRequestPermissionsResult(requestCode, permissions, grantResults);        //将请求结果传递EasyPermission库处理        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);    }    /**     * 请求权限成功     *     * @param requestCode     * @param perms     */    @Override    public void onPermissionsGranted(int requestCode, List perms) {        ToastUtils.showToast(getApplicationContext(), "用户授权成功");    }    /**     * 请求权限失败     *     * @param requestCode     * @param perms     */    @Override    public void onPermissionsDenied(int requestCode, List perms) {        ToastUtils.showToast(getApplicationContext(), "用户授权失败");        /**         * 若是在权限弹窗中,用户勾选了'NEVER ASK AGAIN.'或者'不在提示',且拒绝权限。         * 这时候,需要跳转到设置界面去,让用户手动开启。         */        if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {            new AppSettingsDialog.Builder(this).build().show();        }    }}

权限管理类PermissionManager,代码如下:

public class PermissionManager {    /**     * @param context     * return true:已经获取权限     * return false: 未获取权限,主动请求权限     */   // @AfterPermissionGranted(Constance.WRITE_PERMISSION_CODE) 是可选的    public static boolean checkPermission(Activity context, String[] perms) {        return EasyPermissions.hasPermissions(context, perms);    }    /**     * 请求权限     * @param context     */    public static void requestPermission(Activity context,String tip,int requestCode,String[] perms) {        EasyPermissions.requestPermissions(context, tip,requestCode,perms);    }}

更多详情,请阅读Android EasyPermissions官方库,高效处理权限。

2. Intent调用相机进行拍照

开启相机拍照是通过Intent来实现,在Intent中指定输出图片路径,相机拍照成功后,系统会将图片数据自动输出到指定路径,生成对应的图片。关闭相机后,会在对应的Activity中的onActivityResult()中返回结果,是否拍照成功的标示。

private String picturePath;/**  *Activity中通过Intent调用相机,指定输出图片路径。  */@Overridepublic void camera() {        this.picturePath = FileUtils.getBitmapDiskFile(this.getApplicationContext());        CameraUtils.openCamera(this, Constance.PICTURE_CODE, this.picturePath);}public class CameraUtils {    /**     * 打开相机     * @param context     * @param requestCode     * @return     */    public static void openCamera(Activity context, int requestCode, String picturePath){        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);        if (intent.resolveActivity(context.getPackageManager()) != null) {            /**             * 指定拍照存储路径             * 7.0 及其以上使用FileProvider替换'file://'访问             */            if (Build.VERSION.SDK_INT>=24){                //这里的BuildConfig,需要是程序包下BuildConfig。                intent.putExtra(MediaStore.EXTRA_OUTPUT,                          FileProvider.getUriForFile(context.getApplicationContext(),                         BuildConfig.APPLICATION_ID+".provider",new File(picturePath)));                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);            }else{                intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(picturePath)));            }            context.startActivityForResult(intent, requestCode);        }    }}

这里你会发觉多了,一个匹配android7.0的FileProvider,用于处理file://访问的问题。接下来,会讲解到它。

工具类FileUtils生成图片的路径,代码如下:

public class FileUtils {    /**     * 获得存储bitmap的文件     * getExternalFilesDir()提供的是私有的目录,在app卸载后会被删除     *     * @param context     * @param     * @return     */    public static String getBitmapDiskFile(Context context) {        String cachePath;        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())                || !Environment.isExternalStorageRemovable()) {            cachePath = context.getExternalFilesDir(DIRECTORY_PICTURES).getAbsolutePath();        } else {            cachePath =context.getFilesDir().getAbsolutePath();        }        return new File(cachePath +File.separator+ getBitmapFileName()).getAbsolutePath();    }    public static final String bitmapFormat = ".png";    /**     * 生成bitmap的文件名:日期,md5加密     *     * @return     */    public static String getBitmapFileName() {        StringBuilder stringBuilder = new StringBuilder();        try {            final MessageDigest mDigest = MessageDigest.getInstance("MD5");            String currentDate = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());            mDigest.update(currentDate.getBytes("utf-8"));            byte[] b = mDigest.digest();            for (int i = 0; i < b.length; ++i) {                String hex = Integer.toHexString(0xFF & b[i]);                if (hex.length() == 1) {                    stringBuilder.append('0');                }                stringBuilder.append(hex);            }        } catch (Exception e) {            e.printStackTrace();        }        String fileName = stringBuilder.toString() + bitmapFormat;        return fileName;    }}

3. 处理anroid7.0中禁止file的Uri问题

anroid7.0 行为变更:

android 7.0发生了一些行为变化,禁止应用程序向外部公开file://的URI。

尝试传递file://URI会触发FileUriExposedException。

应用程序之间共享数据,应该发送content://的URI,且授予URI临时访问权限。推举使用FileProvider。更多详情,阅读android 7.0行为变更。

配置FileProvider:

在src\main\res路径下创建xml文件夹,然后在创建一个provider_paths.xml文件,编写以下代码

<?xml version="1.0" encoding="utf-8"?><paths xmlns:android="http://schemas.android.com/apk/res/android">    <files-path name="Pictures" path="/">files-path>    <external-path path="Android/data/${applicationId}/" name="files_root" />    <root-path        name="root"        path="/" />paths>

接下来,在AndroidManifest.xml中注册FileProvider:为FileProvidre配置,指定authorities,name ,不许对外共享,临时授权,访问目录配置

             <provider            android:name="android.support.v4.content.FileProvider"           android:authorities="${applicationId}.provider"            android:exported="false"            android:grantUriPermissions="true">            <meta-data                android:name="android.support.FILE_PROVIDER_PATHS"                android:resource="@xml/provider_paths"/>        provider>

配置完成后,便可以直接使用定义authorities所对应的FileProvider。

更多详情,请阅读Android 7.0 报android.os.FileUriExposedException异常。

若是配置过程中遇到问题,请阅读Android FileProvider配置报错android.content.pm.ProviderInfo.loadXmlMetaData问题。

4. 处理系统内存不足时候,导致界面回收,数据丢失的问题

当跳转到其它运行程序时候,系统可能因内存不足,回收了当前的Activity。而Activity当前数据没有保存,即使系统重新创建该Activity后,也会出现空白页面。

当系统因内存不足,回收activity前,会执行onSaveInstanceState(Bundle outState),因此,将拍照后的图片路径存储起来。

private String picturePath;   /**     * 防止系统内存不足销毁Activity     * ,这里保存数据,便于恢复。     * @param outState     */    @Override    protected void onSaveInstanceState(Bundle outState) {        super.onSaveInstanceState(outState);        outState.putString(TAG, picturePath);    }

当系统重新创建该Activity后,从onCreate()中参数中获取,图片路径:

    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        recoverState(savedInstanceState);    }    /**     * 恢复被系统销毁的数据     * @param savedInstanceState     */    private void recoverState(Bundle savedInstanceState) {        if (savedInstanceState != null) {            this.picturePath = savedInstanceState.getString(TAG);        }    }

这里,举一个例子:

一个界面需要拍照很多张图片,然后显示出。因需要多次打开相机程序,再返回来加载生成的图片。这种需要,铁定容易碰到以上问题。

Activity被系统回收,具备偶然性,但存在问题,终究还是要处理。

这里,延伸一点:

android保存数据,要么放在内存中,要么放在磁盘中。磁盘读写是IO操作,又得筛选数据,面对这种需求,不推举使用。

5. RxJava异步加载拍照图片,向上取整加载

当拍照完成或者取消,都会在Activity的onActivityResult()中返回结果,是否拍照成功的标示。

在磁盘中生成的图片是一个文件,加载文件是IO操作,耗时,考虑RxJava异步加载。

   @Override    protected void onActivityResult(int requestCode, int resultCode, Intent data) {        super.onActivityResult(requestCode, resultCode, data);        switch (requestCode) {            //拍照返回            case Constance.PICTURE_CODE:                if (resultCode == Activity.RESULT_OK) {                    loadPictureBitmap();                }                break;            default:                break;        }    }   private void loadPictureBitmap() {       Observable bitmapObservable= ObservableUtils.loadPictureBitmap(getApplicationContext(), picturePath, show_iv);        executeObservableTask(bitmapObservable);    }    private void executeObservableTask(Observable observable) {        Subscription subscription = observable                .subscribeOn(Schedulers.io())                .observeOn(AndroidSchedulers.mainThread())                .subscribe(bitmap ->                                show_iv.setImageBitmap(bitmap)                        , error ->                                ToastUtils.showToast(getApplicationContext(), "加载图片出错")                );        this.compositeSubscription.add(subscription);    }

一个工具类ObservableUtils,构建Observable对象:

public class ObservableUtils {    /**     * 加载拍照的相片     *     * @param context     * @param picturePath     * @param imageView     * @return     */    public static Observable loadPictureBitmap(Context context, String picturePath, ImageView imageView) {        return Observable.create(subscriber -> {            Bitmap bitmap = BitmapUtils.decodeFileBitmap(context, picturePath                            , imageView.getWidth(), imageView.getHeight());            subscriber.onNext(bitmap);        });    }}

在Activity中显示的ImageView是具备大小的,按尺寸加载对应比率的Bitamp,可以节省内存。这里采用向上取整方式,计算合适的比率。

public class BitmapUtils {    /**     * @param context     * @param path     * @param targetWith     * @param targetHeight     * @return     */    public synchronized static Bitmap decodeFileBitmap(Context context, String path, int targetWith, int targetHeight) {        try {            BitmapFactory.Options options = new BitmapFactory.Options();            options.inJustDecodeBounds = true;            decodeStreamToBitmap(context, path, options);            options.inSampleSize = calculateScaleSize(options, targetWith, targetHeight);            options.inJustDecodeBounds = false;            Bitmap bitmap = decodeStreamToBitmap(context, path, options);            return getNormalBitmap(bitmap, path);        } catch (Exception e) {            e.printStackTrace();        }        return null;    }    private static Bitmap decodeStreamToBitmap(Context context, String path, BitmapFactory.Options options) {        Bitmap bitmap = null;        ContentResolver contentResolver = context.getContentResolver();        try {            //MIME type需要添加前缀            InputStream inputStream = contentResolver.openInputStream(                       Uri.parse(path.contains("file:") ? path : "file://" + path));            bitmap = BitmapFactory.decodeStream(inputStream, null, options);            inputStream.close();        } catch (Exception e) {            e.printStackTrace();        }        return bitmap;    }    /**     * 采用向上取整的方式,计算压缩尺寸     *     * @param options     * @param targetWith     * @param targetHeight     * @return     */    private static int calculateScaleSize(BitmapFactory.Options options, int targetWith, int targetHeight) {        int simpleSize;        if (targetWith > 0 && targetHeight > 0) {            int scaleWith = (int) Math.ceil((options.outWidth * 1.0f) / targetWith);            int scaleHeight = (int) Math.ceil((options.outHeight * 1.0f) / targetHeight);            simpleSize = Math.max(scaleWith, scaleHeight);        } else {            simpleSize = 1;        }        if (simpleSize == 0) {            simpleSize = 1;        }        return simpleSize;    }}

细心的人会发觉getNormalBitmap(bitmap, path),这个用于处理图片旋转的问题。

6. 处理部分手机拍照后,图片旋转角度问题

当图片角度旋转后,若是直接加载出来,对用户体验是非常差劲的。可通过ExifInterface对象,进行角度判断,加以处理。

 /**     * 根据存储的bitmap中旋转角度,来创建正常的bitmap     *     * @param bitmap     * @param path     * @return     */    private static Bitmap getNormalBitmap(Bitmap bitmap, String path) {        int rotate = getBitmapRotate(path);        Bitmap normalBitmap;        switch (rotate) {            case 90:            case 180:            case 270:                try {                    Matrix matrix = new Matrix();                    matrix.postRotate(rotate);                    normalBitmap = Bitmap.createBitmap(bitmap, 0, 0,                                  bitmap.getWidth(), bitmap.getHeight(), matrix, true);                    if (bitmap != null && !bitmap.isRecycled()) {                        bitmap.recycle();                    }                } catch (Exception e) {                    e.printStackTrace();                    normalBitmap = bitmap;                }                break;            default:                normalBitmap = bitmap;                break;        }        return normalBitmap;    }    /**     * ExifInterface :这个类为jpeg文件记录一些image 的标记     * 这里,获取图片的旋转角度     *     * @param path     * @return     */    private static int getBitmapRotate(String path) {        int degree = 0;        try {            ExifInterface exifInterface = new ExifInterface(path);            int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,                               ExifInterface.ORIENTATION_NORMAL);            switch (orientation) {                case ExifInterface.ORIENTATION_ROTATE_90:                    degree = 90;                    break;                case ExifInterface.ORIENTATION_ROTATE_180:                    degree = 180;                    break;                case ExifInterface.ORIENTATION_ROTATE_270:                    degree = 270;                    break;                default:                    break;            }        } catch (Exception e) {            e.printStackTrace();        }        return degree;    }

实现一个完美的拍照功能,填了6个坑,真心不容易,相信不少的开发者都遇到过这些问题。接下来,检验成果的时候到了。

运行效果

Android图库功能


实现图库选择相片的代码很简单,通过Intent开启图库,然后选择需要的图片,会在activity中onActivityResult()中返回Uri。接下来,根据Uri查询到对应的图片路径,最后根据路径加载Bitmap,显示到UI上。

1. 读取权限处理

图库也是需要读取权限的,但上面的拍照功能具备了写入权限,写入权限包含读取权限,因此,这里不需要再做处理。更多详情,可以阅读 Android 6.0 访问图库时,报错 requires android.permission.READ_EXTERNAL_STORAGE异常.

2. 通过Intent开启相册

    /**     * 打开图库     * @param context     * @param requestCode     */    public static void openGallery(Activity context, int requestCode) {        Intent intent = new Intent(Intent.ACTION_PICK, null);        intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,"image/*");        context.startActivityForResult(intent, requestCode);    }

3. 处理图库程序返回的Uri:

    @Override    protected void onActivityResult(int requestCode, int resultCode, Intent data) {        super.onActivityResult(requestCode, resultCode, data);        switch (requestCode) {            //图库返回            case Constance.GALLERY_CODE:                if (resultCode == Activity.RESULT_OK) {                    Uri uri = data.getData();                    loadGalleryBitmap(uri);                }                break;            default:                break;        }    }

很多小伙伴们都发觉,在华为某些型号的手机上,通过图库返回的Uri,查询不出来对应的图片路径。这就相当悲催了的事情。

4. 处理华为手机图库查询不到图片路径

除开权限问题外,还有处理Uri的authority问题。

采用RxJava执行异步操作,处理Uri查询图片路径,根据路径加载合适的bitmap。

    private void loadPictureBitmap() {       Observable bitmapObservable= ObservableUtils.loadPictureBitmap(                              getApplicationContext() , picturePath, show_iv);        executeObservableTask(bitmapObservable);    }    private void executeObservableTask(Observable observable) {        Subscription subscription = observable                .subscribeOn(Schedulers.io())                .observeOn(AndroidSchedulers.mainThread())                .subscribe(bitmap ->                                show_iv.setImageBitmap(bitmap)                        , error ->                                ToastUtils.showToast(getApplicationContext(), "加载图片出错")                );        this.compositeSubscription.add(subscription);    }

查询到图片路径后,直接生成对应的bitmap:

public class ObservableUtils {     /**     * 加载拍照的相片     *     * @param context     * @param picturePath     * @param imageView     * @return     */    public static Observable loadPictureBitmap(Context context, String picturePath, ImageView imageView) {        return Observable.create(subscriber -> {            Bitmap bitmap = BitmapUtils.decodeFileBitmap(context, picturePath                              , imageView.getWidth(), imageView.getHeight());            subscriber.onNext(bitmap);        });    }    /**     * 加载图库中选取的相片     * @param context     * @param uri     * @param imageView     * @return     */    public static Observable loadGalleryBitmap(Context context, Uri uri, ImageView imageView) {        return Observable.create(subscriber -> {            String picturePath = CameraUtils.uriConvertPath(context, uri);            subscriber.onNext(picturePath);        }).flatMap(path -> loadPictureBitmap(context, (String) path, imageView));    }}

解决方法来源于网络:

public class CameraUtils {    /**     * 从相册中返回的Uri查询到对应图片的Path     * @param context     * @param uri     * @return     */    public static String uriConvertPath(Context context,Uri uri){        String path = null;        String scheme = uri.getScheme();        if (scheme.equals("content")) {            path =getPath(context, uri);        } else {            path = uri.getEncodedPath();        }        return path;    }    /**     * 
功能简述:4.4及以上获取图片的方法 *
功能详细描述: *
注意: * @param context * @param uri * @return */
@TargetApi(Build.VERSION_CODES.KITKAT) private static String getPath(final Context context, final Uri uri) { final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; // DocumentProvider if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider if (isExternalStorageDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { return Environment.getExternalStorageDirectory() + "/" + split[1]; } } // DownloadsProvider else if (isDownloadsDocument(uri)) { final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); return getDataColumn(context, contentUri, null, null); } // MediaProvider else if (isMediaDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; Uri contentUri = null; if ("image".equals(type)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if ("video".equals(type)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if ("audio".equals(type)) { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } final String selection = "_id=?"; final String[] selectionArgs = new String[] { split[1] }; return getDataColumn(context, contentUri, selection, selectionArgs); } } // MediaStore (and general) else if ("content".equalsIgnoreCase(uri.getScheme())) { if (isGooglePhotosUri(uri)){ return uri.getLastPathSegment();} return getDataColumn(context, uri, null, null); } // File else if ("file".equalsIgnoreCase(uri.getScheme())) { return uri.getPath(); } return null; } private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; final String column = "_data"; final String[] projection = { column }; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { final int index = cursor.getColumnIndexOrThrow(column); return cursor.getString(index); } } finally { if (cursor != null){ cursor.close();} } return null; } /** * @param uri The Uri to check. * @return Whether the Uri authority is ExternalStorageProvider. */ private static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is DownloadsProvider. */ private static boolean isDownloadsDocument(Uri uri) { return "com.android.providers.downloads.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is MediaProvider. */ private static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is Google Photos. */ private static boolean isGooglePhotosUri(Uri uri) { return "com.google.android.apps.photos.content".equals(uri.getAuthority()); }}

踩完坑,直接看效果如何。

5. 效果如下

Android的拍照和图库选择图片功能介绍完了,期间遇到的坑,心里都有数。本项目的代码也会分享出来,下面有连接。

项目案例:https://github.com/13767004362/EasyPermissionDemo


相关资源

  • Android 6.0 访问图库时,报错 requires android.permission.READ_EXTERNAL_STORAGE异常
  • Android EasyPermissions官方库,高效处理权限
  • Android 7.0 报android.os.FileUriExposedException异常
  • Android FileProvider配置报错android.content.pm.ProviderInfo.loadXmlMetaData问题
  • Android 7.0处理系统裁剪功能异常(适配版)

更多相关文章

  1. 最全的android图片加密
  2. Android中Gif图片的显示
  3. 让Android程序获得系统的权限,实现关机重启,静默安装等功能
  4. Android 蓝牙搜索不到设备(android M权限问题)
  5. Android平台上如何让应用程序获得系统权限以及如何使用platform
  6. android 手机安装应用程序(APK)权限详细对照表
  7. Android加载drawable中图片后自动缩放的原理
  8. Android所设计的权限
  9. Android仿人人客户端(v5.7.1)——对从服务器端(网络)获取的图片进行

随机推荐

  1. 安裝 Android(安卓)開發工具
  2. Android(安卓)SDK版本和ADT版本
  3. Android应用程序资源
  4. Android传感器(第一篇)
  5. Android(安卓)Framework 分析---3Package
  6. Android与linux的区别与联系
  7. Android(安卓)告急!
  8. Android学习方向
  9. flutter与android混合开发一:Android原生
  10. Android程序员指南(3)