Пишем функцию сохранения картинок на SD-карту

    В процессе написания приложения для андроида у меня возникла задача сохранять произвольное изображение в файл на флешке. В этой статье я опишу, как я решил эту проблему, какие трудности встретились мне в процессе, и как они были решены. Хотелось бы отдельно отметить, что я .NET программист, судьба занесла меня в Java-мир только из-за необходимости создания небольших по размеру (поэтому monodroid сразу нет) и довольно простых с точки зрения интерфейса андроид-приложений. Это означает, что я тоже только учусь, а значит буду рад любым советом и замечаниям профессионалов.


    Итак, предположим, что у нас есть ImageView, в котором содержится картинка, необходимая нам в виде файла. Первый же вопрос — куда сохранять эту картинку?

    Самый правильный и гуглом одобренный подход будет сохранять картинку в папку кэша специально выделенную для вашей аппликации.
    Получить абсолютный путь к ней можно вызовом функции:

    context.getCacheDir();
    


    Данная папка очищается при деинсталляции приложения, кроме того ее размер отслеживается системой, и при нехватке места внутренней памяти, файлы оттуда будут автоматически удалены андроидом.

    Есть у данного решения и минусы. Самый главный — папка кэша приложения содержится во внутренней памяти устройства, которой вечно не хватает. Рекомендуется сохранять там лишь временные и не очень большие файлы, сохранение большого количества тяжелых картинок здесь нежелательно.

    Лучше воспользоваться папкой кэша приложения на SD-карте, путь к которой можно получить функцией:

    context.getExternalCacheDir();
    


    Данная папка тоже будет очищена при деинсталляции приложения, но ее размер не отслеживается системой, поэтому перед сохранением туда файлов, ее состояние желательно проверять командой:

    Environment.getExternalStorageState();
    


    В моем случае по совокупности разных факторов оба стандартных решения показались мне неоптимальными, поэтому я просто сохраняю файлы в корне SD-карты, путь туда можно получить функцией:

    Environment.getExternalStorageDirectory();
    


    Теперь о самой процедуре сохранения, ее код приведен ниже.

    ImageView iv; // ImageView, содержащий изображение, которое нужно сохранить
    String folderToSave = Environment.getExternalStorageDirectory().toString(); // папка куда сохранять, в данном случае - корень SD-карты
    
    private String SavePicture(ImageView iv, String folderToSave)
    {
    	OutputStream fOut = null;
    	Time time = new Time();
    	time.setToNow();
    
    	try {
    		File file = new File(folderToSave, Integer.toString(time.year) + Integer.toString(time.month) + Integer.toString(time.monthDay) + Integer.toString(time.hour) + Integer.toString(time.minute) + Integer.toString(time.second) +".jpg"); // создать уникальное имя для файла основываясь на дате сохранения
    		fOut = new FileOutputStream(file);
    
    		Bitmap bitmap = (BitmapDrawable) iv.getDrawable().getBitmap();
    		bitmap.compress(Bitmap.CompressFormat.JPEG, 85, fOut); // сохранять картинку в jpeg-формате с 85% сжатия.
    		fOut.flush();
    		fOut.close();
    		MediaStore.Images.Media.insertImage(getContentResolver(), file.getAbsolutePath(), file.getName(),  file.getName()); // регистрация в фотоальбоме
    	}
    	catch (Exception e) // здесь необходим блок отслеживания реальных ошибок и исключений, общий Exception приведен в качестве примера
    	{
    		return e.getMessage();
    	}
    	return "";
    }
    


    Несколько пояснений по коду.

    File file = new File(folderToSave, Integer.toString(time.year) + Integer.toString(time.month+1) + Integer.toString(time.monthDay) + Integer.toString(time.hour) + Integer.toString(time.minute) + Integer.toString(time.second) +".jpg"); 
    


    Не очень изящное, но простое как карандаш и реально работающее решение получить уникальное имя для файла в формате удобном для сортировки — «2011 11 17 20 31 49.jpg»
    Здесь всего одна маленькая хитрость. Обратили внимание на time.month+1?
    Дело в том, что Java считает месяцы с нуля, и ноябрь получается 10м, а не 11м, как все привыкли, месяцем. Непорядок.

    bitmap.compress(Bitmap.CompressFormat.JPEG, 85, fOut);
    


    Тут тоже все самоочевидно, споры могут возникнуть лишь о процентах сжатия. Мне кажется, что 85% оптимальный вариант, но у каждого может быть свое мнение и свои условия задачи.

    MediaStore.Images.Media.insertImage(getContentResolver(), file.getAbsolutePath(), file.getName(), file.getName());
    


    Интересный финт, о котором многие забывают. Не все андроид-пользователи продвинуты настолько, чтобы отыскать сохраненные вами файлы, даже если вы им показали путь файловой системы. Но вот про приложение «Фотоальбом» знают точно все (это там, где только что снятые фоточки можно посмотреть). Добавьте вышеупомянутую строчку в ваш код, и картинка не только сохранится в нужную директорию, но еще и зарегистрируется в фотоальбоме, и пользователь всегда ее отыщет ее без всяких проблем.

    Кстати, приведенная функция взята из реального работающего приложения Random Pictures, которое показывает случайные картинки из Интернета, а в платной версии программы — имеет возможность сохранения любой картинки к себе на флешку.

    Бесплатная версия Random Picture Free на андроид маркете Random Pictures Free



    Описанная функция сохранения файла работает только в платной версии. Ее цена — 45 рублей, но для хабражителей — конечно же выкладывается бесплатно. (Хорошая традиция, надо сказать!) Ссылка от автора в комментариях.
    Поделиться публикацией
    Комментарии 12
      +1
      Полная версия лежит здесь.
      image
        +1
        Не очень изящное, но простое как карандаш и реально работающее решение...

        Не очень изящное, это мягко сказано.
          +1
          … выделенную для вашей аппликации…

          аппликацию дети из говна и пластелина в садике делают
            0
            Простите, я в России больше семи лет не был, начинаю забывать простые слова.
            Конечно же я имел ввиду application = приложение.
              0
              Тут вы не правы. Аппликацию из бумаги и клея обычно делают.
              А из говна и пластилина скульптура получится. Или художественная лепка на худой конец.
                0
                Знаем, знаем… Причем в основном менты получаются. А если только пластилин использовать, то можно и пожарного попробовать вылепить.
              +1
              Это плохой тон — писать в корень карты. Если пользователь ставит много приложений, пишущих в корень, его карта быстро превращается в файлопомойку, где сложно чтото найти. Тем более файлы остаются после деинсталляции. Пишите лучше в кэш
                0
                Согласен, что в корень класть все подряд — не есть хорошо. Но проблема в том, что кэш приложения обычно ОЧЕНЬ глубоко спрятан в дереве каталогов, и мало кто из непрофессиональных юзеров туда сможет добраться даже с подсказкой, ибо путь действительно — оооочень длинный.

                Тут же (в данном конкретном приложении) часто необходимо сохранить всего 1-2 фотографии, и иметь к ним быстрый доступ, чтобы можно было скинуть на PC или еще что.

                Поэтому и был выбран этот нехороший с точки зрения гайдлайнов, но (в данном конкретном случае) на мой взгляд — оптимальный вариант.

                Есть идея в следующих версиях позволить юзеру указывать путь для сохранения в настройках.
                  0
                  Ну мне кажется сохранение не в корень карты, а хотя бы в папке с именем Вашего приложения, уже сильно улучшит ситуацию, но сохранит возможность быстро обратиться к картинке.
                    +3
                    >просто сохраняю файлы в корне SD-карты

                    Расстрелять! То-то у нас после таких приложений все SD-карточки засорены!

                    >мало кто из непрофессиональных юзеров

                    Зачем им куда то добираться? У них есть Галерея, которая автоматически всё просканирует.

                    >ибо путь действительно — оооочень длинный.

                    По гайдлайнам — /sdcard/Android/com.my.app/ — разве это длинный путь? Если уж не хотите сохранять в ExternalCacheDir так хоть сюда суйте! А теперь вы пропагандируете на Хабре засорение sd-карты своими файлами — причём прямо в корне!
                  0
                  Эх,
                  >по совокупности разных факторов оба стандартных решения показались мне неоптимальными
                  мог бы сделать правильно и чисто, но решил просто валить в кучу в корень карты памяти…

                  Не совсем понял пользу вашей функции ведь вы предварительно установили эту картинку в ImageView, зачем же из неё же потом сохранять на диск? Операция сохранения на диск должна была предшествовать отображению в ImageView.

                  В целом статья довольно примитивна, ни тебе загрузки картинок из интернета, ни использования aSyncTask или потоков, что бы не блокировать файловыми операциями основной поток приложения, я уже не говорю, про одновлеменную загрузку множества картинок, управление очередью загрузки и т.д.
                    0
                    Пара замечаний. Во-первых, как связаны getExternalStorageState и то, что размер чего-то не отслеживается системой? Эту функцию надо вызывать, просто чтобы проверить, доступна ли карта.

                    Во-вторых, соглашусь с тем, что засорять корень карты нехорошо. На мой взгляд, лучший вариант — использовать getExternalFilesDir(Environment.DIRECTORY_PICTURES) и писать туда. Еще вариант — getExternalStoragePublicDirectory.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое