安卓 - 将图片保存到相册

111
我有一个包含图像库的应用程序,并希望用户可以将其保存到自己的图库中。 我已经创建了一个选项菜单,其中只有一个选项“保存”,以允许这样做,但是问题是...如何将图像保存到图库中?
这是我的代码:
@Override
        public boolean onOptionsItemSelected(MenuItem item) {
            // Handle item selection
            switch (item.getItemId()) {
            case R.id.menuFinale:

                imgView.setDrawingCacheEnabled(true);
                Bitmap bitmap = imgView.getDrawingCache();
                File root = Environment.getExternalStorageDirectory();
                File file = new File(root.getAbsolutePath()+"/DCIM/Camera/img.jpg");
                try 
                {
                    file.createNewFile();
                    FileOutputStream ostream = new FileOutputStream(file);
                    bitmap.compress(CompressFormat.JPEG, 100, ostream);
                    ostream.close();
                } 
                catch (Exception e) 
                {
                    e.printStackTrace();
                }



                return true;
            default:
                return super.onOptionsItemSelected(item);
            }
        }

我不确定这段代码的部分:

File root = Environment.getExternalStorageDirectory();
                File file = new File(root.getAbsolutePath()+"/DCIM/Camera/img.jpg");

将其保存到图库中是否正确? 不幸的是,代码不能正常工作 :(


你解决了这个问题吗?能否请你与我分享一下? - user3233280
我也遇到了同样的问题 http://stackoverflow.com/questions/21951558/failed-to-save-image-from-app-assets-folder-to-gallery-folder-in-android/21951643?noredirect=1#21951643 - user3233280
对于那些仍然在保存文件时遇到问题的人,可能是因为您的URL包含非法字符,例如“?”,“:”和“ - ”。删除它们,应该就可以正常工作了。这是外国设备和Android模拟器中常见的错误。在此处了解更多信息:https://dev59.com/7Ggu5IYBdhLWcg3wWVuF - ChallengeAccepted
被接受的答案在2019年已经有点过时了。我在这里写了一个更新的答案:https://dev59.com/jVoV5IYBdhLWcg3wJ8Ab#57265702 - Bao Lei
13个回答

188
MediaStore.Images.Media.insertImage(getContentResolver(), yourBitmap, yourTitle , yourDescription);

前面的代码会将图像添加到画廊的末尾。如果您想修改日期以便它出现在开头或任何其他元数据,请参阅下面的代码(由S-K,samkirton提供):

https://gist.github.com/samkirton/0242ba81d7ca00b475b9

/**
 * Android internals have been modified to store images in the media folder with 
 * the correct date meta data
 * @author samuelkirton
 */
public class CapturePhotoUtils {

    /**
     * A copy of the Android internals  insertImage method, this method populates the 
     * meta data with DATE_ADDED and DATE_TAKEN. This fixes a common problem where media 
     * that is inserted manually gets saved at the end of the gallery (because date is not populated).
     * @see android.provider.MediaStore.Images.Media#insertImage(ContentResolver, Bitmap, String, String)
     */
    public static final String insertImage(ContentResolver cr, 
            Bitmap source, 
            String title, 
            String description) {

        ContentValues values = new ContentValues();
        values.put(Images.Media.TITLE, title);
        values.put(Images.Media.DISPLAY_NAME, title);
        values.put(Images.Media.DESCRIPTION, description);
        values.put(Images.Media.MIME_TYPE, "image/jpeg");
        // Add the date meta data to ensure the image is added at the front of the gallery
        values.put(Images.Media.DATE_ADDED, System.currentTimeMillis());
        values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis());

        Uri url = null;
        String stringUrl = null;    /* value to be returned */

        try {
            url = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

            if (source != null) {
                OutputStream imageOut = cr.openOutputStream(url);
                try {
                    source.compress(Bitmap.CompressFormat.JPEG, 50, imageOut);
                } finally {
                    imageOut.close();
                }

                long id = ContentUris.parseId(url);
                // Wait until MINI_KIND thumbnail is generated.
                Bitmap miniThumb = Images.Thumbnails.getThumbnail(cr, id, Images.Thumbnails.MINI_KIND, null);
                // This is for backward compatibility.
                storeThumbnail(cr, miniThumb, id, 50F, 50F,Images.Thumbnails.MICRO_KIND);
            } else {
                cr.delete(url, null, null);
                url = null;
            }
        } catch (Exception e) {
            if (url != null) {
                cr.delete(url, null, null);
                url = null;
            }
        }

        if (url != null) {
            stringUrl = url.toString();
        }

        return stringUrl;
    }

    /**
     * A copy of the Android internals StoreThumbnail method, it used with the insertImage to
     * populate the android.provider.MediaStore.Images.Media#insertImage with all the correct
     * meta data. The StoreThumbnail method is private so it must be duplicated here.
     * @see android.provider.MediaStore.Images.Media (StoreThumbnail private method)
     */
    private static final Bitmap storeThumbnail(
            ContentResolver cr,
            Bitmap source,
            long id,
            float width, 
            float height,
            int kind) {

        // create the matrix to scale it
        Matrix matrix = new Matrix();

        float scaleX = width / source.getWidth();
        float scaleY = height / source.getHeight();

        matrix.setScale(scaleX, scaleY);

        Bitmap thumb = Bitmap.createBitmap(source, 0, 0,
            source.getWidth(),
            source.getHeight(), matrix,
            true
        );

        ContentValues values = new ContentValues(4);
        values.put(Images.Thumbnails.KIND,kind);
        values.put(Images.Thumbnails.IMAGE_ID,(int)id);
        values.put(Images.Thumbnails.HEIGHT,thumb.getHeight());
        values.put(Images.Thumbnails.WIDTH,thumb.getWidth());

        Uri url = cr.insert(Images.Thumbnails.EXTERNAL_CONTENT_URI, values);

        try {
            OutputStream thumbOut = cr.openOutputStream(url);
            thumb.compress(Bitmap.CompressFormat.JPEG, 100, thumbOut);
            thumbOut.close();
            return thumb;
        } catch (FileNotFoundException ex) {
            return null;
        } catch (IOException ex) {
            return null;
        }
    }
}

