Рано или поздно любого начинающего андроид-разработчика перестает удовлетворять стандартный набор элементов управления. При этом имеется в виду как внешний вид, так и функциональность. И если с внешним видом все более или менее понятно, все достаточно легко кастомизируется, то функционала часто не хватает.

Когда (уже достаточно давно) в одном из своих проектов я столкнулся с необходимостью выбора в настройках одного из трех параметров, решение было очевидно — RadioButton. Но по ряду причин, типа экономии места на экране и некоторых других, возникло желание использовать нечто вроде ToggleButton. Поскольку стандартный Toggle имеет лишь два состояния, был использован костыль в виде программной обработки некоей циклично меняющейся переменной, в зависимости от которой менялись свойства стандартного элемента вроде обычной Button или ImageButton – уже даже не помню. Способ вполне работоспособный, однако не без греха. Первый и самый главный – нарушается Генеральная линия партии, призывающая к раздельному хранению ресурсов и программного кода. Ну и при большом количестве подобных элементов управления код теряет всю свою изящность и привлекательность. Инкапсуляция, опять же, жутко страдает. Посему было решено создать кастомный элемент.

Представленный пример не представляет из себя ничего сложного для опытного разработчика, однако я в свое время был бы весьма рад найти подобное в одном месте, а не собирать по кусочкам. Давайте попытаемся создать кастомный ToggleButton, имеющий произвольное количество (в данном случае четыре) циклически переключающихся состояний. В зависимости от состояния в приведенном примере меняется надпись на кнопке и значок состояния слева (DrawableLeft у кнопки). Однако все настолько просто и прозрачно, что легко применить изменение состояния к любым свойствам контрола.

Первое, что нам необходимо – описать нужные нам свойства в файле res/values/attrs.xml. Как мы уже решили, это будут четыре строки и четыре изображения для различных состояний элемента управления:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomToggle">
    <attr name="customState" format="integer" />
    <attr name="customText_0" format="string" />
    <attr name="customText_1" format="string" />
    <attr name="customText_2" format="string" />
    <attr name="customText_3" format="string" />
    <attr name="customImage_0" format="reference" />
    <attr name="customImage_1" format="reference" />
    <attr name="customImage_2" format="reference" />
    <attr name="customImage_3" format="reference" />
    </declare-styleable>
</resources>

Теперь можем перейти непосредственно к коду нового класса. Отнаследуем его от обычной Button.

public class CustomToggle extends Button implements OnClickListener {
private static final int			STATE_COUNT = 4;
	private int					state;
	private String[]				txt	= new String[3];
	private Drawable[]			img	= new Drawable[3];

	private OnViewChangeListener	listener;
	
	public interface OnViewChangeListener {
		public void onChangeState(int i);
	}
	
	public void setOnViewChangeListener(OnViewChangeListener l) {
		this.listener = l;
	}

	public CustomToggle(Context context) {
		this(context, null);
	}

	public CustomToggle(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
		handleAttributes(context, attrs);
	}

	public CustomToggle(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		setOnClickListener(this);
	}

	private void handleAttributes(Context context, AttributeSet attrs) {
	TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomToggle);
		final int count = a.getIndexCount();
		// Перебираем все имеющиеся атрибуты
		// И присваиваем соответствующим переменным
		for (int i = 0; i < count; i++) {
			int attr = a.getIndex(i);
			switch (attr) {

			case R.styleable.CustomToggle_customText_0:
				txt[0] = a.getString(attr);
				break;

			case R.styleable.CustomToggle_customText_1:
				txt[1] = a.getString(attr);
				break;

			case R.styleable.CustomToggle_customText_2:
				txt[2] = a.getString(attr);
				break;

			case R.styleable.CustomToggle_customText_3:
				txt[3] = a.getString(attr);
				break;

			case R.styleable.CustomToggle_customImage_0:
				img[0] = a.getDrawable(attr);
				break;

			case R.styleable.CustomToggle_customImage_1:
				img[1] = a.getDrawable(attr);
				break;

			case R.styleable.CustomToggle_customImage_2:
				img[2] = a.getDrawable(attr);
				break;

			case R.styleable.CustomToggle_customImage_3:
				img[3] = a.getDrawable(attr);
				break;
			}
		}
		a.recycle();
		setValues();
	}

	// Применяем полученные свойства к контролу
	private void setValues() {
		setText(txt[state]);
		setCompoundDrawablesWithIntrinsicBounds(img[state], null, null, null);
	}

	public void setState(int i) {
		this.state = i;
		setValues();
	}

	private void changeState() {
		if (state < STATE_COUNT) state++;
		else state = 0;
		setValues();
                listener.onChangeState(state);
	}

	public int getState() {
		return state;
	}

