Android 调用系统功能实现图片选择器,你可能会遇到的问题汇总

eason0624 8年前
   <p>图片选择器在手机应用中屡见不鲜,设置头像、聊天传图等常见类似场景都需要使用。为了保持不同设备上体验的一致性和较好的兼容性,比较稳妥的做法是在应用内自实现相机拍照、相册选图和图片裁剪功能。但是,这个实现过程比较复杂,费时费力。更多时候,或者说在项目初期,我们都会选择直接调用系统提供的这些功能来完成一个图片选择器。然而,由于安卓设备的多样性,总会遇到各种各样的兼容问题。本文就来总结总结,调用系统相机、相册和裁剪功能实现图片选择器的过程中,我们需要注意的一些地方。</p>    <h2>示例代码</h2>    <p>这里简单使用一个示例代码,演示调用系统相机或相册,获取图片,然后使用系统裁剪功能处理图片,并显示到一个 ImageButton 视图里面:</p>    <pre>  <code class="language-java">public class MainActivity extends FragmentActivity {        public static final int REQUEST_CAMERA = 1;      public static final int REQUEST_ALBUM = 2;      public static final int REQUEST_CROP = 3;        public static final String IMAGE_UNSPECIFIED = "image/*";        private ImageButton mPictureIb;        private File mImageFile;        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);            mPictureIb = (ImageButton) findViewById(R.id.ib_picture);      }        public void onClickPicker(View v) {          new AlertDialog.Builder(this)                  .setTitle("选择照片")                  .setItems(new String[]{"拍照", "相册"}, new OnClickListener() {                      @Override                      public void onClick(DialogInterface dialogInterface, int i) {                          if (i == 0) {                              selectCamera();                          } else {                              selectAlbum();                          }                      }                  })                  .create()                  .show();      }            private void selectCamera() {          createImageFile();          if (!mImageFile.exists()) {              return;          }            Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);          cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));          startActivityForResult(cameraIntent, REQUEST_CAMERA);      }        private void selectAlbum() {          Intent albumIntent = new Intent(Intent.ACTION_PICK);          albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_UNSPECIFIED);          startActivityForResult(albumIntent, REQUEST_ALBUM);      }        private void cropImage(Uri uri){          Intent intent = new Intent("com.android.camera.action.CROP");          intent.setDataAndType(uri, IMAGE_UNSPECIFIED);          intent.putExtra("crop", "true");          intent.putExtra("aspectX", 1);          intent.putExtra("aspectY", 1);          intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));          startActivityForResult(intent, REQUEST_CROP);      }        private void createImageFile() {          mImageFile = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".jpg");          try {              mImageFile.createNewFile();          } catch (IOException e) {              e.printStackTrace();              Toast.makeText(this, "出错啦", Toast.LENGTH_SHORT).show();          }      }        @Override      protected void onActivityResult(int requestCode, int resultCode, Intent data) {          super.onActivityResult(requestCode, resultCode, data);          if (RESULT_OK != resultCode) {              return;          }          switch (requestCode) {              case REQUEST_CAMERA:                  cropImage(Uri.fromFile(mImageFile));                  break;                case REQUEST_ALBUM:                  createImageFile();                  if (!mImageFile.exists()) {                      return;                  }                    Uri uri = data.getData();                  if (uri != null) {                      cropImage(uri);                  }                  break;                case REQUEST_CROP:                  mPictureIb.setImageURI(Uri.fromFile(mImageFile));                  break;          }      }    }  </code></pre>    <p>效果如图(不同设备,系统功能呈现有所不同):</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/f75c52de8c84866b85022dde6ba1df18.gif"></p>    <p>看似完美,你以为上述代码就能结束了的话,那就大错特错啦!这里面还有一些兼容问题要处理,还有一些地方需要特殊说明。</p>    <h2>拍照图片存储问题</h2>    <p>调用系统相机实现拍照功能的核心代码如下:</p>    <pre>  <code class="language-java">Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);  cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));  startActivityForResult(cameraIntent, REQUEST_CAMERA);  </code></pre>    <p>其中 MediaStore.EXTRA_OUTPUT 数据表示,拍照所得图片保存到指定目录下的文件(一般会在 SD 卡中创建当前应用的目录,并创建临时文件保存图片)。然后,在 onActivityResult 方法中根据文件路径获取图片。</p>    <p>如果不为 intent 添加该数据的话,将在 onActivityResult 的 intent 对象中返回一个 Bitmap 对象,通过如下代码获取:</p>    <pre>  <code class="language-java">Bitmap bmp = data.getParcelableExtra("data");  </code></pre>    <p>值得注意的是,这里的 Bitmap 对象是拍照所得图片的一个 <strong>缩略图</strong> ,尺寸很小!系统这么做也是充分考虑到应用的内存占用问题。试想一下,如今手机设备中高清相机拍出来的照片,一张图的大小高达十几兆,如果返回这么大的图片,内存占用相当严重,何况很多时候知识临时使用而已。所以,调用系统相机时,一般都会添加 MediaStore.EXTRA_OUTPUT 参数,避免返回 Bitmap 对象。当然,这么做也能保证应用产生的数据,包括文件,都能存储在应用目录下,方便清理缓存时统一清除。</p>    <h2>拍照图片旋转问题</h2>    <p>部分手机,比如三星手机,调用系统相机拍照所得的照片可能会发生自动旋转问题,常见为旋转 90°。所以,要求我们在拍照之后,使用图片之前,判断图片是否发生过旋转,如果是,要将照片旋转回来。</p>    <p>这是获取图片旋转角度的代码:</p>    <pre>  <code class="language-java">/**   * 获取图片旋转角度   * @param path 图片路径   * @return   */  private int parseImageDegree(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;          }      } catch (IOException e) {          e.printStackTrace();      }      return degree;  }  </code></pre>    <p>这是根据指定角度旋转图片的代码:</p>    <pre>  <code class="language-java">/**   * 图片旋转操作   *   * @param bm 需要旋转的图片   * @param degree 旋转角度   * @return 旋转后的图片   */  private Bitmap rotateBitmap(Bitmap bm, int degree) {      Bitmap returnBm = null;        Matrix matrix = new Matrix();      matrix.postRotate(degree);      try {          returnBm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);      } catch (OutOfMemoryError e) {      }      if (returnBm == null) {          returnBm = bm;      }      if (bm != returnBm) {          bm.recycle();      }      return returnBm;  }  </code></pre>    <h2>横竖屏切换问题</h2>    <p>在部分手机,调用系统拍照功能时,可能会发生横竖屏切换过程,导致返回应用时当前 Activity 发生销毁重建,各个生命周期又重新走了一遍。此时,一些应用内的变量数据可能丢失,使用时容易发生空值异常,进而导致 app 崩溃退出。</p>    <p>为了避免这种现象,我们需要在 AndroidManifest.xml 文件的对应 <activity> 标签中添加属性:</p>    <pre>  <code class="language-java">android:configChanges="orientation|screenSize"  </code></pre>    <p>这样,当发生屏幕旋转时,不会导致 Activity 销毁重建,而是执行 onConfigurationChanged() 方法:</p>    <pre>  <code class="language-java">@Override  public void onConfigurationChanged(Configuration newConfig) {      super.onConfigurationChanged(newConfig);  }  </code></pre>    <h2>调用系统裁剪问题</h2>    <p>示例中调用系统裁剪的代码如下:</p>    <pre>  <code class="language-java">Intent intent = new Intent("com.android.camera.action.CROP");  intent.setDataAndType(uri, IMAGE_UNSPECIFIED);  intent.putExtra("crop", "true");  intent.putExtra("aspectX", 1);  intent.putExtra("aspectY", 1);  intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mImageFile));  startActivityForResult(intent, REQUEST_CROP);  </code></pre>    <p>可以看出,调用系统裁剪功能,需要设置一些 Extra 参数,很多人容易在这里产生疑惑,不知如何取舍,如何设值。这里列举一下常用的 Extra 名字、值类型和作用:</p>    <ul>     <li><strong>crop</strong> :String 类型数据,发送裁剪信号</li>     <li><strong>aspectX</strong> 和 <strong>aspectY</strong> :int 类型数据,设置裁剪框的 X 与 Y 值比例</li>     <li><strong>outputX</strong> 和 <strong>outputY</strong> :int 类型数据,设置裁剪输出的图片大小</li>     <li><strong>scale</strong> :boolean 类型数据,设置是否支持裁剪缩放</li>     <li><strong>return-data</strong> :boolean 类型数据,设置是否在 onActivityResult 方法的 intent 值中返回 Bitmap 对象</li>     <li><strong>MediaStore.EXTRA_OUTPUT</strong> :Uri 类型数据,设置是否将裁剪结果保存到指定文件中</li>    </ul>    <p>需要注意的是:</p>    <p>第一,设置 return-data 参数为 true 时,返回的 Bitmap 对象也为缩略图,获取方式与前面所述相机拍照获取 Bitmap 的方式一致;</p>    <p>第二,调用系统相册并裁剪时,如果使用 <strong>MediaStore.EXTRA_OUTPUT</strong> 参数,Uri 尽量不要设置为源文件对应的 Uri 值,另做保存,不损坏系统相册中的源图文件;</p>    <p>第三,根据经验,outputX 与 outputY 值设置太大时,容易出现卡屏现象;</p>    <p>第四,可以不设置 outputX 与 outputY 参数,使用户根据自身按比例自由裁剪,就像示例代码这样。</p>    <h2>setImageURI() 注意事项</h2>    <p>你可能会用到 setImageURI() 方法给 ImageView 设置图片内容,这里也有一个地方需要注意。我们先看一下这个方法的源码:</p>    <pre>  <code class="language-java">public void setImageURI(Uri uri) {      if (mResource != 0 ||              (mUri != uri &&               (uri == null || mUri == null || !uri.equals(mUri)))) {          updateDrawable(null);          mResource = 0;          mUri = uri;            final int oldWidth = mDrawableWidth;          final int oldHeight = mDrawableHeight;            resolveUri();            if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {              requestLayout();          }          invalidate();      }  }  </code></pre>    <p>可以看到,这里的 uri 参数在内部持有缓存变量,当多次调用该方法而 uri 参数值不变时,图片展示内容不变。问题就在这,如果你多次拍照或裁剪保存的图片文件路径相同时,虽然每次处理过后实际存储的文件内容发生变化,但由于路径相同,uri 参数一致,导致多次调用 setImageURI() 设置图片内容时,ImageView 显示内容不变!这也是为什么示例代码中我用时间戳处理图片文件名的原因所在,保证每次存储的图片路径不同。</p>    <h2>根据 Uri 获取文件地址</h2>    <p>有时候,我们需要根据 Uri 获取文件路径。比如如果你不需要使用裁剪功能的话,调用系统相册选择图片后返回的就是一个 Uri 对象,我们需要从这个 Uri 对象中解析出对应的图片文件路径,便于上传至服务器等后续处理。</p>    <p>比如,这个 Uri 对象可能是:</p>    <p>content://media/external/images/media/3066</p>    <p>很多朋友相信有过这样的经验,使用 toString() 或者 getPath() 方法获取 Uri 对象所对应的文件路径,其实这是错误的!通过 getPath() 获取的结果字符串是:</p>    <p>media/external/images/media/3066</p>    <p>而正确的获取方式是:</p>    <pre>  <code class="language-java">private String parseFilePath(Uri uri) {      String[] filePathColumn = { MediaStore.Images.Media.DATA };      Cursor cursor = getContentResolver().query(uri, filePathColumn, null, null, null);      cursor.moveToFirst();      int columnIndex = cursor.getColumnIndex(filePathColumn[0]);      String picturePath = cursor.getString(columnIndex);      cursor.close();      return picturePath;  }  </code></pre>    <p>其对应的文件路径应该是这个样子的:</p>    <p>/storage/emulated/0/Pictures/Screenshots/S70302-131606.jpg</p>    <h2>Base64 文件编码处理</h2>    <p>现在很多网络框架内部都做了封装处理,上传图片时只需要传递一个文件路径即可。但是,少数情况下,根据服务器需要,我们要对图片文件字节流编码后再上传。这是使用 Base64 编码并根据字节数组获取字符串的处理过程:</p>    <pre>  <code class="language-java">public static String fileToBase64String(String filePath) {      File photoFile = new File(filePath);      try {          FileInputStream fis = new FileInputStream(photoFile);          ByteArrayOutputStream baos = new ByteArrayOutputStream(10000);          byte[] buffer = new byte[1000];          while (fis.read(buffer)!=-1) {              baos.write(buffer);          }          baos.close();          fis.close();          return Arrays.toString(Base64.encode(baos.toByteArray(), Base64.DEFAULT));      }catch (IOException e) {          e.printStackTrace();      }      return null;  }  </code></pre>    <h2>zip 压缩文件处理</h2>    <p>当上传多张图片至服务器时,为了提升传输效率,往往会采用 zip 格式压缩处理。这里提供一个递归压缩代码,方便大家有需要的时候借鉴参考:</p>    <pre>  <code class="language-java">public String zipCompass(String filePath){      File zipFile = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".zip");      try{          //指定了两个待压缩的文件,都在assets目录中            String[] filenames = new String[]{ "activity_main.xml", "strings.xml" };          FileOutputStream fos = new FileOutputStream(zipFile);          ZipOutputStream zos = new ZipOutputStream(fos);          int i = 1;          //枚举filenames中的所有待压缩文件            while (i <= filenames.length){              //从filenames数组中取出当前待压缩的文件名,作为压缩后的名称,以保证压缩前后文件名一致                ZipEntry zipEntry = new ZipEntry(filenames[i - 1]);              //打开当前的zipEntry对象                zos.putNextEntry(zipEntry);                FileInputStream is = new FileInputStream(filePath);              byte[] buffer = new byte[8192];              int count = 0;              //写入数据                while ((count = is.read(buffer)) >= 0){                  zos.write(buffer, 0, count);              }              zos.flush();              zos.closeEntry();              is.close();              i++;            }          zos.finish();          zos.close();          return zipFile.getAbsolutePath();      }      catch (Exception e){          Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();          return null;      }  }  </code></pre>    <h2>添加系统权限</h2>    <p>说了这么多,别忘了在 AndroidManifest.xml 文件中添加系统权限(前面示例代码中没有考虑到 Android 6.0 运行时权限的问题,实际使用时注意添加处理):</p>    <pre>  <code class="language-java"><uses-permission android:name="android.permission.CAMERA" />  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />  </code></pre>    <p> </p>    <p>来自:http://yifeng.studio/2017/03/19/android-image-picker-by-system-app/</p>    <p> </p>