程序员人生 网站导航

跟Google学写代码:Interacting with Other Apps【Capture Photo from phone】

栏目:综合技术时间:2016-08-05 13:45:24

本文概述


翻译 Interacting with Other Apps相干课程,并通过温习该文档的知识,完成以下功能:

这里写图片描述

与其他利用交互

我们开发的Android 利用1般具有若干个Activity。每一个Activity显示1个用户界面,用户可通过该界面履行特定任务(比如,查看地图或拍照)。要将用户从1个Activity转至另外一Activity,必须使用 Intent 定义当前利用做某事的“意向”。 当使用诸如 startActivity() 的方法将 Intent 传递至系统时,系统会使用 Intent 辨认和启动相应的利用组件。使意图向乃至可让当前利用启动另外一个利用中包括的Activity。

Intent 可以为 显式 以便启动特定组件(特定的 Activity 实例)或隐式 以便启动处理意向操作(比如“拍摄照片”)的任何组件。

本文将展现如何使用 Intent 履行与其他利用的1些基本交互操作,比如启动另外一个利用、接收来自该利用的结果和使我们的利用能够响应来自其他利用的意向。

向其他利用发送信息


必须使意图向(Intent)在自己利用中的Activity之间进行导航。通常使用明确意向履行此操作,该意向定义开发者希望启动的组件的确切类名称。
但是,当我们希望另外一利用履行操作时,比如“查看地图”,就必须使用隐含义向。

构建隐含义向


隐含义向就是不指定名称,而指定动作行动,比如拍照,查看地图,发送邮件等

  • 例如,此处显示如何使用指定电话号码的 Uri 数据创建发起电话呼唤的意向:

    Uri number = Uri.parse("tel:5551234"); Intent callIntent = new Intent(Intent.ACTION_DIAL, number);
  • 查看地图:

    // Map point based on address Uri location = Uri.parse("geo:0,0?q=1600+Amphitheatre+Parkway,+Mountain+View,+California"); // Or map point based on latitude/longitude // Uri location = Uri.parse("geo:37.422219,⑴22.08364?z=14"); // z param is zoom level Intent mapIntent = new Intent(Intent.ACTION_VIEW, location);
  • 查看网页

    Uri webpage = Uri.parse("http://www.android.com"); Intent webIntent = new Intent(Intent.ACTION_VIEW, webpage);

其他类型的隐含义向需要提供不同数据类型(比如,字符串)的“额外”数据。 我们可使用各种 putExtra() 方法添加1条或多条额外数据。

默许情况下,系统基于所包括的 Uri 数据肯定意向需要的相应 MIME 类型。如果未在乎向中包括 Uri,通常应使用 setType() 指定与意向关联的数据的类型。 设置 MIME 类型可进1步指定哪些类型的Activity应接收意向。

  • 比如发送邮件:

    Intent emailIntent = new Intent(Intent.ACTION_SEND); // The intent does not have a URI, so declare the "text/plain" MIME type emailIntent.setType(HTTP.PLAIN_TEXT_TYPE); emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {"jon@example.com"}); // recipients emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Email subject"); emailIntent.putExtra(Intent.EXTRA_TEXT, "Email message text"); emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse("content://path/to/email/attachment")); // You can also attach multiple items by passing an ArrayList of Uris
  • 日历事件

    Intent calendarIntent = new Intent(Intent.ACTION_INSERT, Events.CONTENT_URI); Calendar beginTime = Calendar.getInstance().set(2012, 0, 19, 7, 30); Calendar endTime = Calendar.getInstance().set(2012, 0, 19, 10, 30); calendarIntent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, beginTime.getTimeInMillis()); calendarIntent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endTime.getTimeInMillis()); calendarIntent.putExtra(Events.TITLE, "Ninja class"); calendarIntent.putExtra(Events.EVENT_LOCATION, "Secret dojo");

做点事情,总希望得到1个反馈对吧?那末构建终了意向,现在我们需要肯定是不是有利用接收;