public String getCurrentText() {
		return txt[state];
	}

	@Override
	public void onClick(View v) {
		changeState();
	}
}

Я опустил импорты для экономии, если что – Эклипс подскажет.

Теперь, когда новый класс создан, мы можем добавить его на разметку и канонично указать свойства в xml-файле, не забыв добавить свое пространство имен:

<RelativeLayout xmlns:android=http://schemas.android.com/apk/res/android
xmlns:custom_toggle="http://schemas.android.com/apk/res/com.example"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
<com.example.CustomToggle
    android:id="@+id/custom_toggle"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    custom_toggle:customImage_0="@drawable/img_0"
    custom_toggle:customImage_1="@drawable/img_1"
    custom_toggle:customImage_2="@drawable/img_2"
    custom_toggle:customImage_3="@drawable/img_3"
    custom_toggle:customText_0="@string/toggle_0"
    custom_toggle:customText_1="@string/toggle_1"
    custom_toggle:customText_2="@string/toggle_2"
    custom_toggle:customText_3="@string/toggle_3"
/>

</RelativeLayout>

Как говорится, profit.

Мы получили скелет, с которым дальше можем извращаться как угодно. Как варианты: вместо константы STATE_COUNT добавить соответствующие атрибут и метод типа setStateCount(), что позволит программно динамически ограничивать количество доступных состояний. Полный простор для фантазии.

На этом собственно все. Спасибо за внимание.

P.S: Добавил в код интерфейс OnViewChangeListener.

Комментарии (6)


  1. zorgrhrd
    31.03.2015 16:45
    +5

    Было бы неплохо для наглядности прикрепить изображение.


    1. artalex
      31.03.2015 17:54
      -8

      Да там обычная кнопка, чего прикреплять-то?


  1. demand
    31.03.2015 21:39
    +2

    Ну мне кажется совсем не красиво. Все эти атрибуты custom_toggle:customImage_0 — это страх какой-то. Почему не сделать просто массив? Тем более, что в коде все равно оказывается массив. Зачем эти искусственные ограничения?
    И обработка этих атрибутов перестанет быть такой простыней.

    Ну и вообще для таких вещей StateListDrawable, которые можно назначить хоть кнопке, хоть картинке. Почитать как использовать кастомные state можно тут stackoverflow.com/questions/4336060/android-how-to-add-a-custom-button-state


  1. OJV Автор
    01.04.2015 08:33
    -1

    Спасибо за комментарии и конструктивную критику. Согласен с большинством замечаний. Но в то же время я пытался максимально упростить текст для понимания именно начинающими разработчиками. Более-менее продвинутый организм имхо легко догадается использовать string-array вместо отдельных строк. Главная цель у нас — создать контрол с множественным произвольным количеством внутренних состояний, циклически переключающихся при нажатии на элемент. Именно в этих наших кастомных состояниях весь смысл. Согласитесь, ведь тот же StateList надо привязывать к каким-то состояниям, а что предлагают нам стандартные элементы? Два состояния у Toggle (я не беру всякие focused, pressed или enabled). Нас интересует реакция на последовательные нажатия. В приведенном примере с SO я не заметил каких-либо обработчиков нажатий, только способ привязки xml-атрибутов к свойствам элемента, отличный от моего, но принципиально смысла это не меняет. А так да, имея наш набор состояний мы можем привязать к нему любой селектор и с картинками, и со строками и т.д. Имея внутри элемента собственный обработчик нажатий мы можем прикрутить к нему свой интерфейс (добавил в код). Да все можем. Еще раз повторюсь — ориентировался на очень сильно начинающих, ибо и сам далеко не профи.


  1. OJV Автор
    01.04.2015 09:00
    -1

    Как пример — картинка, немного отличающаяся от приведенного кода. Кнопка с тремя состояниями, при изменении которых циклически меняется текст и DrawableLeft, а в одном из состояний — DrawableRight.

    image


  1. Sandor13
    02.04.2015 13:16

    Спасибо, как раз для начинающих