26
保存了这张照片,但是它被保存到了相册的末尾,而当你用相机拍摄照片时,它会被保存在最前面。我该如何把照片保存在相册的最上方? - eric.itzhak
21
请注意,您还需要将 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> 添加到您的 manifest.xml 文件中。 - Kyle Clegg
3
顶部未保存图库中的图像是因为内部的insertImage并不会添加任何日期元数据。请参见此GIST:https://gist.github.com/0242ba81d7ca00b475b9.git,它是insertImage方法的精确副本,但它添加了日期元数据以确保图像添加到图库的前面。 - S-K'
6
这是上面提到的正确的GIST链接(需要删除末尾的“.git”)。 - minipif
5
"MediaStore.Images.Media.insertImage(...)"已经被弃用。 - Michael Abyzov
显示剩余7条评论

52

实际上,您可以将图片保存在任何地方。如果您想保存在公共空间中,以便其他应用程序可以访问,请使用以下代码:

storageDir = new File(
    Environment.getExternalStoragePublicDirectory(
        Environment.DIRECTORY_PICTURES
    ), 
    getAlbumName()
);

图片没有添加到相册中。要实现这一点,需要调用一个扫描函数:

private void galleryAddPic() {
    Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
    File f = new File(mCurrentPhotoPath);
    Uri contentUri = Uri.fromFile(f);
    mediaScanIntent.setData(contentUri);
    this.sendBroadcast(mediaScanIntent);
}

您可以在https://developer.android.com/training/camera/photobasics.html#TaskGallery找到更多信息。


1
这是一个很好的简单解决方案,因为我们不需要改变整个实现,而且我们可以为应用程序创建一个自定义文件夹。 - Hugo Gresse
2
发送广播可能是一种资源浪费,当你只需要扫描一个文件时:https://dev59.com/U2445IYBdhLWcg3w5OIH#5814533。 - Jérémy Reynaud
3
你实际上在哪里传递位图? - Daniel Reyhanian
6
现在 Environment.getExternalStoragePublicDirectoryIntent.ACTION_MEDIA_SCANNER_SCAN_FILE 已经过时了。 - Michael Abyzov

26

我尝试了很多方法让这在Marshmallow 和 Lollipop 上工作。 最后,我把保存的图片移到了DCIM文件夹中(新版Google照片应用似乎只会扫描此文件夹中的图片)。

