вторник, 11 мая 2010 г.

Использование NumberPicker и кастомизация его внешнего вида

Внимание. Данное решение с NumberPicker не работает на прошивках 2.2 и выше. Другое решение - взять класс NumberPicker из исходных кодов Android и использовать его. Постараюсь переписать статью в ближайшее время.

Наверно многие из вас использовали такие виджеты, как DatePicker и TimePicker. Но функциональность этих виджетов ограничена возможностью выбора даты и времени, но мне захотелось большего - использовать данную функциональность для выбора любого числа в заданном диапазоне (как в контроле NumericUpDown в C#). Такая возможность существует. Виджеты DatePicker и TimePicker основаны на системном виджете NumberPicker, который вы не можете использовать из коробки. Для его использования потребуется написать небольшое количество кода.

Для начала добавим NumberPicker в наш главный layout, xml-код выглядит следующим образом:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.android.internal.widget.NumberPicker
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/numTestNumberPicker"
android:layout_centerInParent="true">
</com.android.internal.widget.NumberPicker>
</RelativeLayout>
У NumberPicker есть 3 метода, которые нам понадобятся setRange, getCurrent и setCurrent. Думаю по названиям методов ясно, что они делают. Сложность состоит в том, что эти методы скрыты для разработчика и вызвать их можно только используя рефлексию. Для удобства использования, я написал helper-класс, который позволяет работать с этими методами. Код класса представлен ниже:
package maximyudin.NumberPickerDemo;

import java.lang.reflect.Method;

public class NumberPicker {
private Object picker;
private Class<?> classPicker;

public NumberPicker(Object o) {
picker = o;
classPicker = picker.getClass();
}

public void setRange(int start, int end) {
try {
Method m = classPicker.getMethod("setRange", int.class, int.class);
m.invoke(picker, start, end);
} catch (Exception e) {
}
}

public Integer getCurrent() {
Integer current = -1;
try {
Method m = classPicker.getMethod("getCurrent");
current = (Integer) m.invoke(picker);
} catch (Exception e) {
}
return current;
}

public void setCurrent(int current) {
try {
Method m = classPicker.getMethod("setCurrent", int.class);
m.invoke(picker, current);
} catch (Exception e) {
}
}
}
В конструктуре класса передается реальный объект NumberPicker (являющийся LinearLayout), полученный с помощью findViewById из нашего главного layout. Рассмотрим как использовать свежеиспеченный NumberPicker на практике.
package maximyudin.NumberPickerDemo;

import android.app.Activity;
import android.os.Bundle;
import android.widget.LinearLayout;

public class NumberPickerDemo extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

LinearLayout numTestNumberPickerView = (LinearLayout) findViewById(R.id.numTestNumberPicker);
NumberPicker numTestNumberPickerObject = new NumberPicker(
numTestNumberPickerView);

numTestNumberPickerObject.setRange(1, 100);
numTestNumberPickerObject.setCurrent(50);
int currentPos = numTestNumberPickerObject.getCurrent();
}
}
В принципе все довольно просто при использовании helper-класса. На экране выглядит это следующим образом:


Теперь приступим ко второй части статьи - полноценному изменению внешнего вида виджета. Для этого используется набор заранее подготовленных мной изображений (из моей программы MyTasks) различных состояний кнопок ('+' и '-') и текстового поля виджета, именно из них состоит NumberPicker. Взять их можно отсюда. Изображения представлены в формате Nine-Patch (9-Patch), такой тип изображений может растягиваться в зависимости, например, от их контента, они часто применяются для установки фона различных виджетов, в том числе и в моем случае. Подробней о 9-Patch вы можете прочитать тут. Изображения необходимо положить в папку drawable, либо в папку drawable-mdpi (-ldpi, -hdpi), как в моем случае.

Для того, чтобы эти изображения заработали нужным нам способом (то бишь реагировали на различные состояния виджетов) необходимо сформировать три xml-файла в селектор-формате (по файлу на каждый виджет - две кнопки и текстовое поле). Селектор представляет собой набор элементов с атрибутами, которые обозначают различные состояния виджета (выбран, нажат, неактивный и т.д.). Эти файлы вы также должны поместить в папку drawable, либо drawable-mdpi (-ldpi, -hdpi). Ниже представлены эти три файла.

