Tuesday, December 27, 2011

ListView сохранение положения


Хочется зафиксировать элегантный способ решения проблемы. Есть ListView, отображающий файлы и папочки, по которым можно перемещаться. Заказчик пожаловался, что при возврате на уровень выше (по аппаратной кнопке back) положение списка должно сохраняться. То есть если мы находились на середине списка, где щелкнули по какой-то папочке, то потом мы должны снова оказаться в середине списка.
Первой попыткой наугад было просто сохранить в стеке (в котором хранилось содержимое каждой папочки уровня выше текущего) Y-координату. Сохранить по getScrollY(), восстановить через scrollTo(O, y). Однако обнаружилось, что getScrollY() - это метод View, а не ListView, и соответственно, возвращает он значение, немного другое по смыслу.
Обнаружился соответствующий вопрос на stackoverflow. Выигрышным ответом было выполнить следующее:
// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
// ...
// restore
mList.setSelectionFromTop(index, top);

Решения понятное, но слишком уж низкоуровневое. Чуть ниже оказался ответ всего с одним плюсом (уже с двумя):
// Save ListView state
Parcelable state = listView.onSaveInstanceState();
// Restore previous state (including selected item index and scroll position)
listView.onRestoreInstanceState(state);

Вот и все! Вызовы всех более низкоуровневых методов ListView осуществляет уже у себя внутри. Все крайне очевидно и понятно.
Мораль такова, что всегда нужно пытаться мыслить более абстрактно, и не забывать про удобный метод View.onSaveInstanceState().

Wednesday, December 14, 2011

ADT 16

На днях ADT обновился до 16-й версии. Самое значительное изменение - появление Lint, утилиты, которая анализирует Android-проект и выявляет потенциальные баги, находит типичные ошибки раскладок (layouts), неиспользуемые ресурсы.

Раньше для поиска неиспользуемых ресурсов приходилось пользоваться сторонними утилитами (как, например, android-unused-resources), удобство пользования которыми, признаемся, оставляло желать лучшего. Я даже думал написать свою собственную утилиту (отчасти для себя, отчасти для того, чтобы получить плюс в карму за создание и поддержку полезного проекта с открытым исходным кодом :)), не взялся отчасти и от того, что ожидал чего-то подобного от Google, а инициатива от Google убила бы проект на корню. Дождался!

Полноценного анализа java-кода пока не производится, как и не производится анализ связей между проектами (например, ресурсы могут не использоваться внутри библиотечного проекта, но использоваться другими проектами в том же workspace), но, следует полагать, Lint будет только развиваться.

Всем удачного обновления и приятного дня!

Friday, November 25, 2011

Motorola DroidX - useless HDMI port


http://www.androidpolice.com/2010/07/06/droid-x-hdmi-out-is-crippled-to-play-only-videos-or-display-photos-taken-on-the-device/
http://techcrunch.com/2010/08/30/motorola-droid-xs-hdmi-port-is-only-active-in-gallery-app/

Инженерам Motorola DroidX - пламенный привет. HDMI-выход в этом телефоне работает только тогда, когда вы запустите приложение "Галерея". Пустить изображение-видео из любого другого приложения на экран телевизора не получится. Нет слов.
Еще надо заказчику это объяснить.

Monday, October 24, 2011

Затемнение ImageView по нажатию

Недавно разобрался с такими типами Drawable, как LayerDrawable и StateListDrawable.
Для начала опишу задачу. Есть GridView, каждая ячейка которого представляет собой картинку 80x80. Картинка получается с сервера. Не так давно заказчик захотел, чтобы ячейка представляла собой не просто картинку, а картинку + круговую тень поверх нее, примерно вот такую, чтобы приложение смотрелось немного "гламурнее":
Кроме того, при нажатии на картинку картинка должна была еще затемняться одноцветной черной полупрозрачной маской.
Как это реализовать?
Вручную программно выполнять какие-то наложения картинок не захотелось сразу. Почти сразу решил сделать 3 картинки во FrameLayout, лежащих одна на другой, но после небольшой попытки выполнять соответствующий setVisibility на полупрозрачную маску по событию MotionEvent от этого тоже захотелось отказаться.
Вспомнились селекторы, которые позволяют указать соответствующий drawable элементу в зависимости от своего state - pressed, focused, и др. Но на свойство visibility нельзя повесить никакой selector, что, в принципе, логично. Поиск привел меня к LayerDrawable, которые позволяют создавать один Drawable по слоям из нескольких. Идея свелась к тому, чтобы использовать selector, в котором на pressed state подключать LayerDrawable с тремя вложенными картинками, на обычный state - с двумя.
Однако - сами-то картинки (не декоративные, а информационные) каждый раз разные! Поэтому на чистом xml, при всем желании, реализовать бы все не удалось, поэтому я плюнул, и решил все реализовать программно с использованием уже знакомых LayerDrawable и StateListDrawable (который может реализовать selector прямо в коде).
Получившийся код:
public class ObscuredImageView extends ImageView {
    protected static final String TAG = "ObscuredImageView";
    private Drawable _innerShading;
    private Drawable _obscured;

    public ObscuredImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        _innerShading = getContext().getResources().getDrawable(R.drawable.inner_shading);
        _obscured = getContext().getResources().getDrawable(R.color.semitransparent);
    }

    @Override
    public void setImageBitmap(Bitmap bitmap) {
        Log.v(TAG, "Going to set state drawable to ImageView");
        BitmapDrawable image = new BitmapDrawable(getContext().getResources(), bitmap);
        Drawable pressed = new LayerDrawable(new Drawable[] { image, _innerShading, _obscured });
        Drawable normal = new LayerDrawable(new Drawable[] { image, _innerShading });
        StateListDrawable states = new StateListDrawable();
        states.addState(new int[] { android.R.attr.state_pressed }, pressed);
        states.addState(new int[] { android.R.attr.state_focused }, pressed);
        states.addState(new int[] {}, normal);
        setImageDrawable(states);
    }
}

Решение вызывать setImageDrawable в переопределенном методе setImageBitmap на первый взгляд выглядит неудачно и "грязно", но если мы заглянем в исходный код класса ImageView, то мы увидим следующее:
public void setImageBitmap(Bitmap bm) {
    // if this is used frequently, may handle bitmaps explicitly
    // to reduce the intermediate drawable object
    setImageDrawable(new BitmapDrawable(bm));
}
Так что вызов setImageDrawable абсолютно корректен.

Таким образом, я получил работающее и гибкое решение. Если кто-то реализовал подобное другим образом - с интересом приму к сведению.

Friday, October 21, 2011

Meld Diff Viewer

Не так давно открыл для себя отличную программу под Linux для сравнения файлов и директорий - Meld Diff Viewer. Под Mac OS X, как написано здесь , тоже есть билд, правда, не знаю, насколько он симпатичный по сравнению с билдом из более "родной" среды.
Скриншот:
Уверен, каждый программист найдет ей применение.

Wednesday, October 19, 2011

Обновление ADT 14


ADT был обновлен до 14 версии, и сразу вылезла куча ошибок, связанных с использование R-ресурсов проекта-библиотеки.

Теперь R-файл библиотек содержит не константы, а обычные целые числа, что, в определенных случаях, позволяет избежать некоторых трудностей, как, например, переопределение какой-нибудь константы R.id и быстрее компилировать сложные проекты. Однако, использовать R.id в конструкциях switch-case не получится, т.к. в case обязаны лежать только константы. К счастью, Eclipse позволяет автоматически преобразовать switch-case в if-else по Ctrl+1 (Cmd+1 на маках).

Подробнее можно посмотреть здесь: http://tools.android.com/tips/non-constant-fields