public static File createImageFile() throws IOException {
    // Create an image file name
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss")
         .format(System.currentTimeInMillis());
    File storageDir = new File(Environment
         .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/Camera/");
    if (!storageDir.exists())
        storageDir.mkdirs();
    File image = File.createTempFile(
            timeStamp,                   /* prefix */
            ".jpeg",                     /* suffix */
            storageDir                   /* directory */
    );
    return image;
}

然后是用于扫描文件的标准代码,您可以在Google Developers 网站中找到。

public static void addPicToGallery(Context context, String photoPath) {
    Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
    File f = new File(photoPath);
    Uri contentUri = Uri.fromFile(f);
    mediaScanIntent.setData(contentUri);
    context.sendBroadcast(mediaScanIntent);
}
请记住,这个文件夹可能不会出现在全世界的每个设备上,并且从Marshmallow(API 23)开始,您需要向用户请求WRITE_EXTERNAL_STORAGE权限。
请注意,以下是翻译内容:

请记住,这个文件夹可能不会出现在全世界的每个设备上,并且从Marshmallow(API 23)开始,您需要向用户请求WRITE_EXTERNAL_STORAGE权限。


1
感谢提供有关Google照片的信息。 - Jérémy Reynaud
3
这是唯一解释得很好的解决方案。 没有其他人提到文件必须在DCIM文件夹中。 谢谢! - Predrag Manojlovic
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) 对我很有帮助。谢谢! - saltandpepper
3
getExternalStoragePublicDirectory() 在 API 29 上已经被弃用,需要使用 MediaStore。 - riggaroo
截至2021年,这是唯一可行的答案! - Hank Chan
显示剩余2条评论

15

根据这个教程,正确的做法是:

Environment.getExternalStoragePublicDirectory(
        Environment.DIRECTORY_PICTURES
    )

这将为您提供图库目录的根路径。


我尝试了这段新代码,但它崩溃了。java.lang.NoSuchFieldError: android.os.Environment.DIRECTORY_PICTURES - Christian Giupponi
好的,那么在 Android < 2.2 上没有办法将图片放在画廊上了吗? - Christian Giupponi
完美 - 直接链接到Android开发者网站。这个方法行之有效,而且非常简单。 - Phil
1
不错的答案,但是如果加上其他答案中的“galleryAddPic”方法会更好,因为通常希望画廊应用程序能够注意到新图片。 - Andrew Koster
3
"Environment.getExternalStoragePublicDirectory" 已经过时了... - Michael Abyzov

12
private void galleryAddPic() {
    Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
    File f = new File(mCurrentPhotoPath);
    Uri contentUri = Uri.fromFile(f);
    mediaScanIntent.setData(contentUri);
    this.sendBroadcast(mediaScanIntent);
}

10

你可以在相机文件夹内创建一个目录并保存照片。之后,你可以直接进行扫描,它会立即显示在相册中。

String root = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString()+ "/Camera/Your_Directory_Name";
File myDir = new File(root);
myDir.mkdirs();
String fname = "Image-" + image_name + ".png";
File file = new File(myDir, fname);
System.out.println(file.getAbsolutePath());
if (file.exists()) file.delete();
    Log.i("LOAD", root + fname);
    try {
        FileOutputStream out = new FileOutputStream(file);
        finalBitmap.compress(Bitmap.CompressFormat.PNG, 90, out);
        out.flush();
        out.close();
    } catch (Exception e) {
       e.printStackTrace();
    }

MediaScannerConnection.scanFile(context, new String[]{file.getPath()}, new String[]{"image/jpeg"}, null);

1
在这个标准下,这就是最佳答案。 - Noor Hossain
你的代码中 finalBitmap 在哪里初始化了?在示例中缺失。 - Michael Paccione
@MichealPaccione 显然,您需要将图像保存为位图。如果您在创建所需图像的位图对象时遇到问题,则是另一种情况。请先尝试解决它。 - javatar
当targetSdkVersion为32时,这将无法工作。 - Ashish Garg

4
注意:对于 Build.VERSION.SDK_INT < 29,必须先将图像保存到本地磁盘上,这会随着用户保存更多图像而增加应用程序的大小。用户可以稍后在“文件”应用程序中删除该图像,但本地图像必须与 Google 照片或亚马逊照片同步到云端。
将图像保存到云端是通过在导出并在删除您的应用程序 APK 之前让用户打开他们的 Google 照片或亚马逊照片应用程序来完成的。如果 SDK_INT < 29 的用户在打开 Google 照片或亚马逊照片之前删除了您的 APK,则该照片将丢失。
这是 Android Q(Level 29)之前版本的错误。Level 29 及更高版本直接保存到照片库中。
Android Manifest XML
<!-- Adding Read External Storage Permission -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