Для кнопки '+' (timepicker_up_btn.xml)
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false" android:state_enabled="true"
android:state_focused="false" android:drawable="@drawable/btn_up_picker_notselected" />
<item android:state_pressed="true" android:state_enabled="true"
android:drawable="@drawable/btn_up_picker_pressed" />
<item android:state_pressed="false" android:state_enabled="true"
android:state_focused="true" android:drawable="@drawable/btn_up_picker_selected" />
<item android:state_pressed="false" android:state_enabled="false"
android:state_focused="false" android:drawable="@drawable/btn_up_picker_disabled" />
<item android:state_pressed="false" android:state_enabled="false"
android:state_focused="true" android:drawable="@drawable/btn_up_picker_disabled_focus" />
</selector>
Для текстового поля (ed_picker_background.xml)
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> 
<item android:state_window_focused="false" android:state_enabled="true"
android:drawable="@drawable/edit_picker_notselected" /> 
<item android:state_window_focused="false" android:state_enabled="false"
android:drawable="@drawable/edit_picker_notselected" />
<item android:state_pressed="true" android:drawable="@drawable/edit_picker_pressed" />
<item android:state_focused="true" android:state_enabled="true"
android:drawable="@drawable/edit_picker_selected" />
<item android:state_enabled="true" android:drawable="@drawable/edit_picker_notselected" />
<item android:state_focused="true" android:drawable="@drawable/edit_picker_notselected" />
<item android:drawable="@drawable/edit_picker_notselected" />
</selector>
Для кнопки '-' (timepicker_down_btn.xml)
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false" android:state_enabled="true"
android:state_focused="false" android:drawable="@drawable/btn_down_picker_notselected" />
<item android:state_pressed="true" android:state_enabled="true"
android:drawable="@drawable/btn_down_picker_pressed" />
<item android:state_pressed="false" android:state_enabled="true"
android:state_focused="true" android:drawable="@drawable/btn_down_picker_selected" />
<item android:state_pressed="false" android:state_enabled="false"
android:state_focused="false" android:drawable="@drawable/btn_down_picker_disabled" />
<item android:state_pressed="false" android:state_enabled="false"
android:state_focused="true" android:drawable="@drawable/btn_down_picker_disabled_focus" />
</selector>

Теперь необходимо немного изменить конструктор helper-класса, созданного для NumberPicker в начале статьи. Это позволит нам при создании объекта NumberPicker изменить внешний вид его составляющих. Код модифицированного класса представлен ниже.
package maximyudin.NumberPickerDemo;

import java.lang.reflect.Method;

import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;

public class NumberPicker {
private Object picker;
private Class<?> classPicker;

public NumberPicker(LinearLayout numberPickerView) {
picker = numberPickerView;
classPicker = picker.getClass();

// Кнопка '+', тип - NumberPickerButton
View upButton = numberPickerView.getChildAt(0);
upButton.setBackgroundResource(R.drawable.timepicker_up_btn);

// Текстовое поле, тип - EditText
EditText edDate = (EditText) numberPickerView.getChildAt(1);
edDate.setTextSize(17);
edDate.setBackgroundResource(R.drawable.ed_picker_background);

// Кнопка '-', тип - NumberPickerButton
View downButton = numberPickerView.getChildAt(2);
downButton.setBackgroundResource(R.drawable.timepicker_down_btn);
}

public void setRange(int start, int end) {
try {
Method m = classPicker.getMethod("setRange", int.class, int.class);
m.invoke(picker, start, end);
} catch (Exception e) {
}
}

public Integer getCurrent() {
Integer current = -1;
try {
Method m = classPicker.getMethod("getCurrent");
current = (Integer) m.invoke(picker);
} catch (Exception e) {
}
return current;
}

public void setCurrent(int current) {
try {
Method m = classPicker.getMethod("setCurrent", int.class);
m.invoke(picker, current);
} catch (Exception e) {
}
}
}
Результат представлен ниже на рисунке.



Полный исходный код проекта можно скачать тут.

На этом моя статья завершена, надеюсь я помог вам узнать что-то новое. Жду комментариев и поправок, если есть кому что предложить.