Создание удобного OpenFileDialog для Android
Наверное, как и многие разработчики под Android, столкнулся на днях с необходимостью реализовать в своем приложении выбор файла пользователем. Так как изначально в Android такого функционала нет, обратился к великому и ужасному. Это показалось мне странным, но из вороха вопросов на stackoverflow и небольшого числа отечественных форумов можно выделить всего три основных источника:
- Android File Dialog – почти все ссылки из stackoverflow ведут сюда. В принципе, неплохое решение, но реализовано через отдельную activity, а хотелось чего-то в духе OpenFileDialog’а из .Net.
- В данной статье речь идет вообще об отдельном файл-менеджере, и почерпнуть какие-то идеи из неё не удалось.
- Идея же отсюда очень понравилась, однако, как мне показалось реализовать все это можно несколько красивее.
В результате, начав реализовывать своё решение, я столкнулся с некоторыми трудностями решать которые показалось очень интересно. А посему, решил описать в данной статье не просто готовое решение, а все шаги, которые к нему привели. Желающие пройти их вместе –
Итак, приступим! В любой привычной среде (я использую IntelliJ IDEA) создадим новое приложение. На главной activity расположим одну единственную кнопку и напишем к ней, пока пустой, обработчик нажатия:
Создадим новый класс с конструктором:
а в обработчике кнопки вызовем диалог:
Кнопки показались, теперь надо бы и сами файлы найти. Начнем поиск с корня sdcard, для чего определим поле:
и реализуем следующий метод:
(так как главное требование к классу – работать сразу у любого разработчика, без подключения дополнительных библиотек, — то никаких google-collections использовать не будем, и с массивами приходится работать по старинке), а в конструкторе к вызову setNegativeButton добавим .setItems(getFiles(currentPath), null).
Что же, неплохо, однако файлы не отсортированы. Реализуем для этого дела Adapter как внутренний класс, заменим setItems на setAdapter и немного перепишем getFiles:
Еще лучше, но нам надо по клику на папке идти внутрь. Можно достучаться до встроенного listview, но я просто подменил его собственным (это потом пригодится). Плюс, изменения adapter’а внутри обработчика listview вызывало exception, и список файлов пришлось вынести в отдельное поле:
Отлично, вот только нажав на папку Android мы получаем список всего из одного каталога data, и наше окно тут же уменьшается в размере.
Возможно это нормально, но мне это не понравилось, и я стал искать возможности размер сохранить. Единственный найденный мною вариант – это установка setMinimumHeight. Установка этого свойства для listview вызвала дополнительные проблемы, но они решились оберткой его в LinearLayout:
Результат, все равно получился немного не таким, каким хотелось бы: при старте диалог развернут на весь экран, а после перехода в каталог Android – уменьшается до 750px. Да еще и экраны разных устройств имеют разную высоту. Решим сразу обе этих проблемы, установив setMinimumHeight в максимально возможную для текущего экрана:
Не нужно пугаться того, что мы устанавливаем в setMinimumHeight полный размер экрана, сама система уменьшит значение до максимально допустимого.
Теперь появляется проблема понимания пользователя, в каком каталоге он сейчас находится, и возврата вверх. Давайте разберемся с первой. Вроде все легко — установить значение title в currentPath и менять его при изменении последнего. Добавим в конструктор и в метод RebuildFiles вызов setTitle(currentPath).
А нет – заголовок не изменился. Почему не срабатывает setTitle после показа диалога, документация молчит. Однако мы может это исправить, создав свой заголовок и подменив им стандартный:
И снова не все ладно: если пройти достаточно далеко, то строка в заголовок влезать не будет
Решение с установкой setMaximumWidth не верно, так как пользователь будет видеть только начало длинного пути. Не знаю, насколько верно мое решение, но я сделал так:
Решим теперь проблему с возвратом. Это достаточно легко, учитывая, что у нас есть LinearLayout. Добавим в него еще один TextView и немного отрефракторим код:
Возможность возвращаться на шаг вверх, может привести пользователя в каталоги, к которым ему доступ запрещен, поэтому изменим функцию RebuildFiles:
(cообщение пока не очень информативное, но вскоре мы добавим разработчику возможность исправить это).
Ни один OpenFileDialog не обходится без фильтра. Добавим и его:
Обратите внимание — фильтр принимает регулярное выражение. Казалось бы – все хорошо, но первая выборка файлов сработает в конструкторе, до присвоения фильтра. Перенесем её в переопределенный метод show:
Осталось совсем чуть-чуть: вернуть выбранный файл. Опять же, я так и не понял зачем нужно устанавливать CHOICE_MODE_SINGLE, а потом все равно писать лишний код для подсветки выбранного элемента, когда он (код) и так будет работать без CHOICE_MODE_SINGLE, а потому обойдемся без него:
Источник
AndroidDev #1. Создаем файловый менеджер
Android — перспективная и динамично развивающаяся операционная система. И многие программисты хотят научиться программировать приложения для OS Android, но беглый поиск структурированных материалов в сети Интернет и, в частности, Рунете, приводит их в ступор. Действительно, до сих пор существует проблема поиска обучающих статей (особенно на русском языке) по разработке приложений для этой весьма популярной операционной системы.
Ну, что ж, будем потихоньку улучшать данную ситуацию при помощи Хабра.
Сразу предупрежу, что материалы предназначены для тех, кто не имеет опыта разработки приложений для Android, но очень-очень хочет этот опыт приобрести.
Готовимся
Первое, что нам потребуется — установить и настроить IDE. Мы будем использовать Eclipse, однако важно отметить, что существует возможность использовать любую среду разработки.
Подробная инструкция по установке и настройке IDE Eclipse содержится в документации на сайте Android. Если вкратце:
- Устанавливаем Eclipse Classic отсюда
- Устанавливаем SDK Starter Package отсюда
- Устанавливаем ADT Plugin при помощи данной инструкции, по той же инструкции выполняем действия, описанные в разделе Configuring the ADT Plugin
- Запускаем Eclipse, нажимаем Window > Android SDK and AVD Manager и выбираем необходимые компоненты для установки. Самый простой вариант — выбрать всё, но необходимый минимум — SDK Platform Android 2.1 (или 2.2), Android SDK Tools, Android SDK Platform-tools
После всего этого мы имеем рабочую среду для разработки приложений для ОС Android. Но нам нужно где-то проверять наши приложения, верно? И тут мы стоим перед выбором — либо осуществлять эти действия в эмуляторе, либо на реальном устройстве.
Эмулятор можно добавить в уже знакомом нам окне Android SDK and AVD Manager, нажав кнопку New. во вкладке Virtual Devices. Заполняем поле Name, указываем версию API и добавляем наше виртуальное устройство.
Если же вы хотите тестировать свои приложения на реальном устройстве, смотрите сюда. Важно отметить, что в конечном счёте вам всё равно придётся проверять своё приложение на реальном устройстве, ведь немного странно выпускать приложение, допустим, в Android Market, как следует его не протестировав на действующих образцах телефонов/планшетов.
Создаем проект
Пришло время создать новый проект. Для этого жмём File > New > Project, выбираем в появившемся окне Android > Android Project и жмем Next. Появляется следующее окно, которое мы заполняем примерно так:
Теперь немного объяснений.
Project Name — имя проекта в среде Eclipse.
Application Name — название приложение, то самое, которое будут видеть пользователи на конечном Android-устройстве.
Package Name — название пакета. Тут всё аналогично любому Java-проекту. Важно знать, что данное имя должно быть уникальным среди всех пакетов, имеющихся на конечном устройстве. Посему достаточно эффективна классическая логика давать название, соответствующего веб-домену, записанному наоборот (как тут — ru.alwake), а далее — название самого проекта. Данный шаг в какой-то мере обеспечит уникальность названия.
Create Activity — имя класса, который в будущем будет являться подклассом класса Activity.
Min SDK Version — минимальная версия SDK. Если посмотреть выше на список Build Target в данном окне, то можно прийти к выводу, что наше приложение запускается только для устройств с Android >= 2.1 (Android 2.1 соответствует версия SDK 7). В данном случае это не очень принципиально, но пусть будет так.
Теперь можно смело нажать Finish и лицезреть свой проект в панели Package Exploper. Проект создан и неплохо бы ознакомиться с некоторой теоретической основой по устройству Android-приложений, которая неплохо изложена тут.
О нашем проекте
Для начала определимся, что мы хотим от нашего файлового менеджера. В идеале — полную замену ASTRO и eStrongs. Ну, а пока нам требуется обеспечить базовую навигацию по каталогам, причем под «базовой» мы понимаем, что нам не нужно заходить в те папки, в которые имеет доступ только root. Помимо этого, в поле сверху у нас будет отображаться наше текущее расположение в структуре каталогов.
Ну, а сначала поговорим о структуре проекта:
/res/drawable-*dpi — в этих трёх папках у нас содержатся ресурсы, предназначенные для разных расширений экрана. На данный момент там имеется всего-навсего файл icon.png, то есть иконка нашего приложения.
/res/layout — в данной папке содержатся xml-файлы, описывающие внешний вид форм и различных элементов форм. После создания проекта там уже имеется файл main.xml, также там нужно создать файл row.xml, который будет описывать внешний вид каждого отдельно взятого ряда (то есть элемента списка нашего файлового древа).
/res/values — тут у нас располагаются какие-либо константы, которые мы можем использовать в нашем проекте.
Стоит отметить, что данные XML-файлы можно редактировать как в визуальном режиме, так и в текстовом (меняя непосредственно xml-код). Мы будем действовать по второму способу. Для редактирования кода необходимо нажать правой кнопкой по xml-файлу в панели Package Explorer и выбрать Open with > Text Editor. Это так, на будущее 😉
Файл FileManager.java — в данном файле, собственно, содержится наш основной класс для главной и единственной формы приложения. Весь наш код в данном проекте будет размещаться тут.
Файл AndroidManifest.xml — файл с основными свойствами нашего проекта, в частности, заданными при создании проекта (такими как, например, название). Соответственно, чтобы поменять, предположим, название, нам нужно ковырять данный файл.
Остальные файлы нас пока мало интересуют.
Начинаем писать код
В общем-то, всё что было выше — базовые сведения, которые каждый Android-программист обязан знать. А теперь пришла пора разбираться непосредственно с нашим кодом.
xml version =»1.0″ encoding =»utf-8″ ? >
TableLayout xmlns:android =»http://schemas.android.com/apk/res/android»
android:orientation =»vertical»
android:layout_width =»fill_parent»
android:layout_height =»fill_parent» >
TableRow >
TextView android:id =»@+id/titleManager»
android:layout_width =»fill_parent»
android:layout_height =»fill_parent»
android:padding =»5dip»
/>
TableRow >
TableRow >
ListView android:id =»@id/android:list»
android:layout_width =»fill_parent»
android:layout_height =»fill_parent»
android:layout_weight =»2″
android:drawSelectorOnTop =»false»/>
TableRow >
TableLayout >
* This source code was highlighted with Source Code Highlighter .
Здесь у нас задается разметка для нашего основного Layout’а формы. TableLayout здесь значит, что элементы у нас выстроены в виде таблицы. Далее в верхней ячейке таблицы размещается элемент TextView (текстовое поле), а в нижней ячейке — ListView (список). Оба элемента имеют id, используя который, мы можем изменять содержимое элементов. Например, используя R.id.titleManager для нашего текстового поля TextView.
xml version =»1.0″ encoding =»utf-8″ ? >
TextView
xmlns:android =»http://schemas.android.com/apk/res/android»
android:layout_width =»fill_parent»
android:layout_height =»40sp»
android:padding =»5dip»
android:gravity =»center_vertical»
/>
* This source code was highlighted with Source Code Highlighter .
Здесь мы задаем разметку для каждого элемента нашего ListView, то есть непосредственно для каждой отдельно взятой папки или каждого файла. В данном коде у нас задается ширина каждого элемента, высота, отступ (padding) и выравнивание center_vertical — то есть центрование по вертикали.
- package ru.alwake.filemanager;
- import java.io. File ;
- import java.util. ArrayList ;
- import java.util. List ;
- import android.app.AlertDialog;
- import android.app.ListActivity;
- import android.content.DialogInterface;
- import android.content.Intent;
- import android.content.DialogInterface.OnClickListener;
- import android.net. Uri ;
- import android.os.Bundle;
- import android.view.View;
- import android.widget.ArrayAdapter;
- import android.widget.ListView;
- import android.widget.TextView;
- public class FileManager extends ListActivity <
- private List String > directoryEntries = new ArrayList String >();
- private File currentDirectory = new File ( «/» );
- //when application started
- @Override
- public void onCreate(Bundle icicle) <
- super.onCreate(icicle);
- //set main layout
- setContentView(R.layout.main);
- //browse to root directory
- browseTo( new File ( «/» ));
- >
- //browse to parent directory
- private void upOneLevel() <
- if ( this .currentDirectory.getParent() != null ) <
- this .browseTo( this .currentDirectory.getParentFile());
- >
- >
- //browse to file or directory
- private void browseTo(final File aDirectory) <
- //if we want to browse directory
- if (aDirectory.isDirectory()) <
- //fill list with files from this directory
- this .currentDirectory = aDirectory;
- fill(aDirectory.listFiles());
- //set titleManager text
- TextView titleManager = (TextView) findViewById(R.id.titleManager);
- titleManager.setText(aDirectory.getAbsolutePath());
- > else <
- //if we want to open file, show this dialog:
- //listener when YES button clicked
- OnClickListener okButtonListener = new OnClickListener() <
- public void onClick(DialogInterface arg0, int arg1) <
- //intent to navigate file
- Intent i = new Intent(android.content.Intent.ACTION_VIEW, Uri .parse( «file://» + aDirectory.getAbsolutePath()));
- //start this activity
- startActivity(i);
- >
- >;
- //listener when NO button clicked
- OnClickListener cancelButtonListener = new OnClickListener() <
- public void onClick(DialogInterface arg0, int arg1) <
- //do nothing
- //or add something you want
- >
- >;
- //create dialog
- new AlertDialog.Builder( this )
- .setTitle( «Подтверждение» ) //title
- .setMessage( «Хотите открыть файл » + aDirectory.getName() + «?» ) //message
- .setPositiveButton( «Да» , okButtonListener) //positive button
- .setNegativeButton( «Нет» , cancelButtonListener) //negative button
- .show(); //show dialog
- >
- >
- //fill list
- private void fill( File [] files) <
- //clear list
- this .directoryEntries.clear();
- if ( this .currentDirectory.getParent() != null )
- this .directoryEntries.add( «..» );
- //add every file into list
- for ( File file : files) <
- this .directoryEntries.add(file.getAbsolutePath());
- >
- //create array adapter to show everything
- ArrayAdapter String > directoryList = new ArrayAdapter String >( this , R.layout.row, this .directoryEntries);
- this .setListAdapter(directoryList);
- >
- //when you clicked onto item
- @Override
- protected void onListItemClick(ListView l, View v, int position, long id) <
- //get selected file name
- int selectionRowID = position;
- String selectedFileString = this .directoryEntries. get (selectionRowID);
- //if we select «..» then go upper
- if (selectedFileString.equals( «..» )) <
- this .upOneLevel();
- > else <
- //browse to clicked file or directory using browseTo()
- File clickedFile = null ;
- clickedFile = new File (selectedFileString);
- if (clickedFile != null )
- this .browseTo(clickedFile);
- >
- >
- >
* This source code was highlighted with Source Code Highlighter .
В начале указывается название пакета (package name).
Строки 2-18 отвечают за импорт нужных нам библиотек. Важно отметить, что импорт библиотек Eclipse может производить автоматически, как только встретит что-нибудь неизвестное.
В данном коде у нас всего 5 достаточно очевидных функций, в которых несложно разобраться. И это — скелет приложения, обеспечивающий основную навигацию по файловой структуре. Однако у данного приложения существует одна проблема — при попытке зайти в директорию, доступ к которой разрешен только суперпользователю, мы получаем ошибку и приложение завершает работу.
Соответственно, в следующий раз мы подумаем как избавиться от этой ошибки, а также о том, как сделать отображение иконок, соответствующих типу файла. Кроме того, мы подумаем о реализации функции Copy-Paste в нашем файловом менеджере.
В конечном счёте предполагается получить большое и грамотное приложение, которое должно дать основную информацию об аспектах программирования под Android.
Источник