确认是不是存在接收意向的利用

注意:如果调用了意向,但装备上没有可用于处理意向的利用,利用将崩溃

要确认是不是存在可响应意向的可用Activity,请调用 queryIntentActivities() 来获得能够处理Intent 的Activity列表。 如果返回的 List 不为空,可以安全地使用该意向。例如

PackageManager packageManager = getPackageManager(); List activities = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); boolean isIntentSafe = activities.size() > 0;

如果 isIntentSafe 是 true,则最少有1个利用将响应当意向。 如果它是 false,则没有任何利用处理该意向。

启动目标Activity


1旦已创建 Intent 并设置附加信息,调用 startActivity() 将其发送给系统 。如果系统辨认可处理意向的多个Activity,它会为用户显示对话框供其选择要使用的利用,如图 1 所示。 如果只有1个Activity处理意向,系统会立即开始这个Activity。
这里写图片描述 图1
此处显示完全的示例:如何创建查看地图的意向,确认是不是存在处理意向的利用,然后启动它:

// Build the intent Uri location = Uri.parse("geo:0,0?q=1600+Amphitheatre+Parkway,+Mountain+View,+California"); Intent mapIntent = new Intent(Intent.ACTION_VIEW, location); // Verify it resolves PackageManager packageManager = getPackageManager(); List<ResolveInfo> activities = packageManager.queryIntentActivities(mapIntent, 0); boolean isIntentSafe = activities.size() > 0; // Start an activity if it's safe if (isIntentSafe) { startActivity(mapIntent); }

显示利用选择器


注意,当通过将 Intent 传递至 startActivity() 而开始Activity时,有多个利用响应意向,用户可以选择默许使用哪一个利用(通过选中对话框底部的复选框;见图 1。 当履行用户通常希望每次使用相同利用进行的操作时,比如当打开网页(用户可能只使用1个网页阅读器)或拍照(用户可能习惯使用1个照相机)时,这非常有用。

但是,如果要履行的操作可由多个利用处理并且用户可能习惯于每次选择不同的利用,—比如“同享”操作,用户有多个利用分享项目—,应明确显示选择器对话框如图 2 所示。 选择器对话框强迫用户选择用于每次操作的利用(用户不能对此操作选择默许的利用)。

这里写图片描述 图2

要显示选择器,使用 createChooser() 创建Intent 并将其传递至 startActivity()。例如:

Intent intent = new Intent(Intent.ACTION_SEND); ... // Always use string resources for UI text. // This says something like "Share this photo with" String title = getResources().getString(R.string.chooser_title); // Create intent to show chooser Intent chooser = Intent.createChooser(intent, title); // Verify the intent will resolve to at least one activity if (intent.resolveActivity(getPackageManager()) != null) { startActivity(chooser); }

这将显示1个对话框,其中有响应传递给 createChooser() 方法的意向的利用列表,并且将提供的文本用作 对话框标题。

取得Activity的结果


要接收结果,请调用 startActivityForResult()(而不是 startActivity())。并在原Acitivity的 onActivityResult() 回调中接收它。

启动Activity

启动针对结果的Activity时,所使用的 Intent 对象并没有甚么特别的地方,但需要向 startActivityForResult() 方法传递额外的整数参数。

该整数参数是辨认当前要求的“要求代码”。当、原Acitivity收到结果Intent 时,回调提供相同的要求代码,以便当前利用可以正确辨认结果并肯定如何处理它。

例如,此处显示如何开始允许用户选择联系人的Activity:

static final int PICK_CONTACT_REQUEST = 1; // The request code ... private void pickContact() { Intent pickContactIntent = new Intent(Intent.ACTION_PICK, Uri.parse("content://contacts")); pickContactIntent.setType(Phone.CONTENT_TYPE); // Show user only contacts w/ phone numbers startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST); }

接收结果

当用户完成后续Activity并且返回时,系统会调用本来Activity onActivityResult() 的方法。此方法包括3个参数:

  • 之前使用 startActivityForResult() 传递的要求代码。
  • 第2个是Activity指定的结果代码。如果操作成功,这是 RESULT_OK;如果用户退出或操作出于某种缘由失败,则是 RESULT_CANCELED。
  • 传送结果数据的 Intent。

本例说明您可以如何处理“选择联系人”意向的结果。

代码注释很简洁,就不翻译了,具体关于 如何通过URL获得到联系人信息的,需要温习内容提供者的知识

@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // Check which request it is that we're responding to if (requestCode == PICK_CONTACT_REQUEST) { // Make sure the request was successful if (resultCode == RESULT_OK) { // Get the URI that points to the selected contact Uri contactUri = data.getData(); // We only need the NUMBER column, because there will be only one row in the result String[] projection = {Phone.NUMBER}; // Perform the query on the contact to get the NUMBER column // We don't need a selection or sort order (there's only one result for the given URI) // CAUTION: The query() method should be called from a separate thread to avoid blocking // your app's UI thread. (For simplicity of the sample, this code doesn't do that.) // Consider using CursorLoader to perform the query. Cursor cursor = getContentResolver() .query(contactUri, projection, null, null, null); cursor.moveToFirst(); // Retrieve the phone number from the NUMBER column int column = cursor.getColumnIndex(Phone.NUMBER); String number = cursor.getString(column); // Do something with the phone number... } } }

允许其他利用启动我们的Activity


前两节重点讲述1方面:从当前利用开始另外一个利用的Activity。但如果当前利用可以履行对另外一个利用可能有用的操作,但钱利用应准备好响应来自其他利用的操作要求。 例如,如果我们构建1款可与用户的好友分享消息或照片的社交利用,那末我们最关注的是支持 ACTION_SEND 意向以便用户可以从另外一利用发起 “同享”操作并且启动您的利用履行该操作。

要允许其他利用启动我们的Activity,我们需要 在相应元素的宣示说明文件中添加1个 元素。

当利用安装在装备上时,系统会辨认您的意向过滤器并添加信息至所有已安装利用支持的意向内部目录。当利用通过隐含义向调用 startActivity() 或 startActivityForResult() 时,系统会找到可以响应当意向的Activity

添加意向过滤器


为了正肯定义Activity可处理的意向,添加的每一个意向过滤器在操作类型和Activity接受的数据方面应尽量具体。

如果Activity具有满足以下 Intent 对象条件的意向过滤器,系统可能向Activity发送给定的 Intent:

  • 操作
    对要履行的操作命名的字符串。通常是平台定义的值之1,比如 ACTION_SEND 或 ACTION_VIEW。
    使用 元素在乎向过滤器(Intent filter)中指定此值。在此元素中指定的值必须是操作的完全字符串名称,而不是 API 常数(可以参阅以下示例)。

  • 数据
    与意向关联的数据描写。
    用 元素在您的意向过滤器中指定此内容。使用此元素中的1个或多个属性,可以只指定 MIME 类型、URI 前缀、URI 架构或这些的组合和其他唆使所接受数据类型的项。

    注意:如果无需声明关于数据的具体信息 Uri(比如,当前Activity处理其他类型的“额外”数据而不是 URI 的时),我们应只指定 android:mimeType 属性声明您的Activity处理的数据类型,比如 text/plain 或 image/jpeg。

  • 种别
    提供另外1种表征处理意向的Activity的方法,通常与用户手势或Activity开始的位置有关。 系统支持多种不同的种别,但大多数都很少使用。 但是,所有隐含义向默许使用 CATEGORY_DEFAULT 进行定义。
    用 元素在乎向过滤器中指定此内容。

在乎向过滤器中,可以通过声明嵌套在 元素中的具有相应 XML 元素的各项,来声明当前Activity接受的条件。

例如,此处有1个在数据类型为文本或图象时处理 ACTION_SEND 意向的意向过滤器:

<activity android:name="ShareActivity"> <intent-filter> <action android:name="android.intent.action.SEND"/> <category android:name="android.intent.category.DEFAULT"/> <data android:mimeType="text/plain"/> <data android:mimeType="image/*"/> </intent-filter> </activity>

别的利用来启动了,接下来我们固然要响应操作咯!

处理Activity中的意向


当Activity开始时,调用 getIntent() 检索开始Activity的 Intent。可以在Activity生命周期的任什么时候间履行此操作,通常应在初期回调时(比如, onCreate() 或 onStart())履行。

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Get the intent that started this activity Intent intent = getIntent(); Uri data = intent.getData(); // Figure out what to do based on the intent type if (intent.getType().indexOf("image/") != -1) { // Handle intents with image data ... } else if (intent.getType().equals("text/plain")) { // Handle intents with text ... } }

从被启动的Acitivity中获得返回结果


想获得返回结果,只需调用 setResult() 指定结果代码和结果 Intent。当操作完成且用户应返回原始Activity时,调用 finish() 关闭(和烧毁)的Activity。 例如:

// Create intent to deliver some kind of result data Intent result = new Intent("com.example.RESULT_ACTION", Uri.parse("content://result_uri"); setResult(Activity.RESULT_OK, result); finish();

我们必须始终为结果指定结果代码。通常,它为 RESULT_OK 或 RESULT_CANCELED。在这以后可以根据需要为 Intent 提供额外的数据。
结果默许设置为 RESULT_CANCELED。因此,如果用户在完成操作动作或设置结果之前按了返回按钮,原始Activity会收到“已取消”的结果。

如果我们的需求是返回唆使若干结果选项之1的整数,那末可以将结果代码设置为大于 0 的任何值。 如果我们使用结果代码传递整数,并且无需包括 Intent,可以调用 setResult() 并且仅传递结果代码。 例如:

setResult(1); finish();

在这类情况下,只有几个可能的结果,因此结果代码是1个本地定义的整数(大于 0)。 当向自己利用中的Activity返回结果时,这将非常有效,由于接收结果的Activity可援用公共常数来肯定结果代码的值。

无需检查Activity是使用 startActivity() 还是 startActivityForResult() 开始的。如果开始您的Activity的意向可能需要结果,只需调用 setResult()。 如果原始Activity已调用 startActivityForResult(),则系统将向其传递您提供给 setResult() 的结果;否则,会疏忽结果。

项目实例


从主Activity跳转到相机或相册,选中1张图片,或拍摄1张图片返回,放在主Activity中展现

这里写图片描述

UML图:

这里写图片描述

MainActivity中有两个点击事件:photo()和camera(),分别代表:从相册获得图片,从相机获得图片

斟酌有的图片太占内存,所以引入compressed()是来优化内存,通过调用PhotoUtils里的静态方法完成图片紧缩

思路分析


  1. 从MainActivity 发送1个信息(去相册的信息或去相机的信息)

  2. 启动新的利用:相机app 或相册app

  3. 有两个分支:

    • 用户click,选择1张图片->
      MainActivity展现

    • 用户取消,没有选择图片->
      MainActivity不做任何改变

关键代码实现


  • Intent 作为信息的载体
  • startActivity() startActivityForResult()来完成启动操作
  • 在onActivityResult()完成 后续操作

代码分析


先介绍MainActivity.java的布局文件

很多朋友可能会喊f**k,第1个布局元素ConstraintLayout就不认识,莫急莫慌,详情请看我的另外一篇博客 2016谷歌新技术

<?xml version="1.0" encoding="utf⑻"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.learn.chaofun.interactingwithotherapp.MainActivity"> <Button android:id="@+id/photo" android:layout_width="150dp" android:layout_height="48dp" android:text="相册" android:onClick="photo" app:layout_constraintTop_toTopOf="@+id/activity_main" android:layout_marginTop="72dp" app:layout_constraintRight_toRightOf="@+id/activity_main" android:layout_marginRight="24dp" android:layout_marginEnd="24dp" /> <Button android:id="@+id/camer" android:layout_width="150dp" android:layout_height="48dp" android:text="camera" android:onClick="camera" app:layout_constraintLeft_toLeftOf="@+id/activity_main" android:layout_marginLeft="40dp" android:layout_marginStart="40dp" app:layout_constraintTop_toTopOf="@+id/activity_main" android:layout_marginTop="72dp" /> <ImageView android:id="@+id/main_show_pic" android:layout_width="300dp" android:layout_height="300dp" android:background="#ff313131" app:layout_constraintLeft_toLeftOf="@+id/activity_main" android:layout_marginLeft="40dp" android:layout_marginStart="40dp" app:layout_constraintTop_toTopOf="@+id/activity_main" android:layout_marginTop="160dp" app:layout_constraintRight_toRightOf="@+id/activity_main" android:layout_marginRight="16dp" android:layout_marginEnd="16dp" app:layout_constraintHorizontal_bias="0.38" /> </android.support.constraint.ConstraintLayout>

重点在于默许ImageView:大小为矩形,默许背景色彩为灰色,目的是为了看出图片裁剪的效果,到达内存优化的目的

下面我们来看MainActivity中是如何书写的吧:

public class MainActivity extends AppCompatActivity { /* 用来标识要求照相功能的activity */ private static final int CAMERA_WITH_DATA = 3023; /* 用来标识要求相册的activity */ private static final int PHOTO_PICKED_WITH_DATA = 3021; /* 照相机拍照得到的图片 */ private File mCurrentPhotoFile; private String photoPath = null, tempPhotoPath, camera_path; //首先使用butterkniff 拿到view对象 @InjectView(R.id.photo) Button photo; @InjectView(R.id.camer) Button camer; @InjectView(R.id.main_show_pic) ImageView mImageView; @InjectView(R.id.activity_main) ConstraintLayout activityMain; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.inject(this); } //定时器来完成任务 Timer timer = new Timer(); TimerTask task = new TimerTask() { public void run() { Message message = new Message(); message.what = 1; myHandler.sendMessage(message); } }; //重点演示Activity之间跳转,接收返回的数据 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.i("result", "result"); //判断用户是不是操作了,没有选择的话,图片为空,为了避免空指针异常,这里判断结果码来避免异常 if (resultCode == RESULT_OK) { switch (requestCode) { case CAMERA_WITH_DATA: photoPath = tempPhotoPath; if (mImageView.getWidth() == 0) { timer.schedule(task, 10, 1000); } else { compressed(); } break; case PHOTO_PICKED_WITH_DATA: Uri originalUri = data.getData(); String[] filePathColumn = {MediaStore.MediaColumns.DATA}; Cursor cursor = MainActivity.this.getContentResolver().query( originalUri, filePathColumn, null, null, null); cursor.moveToFirst(); int columnIndex = cursor.getColumnIndex(filePathColumn[0]); photoPath = cursor.getString(columnIndex); // 延迟每次延迟10 毫秒 隔1秒履行1次 if (mImageView.getWidth() == 0) { timer.schedule(task, 10, 1000); } else { compressed(); } break; default: break; } } } /* 从相机中获得照片 */ public void camera(View view) throws IOException { Intent intent = new Intent("android.media.action.IMAGE_CAPTURE"); tempPhotoPath = PhotoUtils.DCIMCamera_PATH + getNewFileName() + ".jpg"; mCurrentPhotoFile = new File(tempPhotoPath); if (!mCurrentPhotoFile.exists()) { try { mCurrentPhotoFile.createNewFile(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mCurrentPhotoFile)); startActivityForResult(intent, CAMERA_WITH_DATA); } /* 从相册中获得照片 */ public void photo(View view) { Intent openphotoIntent = new Intent(Intent.ACTION_PICK); openphotoIntent.setType("image/*"); startActivityForResult(openphotoIntent, PHOTO_PICKED_WITH_DATA); } public static String getNewFileName() { SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss"); Date curDate = new Date(System.currentTimeMillis()); return formatter.format(curDate); } //重点是调用了PhotoUtils的接口 private void compressed() { Bitmap resizeBmp = PhotoUtils.decodeBitmapFromPath(photoPath, 540, 540); mImageView.setImageBitmap(resizeBmp); camera_path = PhotoUtils.SaveBitmap(resizeBmp, "saveTemp"); } //handler 负责刷新UI final Handler myHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 1) { if (mImageView.getWidth() != 0) { // 取消定时器 timer.cancel(); compressed(); } } } }; }

MainActivity.java有很多细节值得温习

  1. Activity中的点击事件 是在XML中以属性的方式配置的 比如 相机Button的 android:onClick=”camera”,以后在Activity中以(自己加1个camera函数)同名函数被系统回调的方式,避免setOnclickListener的繁琐,或onClick中switch判断每个View的id致使性能太低的情况。

  2. 引入定时器的技术,这里是我刻意加上去的,为的的是介绍Timer和TimerTask的概念,它们用处很广泛,比如闪屏页定时展现几秒后跳转到主页,或定时完成1些任务,跟Handler的sendMessageDelay,postDelay类似,还可以实现AdapterViewFliper的定时更换页面类似效果,不过定时器的方式更加简洁。

  3. Sending the User to Another App的做法:

    比如

    /* 从相册中获得照片 */ public void photo(View view) { Intent openphotoIntent = new Intent(Intent.ACTION_PICK); openphotoIntent.setType("image/*"); startActivityForResult(openphotoIntent, PHOTO_PICKED_WITH_DATA); }

    不言而喻:

    1. 用Intent封装数据信息
    2. startActivityForResult()来履行跳转,固然还有另外一种启动方式,startActivity()
  4. onActivityResult中的处理,用RESULT_OK 来判断用户是进行了“在相机利用中或图库利用当选中图片,系统默许返回跳转原MainActivity”操作或是进行了“用户点击Back按钮返回原MainActivity”操作

  5. 紧缩图片的尺寸,调用了PhotoUtils的接口

下面是工具类PhotoUtils:

public class PhotoUtils { public static String SDCARD_PAHT = Environment .getExternalStorageDirectory().getPath(); public static String DCIMCamera_PATH = Environment .getExternalStorageDirectory() + "/DCIM/Camera/"; // 将生成的图片保存到内存中 public static String SaveBitmap(Bitmap bitmap, String name) { if (Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)) { File dir = new File(SDCARD_PAHT); if (!dir.exists()) dir.mkdir(); File file = new File(SDCARD_PAHT + "/" + name + ".jpg"); FileOutputStream out; try { out = new FileOutputStream(file); if (bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)) { out.flush(); out.close(); } return file.getAbsolutePath(); } catch (IOException e) { e.printStackTrace(); } } return null; } public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) { // 取得内存中图片的宽高 final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; // 计算出1个数值,必须符合为2的幂(1,2,4,8,tec),赋值给inSampleSize // 图片宽高应大于期望的宽高的时候,才进行计算 while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } } return inSampleSize; } public static Bitmap decodeBitmapFromPath(String path , int reqWidth, int reqHeight) { // 第1次解析 inJustDecodeBounds=true 只是用来获得bitmap在内存中的尺寸和类型,系统其实不会为其分配内存, final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(path, options); // 计算出1个数值 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 根据inSampleSize 数值来解析bitmap options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(path, options); } }

关于图片紧缩,可以通过我之前两篇博文Google 优化内存史诗巨著 和 图片紧缩看我的博客就够了! 来温习为何要紧缩图片

参考感谢

  1. Google 关于 Intent 的官方文档
  2. Google 关于 内容提供者的课程
  3. Google 关于 捕获照片的课程

自制demo

Interacting with Other Apps by chaofan

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