保存功能

// - Save Image -

@Throws(FileNotFoundException::class)
private fun saveImage(
    bitmap: Bitmap,
    context: Context,
    folderName: String
) {

    if (Build.VERSION.SDK_INT >= 29) {

        val values = ContentValues()
        values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/$folderName")
        values.put(MediaStore.Images.Media.IS_PENDING, true)

        // RELATIVE_PATH and IS_PENDING are introduced in API 29.

        val uri: Uri? = context.contentResolver
            .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)

        if (uri != null) {
            saveImageToStream(bitmap, context.contentResolver.openOutputStream(uri))
            values.put(MediaStore.Images.Media.IS_PENDING, false)
            context.contentResolver.update(uri, values, null, null)
        }

    } else {

        var dir = File(
            applicationContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES),
            ""
        )

        // getExternalStorageDirectory is deprecated in API 29

        if (!dir.exists()) {

            dir.mkdirs()

        }

        val date = Date()

        val fullFileName = "myFileName.jpeg"

        val fileName = fullFileName?.substring(0, fullFileName.lastIndexOf("."))
        val extension = fullFileName?.substring(fullFileName.lastIndexOf("."))

        var imageFile = File(
            dir.absolutePath
                .toString() + File.separator
                    + fileName + "_" + Timestamp(date.time).toString()
                    + ".jpg"
        )

        println("imageFile: $imageFile")

        saveImageToStream(bitmap, FileOutputStream(imageFile))

        if (imageFile.getAbsolutePath() != null) {

            val values = ContentValues()

            values.put(MediaStore.Images.Media.DATA, imageFile.absolutePath)

            // .DATA is deprecated in API 29

            context.contentResolver
                .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)

        }

    }

}

private fun contentValues(): ContentValues? {

    val values = ContentValues()

    values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
    values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000)
    values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())

    return values

}

private fun saveImageToStream(bitmap: Bitmap, outputStream: OutputStream?) {

    println("saveImageToStream")

    if (outputStream != null) {

        try {

            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
            outputStream.close()

            // success dialog

            runOnUiThread {

                val successDialog = SuccessDialog.getInstance(null)
                successDialog.show(supportFragmentManager, SuccessDialog.TAG)

            }

        } catch (e: Exception) {

            e.printStackTrace()

            // warning dialog

            runOnUiThread {

                val warningDialog = WarningDialog.getInstance(null)
                warningDialog.show(supportFragmentManager, WarningDialog.TAG)

            }

        }

    }

}

2

这是我成功的方法:

 private fun saveBitmapAsImageToDevice(bitmap: Bitmap?) {
    // Add a specific media item.
    val resolver = this.contentResolver

    val imageStorageAddress = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    } else {
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    }

    val imageDetails = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, "my_app_${System.currentTimeMillis()}.jpg")
        put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
        put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis())
    }

    try {
        // Save the image.
        val contentUri: Uri? = resolver.insert(imageStorageAddress, imageDetails)
        contentUri?.let { uri ->
            // Don't leave an orphan entry in the MediaStore
            if (bitmap == null) resolver.delete(contentUri, null, null)
            val outputStream: OutputStream? = resolver.openOutputStream(uri)
            outputStream?.let { outStream ->
                val isBitmapCompressed =
                    bitmap?.compress(Bitmap.CompressFormat.JPEG, 95, outStream)
                if (isBitmapCompressed == true) {
                    outStream.flush()
                    outStream.close()
                }
            } ?: throw IOException("Failed to get output stream.")
        } ?: throw IOException("Failed to create new MediaStore record.")
    } catch (e: IOException) {
        throw e
    }
}

1

在我的情况下,上述解决方案没有起作用,我不得不执行以下操作:

sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(f)));

了解这个选项确实很好,但不幸的是,在某些搭载Android 6的设备上无法使用,因此ContentProvider是更可取的解决方案。 - Siarhei

1
 String filePath="/storage/emulated/0/DCIM"+app_name;
    File dir=new File(filePath);
    if(!dir.exists()){
        dir.mkdir();
    }

这段代码在onCreate方法中。它用于创建一个名为app_name的目录。现在,可以使用Android默认文件管理器访问该目录。在需要设置目标文件夹的地方,请使用此字符串filePath。我确定此方法也适用于Android 7,因为我已经测试过了。因此,它也可以在其他版本的Android上工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接