UPD 21.10.2011. Если бы изменения касались только R-ресурсов :-( Навскидку за последние 48 часов, проблемы, с которыми столкнулся лично я:
http://groups.google.com/group/android-developers/browse_thread/thread/9findViewById93dc13262b84441/2c9d0f52e8a16bc1?lnk=raot
http://code.google.com/p/android/issues/detail?id=21031 (UPD 24.10.2011 - на эту проблему получен ответ от Tech Lead for the Android SDK at Google https://groups.google.com/group/adt-dev/msg/c1f4d73072f8b0e8)
UPD 27.10.2011. http://code.google.com/p/android/issues/detail?id=21162
http://code.google.com/p/android/issues/detail?id=21048
Так что обновляться пока никому посоветовать нельзя. А мне - урок, что незачем обновляться сразу после выхода обновления, а если обновляться, то аккуратно и с возможностью бысрого бэкапа.
UPD 28.10.2011. Вышел ADT 15, который должен исправлять многие проблемы. Попробую обновиться через пару дней.

Tuesday, August 30, 2011

Правила хорошего тона

В этом посте я постараюсь собрать правила хорошего тона при Android-разработке.
Все сразу вспомнить все равно не удастся, поэтому пункты еще долго будут добавляться.

1. Константы, используемые как имена (а такжы и как возможные значения) для данных, передаваемых через Intent, должны начинаться с INTENT_. Использовать данные из Intent нужно только один раз в методе onCreate(). Все это позволяет программисту сразу видеть, какие данные передаются или могут передаваться в Activity. Мне не нравится, когда данные, передаваемые фактически как входные аргументы, инициализируются в середине работы Activity непонятно в каком методе.

2. Переопределять методы onCreate(), onResume(), onDestroy() и прочие методы класса Activity следует как protected, вызываться из других классов они не должны. Также не следует вручную обращаться к методам, начинающимся с "on", таким, как onActivityResult() и другим. Я никогда не догадаюсь смотреть Call Hierarchy какого-либо из этих методов.

3. Все свойства (SharedPreferences) приложения должны считываться и записываться через статические методы get-set одного специального класса (который можно назвать, например, Preferences). Так мы сможем, во-первых, всегда видеть, какие в принципе свойства приложения устанавливаются, во-вторых, анализировать в любой момент, откуда какое свойство считывается или записывается.

4. Забудьте про такой xml-атрибут объекта View, как android:onClick. Он нарушает все возможные правила по разделению интерфейса и логики.
Когда я просматриваю код Activity, я ожидаю, что я буду видеть всю логику в этом же файле. Когда я вижу незнакомый метод неочевидного назначения, который не вызывает ни один другой метод, я его со спокойной совестью удалю (или сначала закомментирую на недельку).

5. Подавляющее большинство слушателей (listeners) должны быть реализованы как поля класса с говорящими именами. Если я хочу изменить поведение кнопки button1, то я не хочу пролистывать весь код, чтобы найти место, в котором кнопке назначается какой-то сразу реализованный слушатель. Я хочу пролистать список полей класса, и увидеть что-то вроде button1Listener. Исключения:
1) Логика нескольких слушатетелей достаточно тесно переплетается. В этом случае может быть удобно создать отдельный listener сразу для нескольких элементов (и распознавать элементы через передаваемый аргумент View v).
2) В некоторых случаях слушателем может быть назначено само Activity. На мой взгляд, это уместно, если назначение метода слушателя очевидно внутри Activity. Например, когда весь Activity фактически представляет из себя ListView, то реализация самим Activity интерфейсов OnItemClickListener или OnItemLongClickListener вполне логична. Назначать же какой-нибудь OnSeekBarChangeListener для какого-то ползунка в углу экрана на целое Activity смысла не имеет и путает программиста.

6. Если обращение к какому-то View-элементу происходит из Activity не один раз, то его нужно выносить в поле класса с инициализацией и назначением всех слушателей в onCreate() (или отдельном методе, например, initUI(), вызываемом из onCreate()) сразу после setContentView(). Так можно сразу увидеть, какие графические элементы активны, т. е. связаны с логикой приложения, а какие - нет. findViewById(int) внутри произвольного метода недопустимо.

7. Все создаваемые диалоги должны храниться в полях класса, а в методе onDestroy() для каждого диалога должен вызываться метод dismiss(). Примерно так:

@Override
protected void onDestroy() {
    if (progressDialogGettingData != null) {
        progressDialogGettingData.dismiss();
        progressDialogGettingData = null;
    }
    super.onDestroy();
}

Иначе, если Activity по каким-то причинам закрылось (например, при перевороте девайса), а диалог нет, и через несколько секунд поток, работающий с этим диалогом, вместе с завершением работы попытается закрыть и диалог, то приложение ждет force close. Мелочь, а неприятно. Конечно, в файле манифеста у Activity может быть установлена одна ориентация, или запрет закрытия Activity при перевороте, тогда закрыть Activity при открытом not cancellable диалоге будет затруднительно, но точно сказать, как поменяется манифест, нельзя, а такая конструкция в любом случае нам никак не повредит.

8. Элементы раскладки, не контролируемые из кода, не должны иметь аттрибута android:id вовсе. Это позволит легко, глядя на xml-файл, различать активные и пассивные элементы.

9. Не писать в лог в геттерах, а также в циклах. Если очень надо, то ставить минимальный уровень приоритета (verbose), чтобы кучу дежурных сообщений можно было легко отфильтровать. Когда нужно выловить действительно важное сообщение, а лог пополняется сообщениями со стандартным приоритетом с завидной скоростью - это действительно раздражает.

Sunday, August 28, 2011

Определение ориентации экрана

Установил на VirtualBox x86-версию Android, чтобы отладка не была мученьем (об этом потом). Установил разрешение 1024х600, запустил отлаживаемую программу. Приложение вело себя соответственно портретному режиму.
В коде обнаружилось следующее:
public static boolean isLandscapeMode(Context context) {
    Display display = ((WindowManager) (context.getSystemService(Context.WINDOW_SERVICE))).getDefaultDisplay();
    return display.getRotation() == Surface.ROTATION_90 || display.getRotation() == Surface.ROTATION_270;
}

В документации нашлось, что метод getRotation() returns the rotation of the screen from its "natural" orientation. В общем-то, все понятно, что Android внутри виртуальной машины воспринимает свою ориентацию как "натуральную", не думая о, собственно, разрешении экрана. На телефонах вроде моего любимого HTC ChaCha этот метод также должен работать неправильно. Заменил на решение "в лоб":

public static boolean isLandscapeMode(Context context) {
    Display display = ((WindowManager) (context.getSystemService(Context.WINDOW_SERVICE))).getDefaultDisplay();
    return display.getWidth() >= display.getHeight();
}

Все работает.

Thursday, August 11, 2011

А знаете ли вы, что...


Чтобы удалить подпункт options menu при его подготовке к отображению (в методе onPrepareOptionsMenu(Menu menu)), нужно сделать следующее:
menu.findItem(resourceMenuId).setVisible(false);
Стандартный вызов
menu.removeItem(resourceMenuId);
не сработает!

Sunday, August 7, 2011

Замена ContextMenu в Android

Эта статья - переведенное подведение итогов к вопросам http://stackoverflow.com/questions/6950586/how-could-i-customize-context-menu-item-click-callback и http://stackoverflow.com/questions/6958622/how-to-create-dialog-that-looks-exactly-like-contextmenu .

Контекстное меню на настольных ПК - это меню, вызываемое при нажатии правой кнопки мыши. Контекстное меню используется для того, чтобы предоставить пользователю выбор действия, которые надо совершить над элементом, чье контекстное меню выбрано.
В Android контекстное меню (класс ContextMenu) вызывается нажатием и удержанием графического элемента.

Чтобы контекстное меню было вызвано по нажатию на View, необходимо зарегистрировать в Activity методом registerForContextMenu(View). Все, что делает этот метод - вызывает View.setOnCreateContextMenuListener(OnCreateContextMenuListener), при чем Activity реализовывает интерфейс OnCreateContextMenuListener, так что как единственный параметр он передает ссылку на себя.

За создание контекстного меню отвечает метод Activity onCreateContextMenu (ContextMenu menu, View v,  ContextMenu.ContextMenuInfo menuInfo), который вызывается каждый раз при долгом нажатии на View v, а при выборе пункта контекстного меню вызывается метод onContextItemSelected (MenuItem item). Все просто.

Но тут возникают сложности.
Предположим, в нашем Activity есть несколько View, каждое из которых вызывает контекстное меню. Причем у каждого View контекстное меню совершенно разное, и поведение контекстного меню тоже разное. Тем не менее, обработку действий мы должны проводить в одном методе onContextItemSelected(MenuItem). Не очень логично реализовывать действия контекстного меню над, скажем, картинкой и элементом списка в одном методе, не так ли?
Конечно, можно перенаправиться в метод класса того элемента, чье контекстное меню вызывалось, где и будет реализовываться основная логика, но суть от этого не меняется.

Кроме того, в View должна храниться ссылка на Activity. Вроде бы мелочь, но заводить отдельное поле для того, чтобы зарегистрировать контекстное меню, которое из Activity перенаправится обратно во View не очень-то логично.

Изначально я хотел использовать кое-где ContextMenu, где это возможно, а кое-куда вставить хак - диалоговое окно, выглядящее и ведущее себя как ContextMenu. Оказалось, что да, во внутреннем пакете Android com.android.internal лежат необходимые классы, генерирующие ContextMenu посредством того же диалогового окна. Но копировать необходимые системные layout, недоступные извне, в свой проект мне показалось глупым - где гарантия, что какой-то из производителей Android-фонов не решит слегка изменить внешний вид контекстного меню?

Поэтому я решил не использовать ContextMenu вообще. Вместо контекстного меню можно использовать диалоговое окно (AlertDialog). Класс AlertDialog.Builder предоставляет несколько удобных методов для построения диалогового окна, среди которых самый важный в данном случае - setItems(CharSequence[], DialogInterface.OnClickListener) (также массив строк можно передавать через указатель на ресурс, например, R.array.foo), который создает ListView, адаптер к нему, используя, опять же, какую-то внутреннюю layout, и обработчик (подробнее - здесь). На вид получается точно такое же контекстное меню. Кроме того, сразу на месте можно создать listener, который обработает действия на месте, без перенаправления в другие классы. Таким образом, мы можем реализовывать логику в тех местах, в которых нам это необходимо, а внешний вид диалогов будет одинаковым везде.