Android компонент с нуля

Всем привет! Создание собственных компонентов интерфейса часто является необходимостью чтобы выделиться из общей массы похожих программ. В этой статье как раз рассматривается создание простого, нестандартного компонента на примере кнопки-таймера.

Задание:

Разработать кнопку-бегунок, которая работает следующим образом: прямоугольная область, слева находится блок со стрелкой, показывающий направление сдвига:

Пользователь зажимает стрелку и переводит её в право, по мере отвода, стрелка вытягивает цветные квадратики:

Как только пользователь отпускает блок, то вся линия сдвигается влево и скрывает все показанные блоки. После скрытия последнего блока должно генерироваться широковещательное сообщение что лента полностью спрятана.

Подготовка

Для создания нового компонента создадим новый проект. Далее создаём новый класс с именем «CustomButton», в качестве предка используем класс «View». Далее создадим конструктор класса и в итоге наш будущий компонент будет иметь вид:
package com.racckat.test_coponent;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;

public class CustomButton extends View {

	public CustomButton(Context context, AttributeSet attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
	}

}

Теперь приступаем к написанию кода класса. Прежде чем начать писать код, скиньте в папку /res/drawable-hdpi, изображение разноцветной ленты. В конструкторе нужно перво наперво инициализировать все объекты и сделать все предварительные настройки. Делаем следующее:
1 — Копируем ссылку на контекст основной активности;
2 — Загружаем подготовленную заготовку-полоску разделённую цветными квадратиками;
3 — Настраиваем компонент необходимый для рисования на поверхности/
public CustomButton(Context context, AttributeSet attrs) {
		super(context, attrs);
		_Context = context;	// Сохраняем контекст
		// Загрузка заготовок
		_BMP_line = BitmapFactory.decodeResource(getResources(),R.drawable.line);
		// Настройка шрифта
		mPaint = new Paint();
		mPaint.setAntiAlias(true);
		mPaint.setTextSize(16);
		mPaint.setColor(0xFFFFFFFF);
		mPaint.setStyle(Style.FILL);
	}

Также объявим объекты в начале класса:
	private Paint mPaint;		// Настройки рисования
	public Bitmap _BMP_line;	// Цифровая линия
	Context _Context; 			// Контекст

Теперь нам необходимо переопределить процедуру настройки размеров компонента — onMeasure. Я специально сделал постоянные размеры для компонента (300*50) чтобы не усложнять пример. Процедура будет иметь вид:
	@Override
	protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) {
		setMeasuredDimension(300, 50);
	}

Теперь переопределим процедуру перерисовки компонента «onDraw». Данная процедура вызывается каждый раз когда необходимо перерисовать компонент. Процедура будет иметь вид:
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawRect(0,0, 300, 50, mPaint);
        canvas.drawBitmap(_BMP_line, 0, 0,null);
    }

Заготовка для нашего нового компонента готова, давайте поместим её на главную активность. Во первых разместим на главной поверхности новый LinearLayout, под именем «LinearLayout1». Далее в конструкторе класса создадим класс для новой кнопки, создадим класс реализации«LinearLayout1» и добавим кнопку на поверхность. Класс активности будет иметь вид:
package com.racckat.test_coponent;
import android.os.Bundle;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.widget.LinearLayout;

public class MainActivity extends Activity {

	@SuppressLint("WrongCall")
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		LinearLayout _LL1 = (LinearLayout) findViewById(R.id.LinearLayout1);
		CustomButton _CB1 = new CustomButton(MainActivity.this, null);
		_LL1.addView(_CB1);
		}
}

Если вы запустите проект на выполнение то на устройстве (эмуляторе) вы увидите примерно следующее:


Функционал

Теперь приступим к реализации анимации и реакции на внешние события. Когда пользователь нажимает на компонент интерфейса, предком которого является View, то автоматически генерируются события, в частности можно отследить координаты нажатия на компонент, и этапы нажатия (нажали, подвигали, отжали). Поэтому требуется переопределить процедуру onTouchEvent, отвечающую за внешние события. Процедура имеет один аргумент «MotionEvent event», он содержит в себе все параметры текущего события. Извлекаем эти параметры следующим образом:
		Float X=(Float)event.getX();	// Позиция по X
		Float Y=(Float)event.getY();	// Позиция по Y
		int Action=event.getAction();	// Действие

Приводим процедуру к следующему виду:
	@Override
	public boolean onTouchEvent(MotionEvent event)
	{
		// Вытягиваем совершённое действие
		Float X=(Float)event.getX();	// Позиция по X
		Float Y=(Float)event.getY();	// Позиция по Y
		int Action=event.getAction();	// Действие
		if((Action==MotionEvent.ACTION_DOWN)&&(X<60)&&(_Last_Action==0))
		{
			_Last_Action = 1; // Клик
			_X = 0;
		}
		if((Action==MotionEvent.ACTION_MOVE)&&(_Last_Action == 1))
		{
			_X = (int) (X/60);
			if (_X>4) _X=4; // Если пользователь далеко переставляет бегунок, то запускаем ограничение
			if (_X<0) _X=0;
			invalidate(); // Принудительная перерисовка виджета
		}
		if (Action==MotionEvent.ACTION_UP){
			_Last_Action = 2;
			if (_X>0)
				MyTimer(); // Запуск анимации
			else
				_Last_Action = 0;
		}
		return true;
	}

Каждую строчку расписывать не буду, определю только главную идею. Пользователь нажимает на стрелку компонента, это действие фиксируется в переменной _Last_Action = 1, также фиксируем что пользователь не вытянул ни одного кубика из ленты — _X = 0. Далее отслеживаем перемещение пальца по компоненту и вычисляем сколько кубиков должно показаться на экране, для этого вычисляем _X. Принудительная перерисовка происходит с помощью команды invalidate(). В конце фиксируем отжатие пальца и запускаем таймер, если пользователь вытянул хотя бы один кубик. Таймер необходим чтобы возвращать полоску в исходное состояние не резко, а постепенно.

Теперь реализуем сам таймер, который будет возвращать полоску в исходное положение. Код таймера будет иметь вид:
	// Реализация таймера
	public void MyTimer(){
		Thread t = new Thread(new Runnable() {
	        public void run() {
	        	for(;;){
	        		try {
	        			TimeUnit.MILLISECONDS.sleep(500);
	        		} catch (InterruptedException e) {e.printStackTrace();}
	        		_X--;
	        		myHandler.sendEmptyMessage(0);
	        		if (_X==0){// Проверка что лента вся спрятана
	        			myHandler.sendEmptyMessage(0); // Перерисовка виджета
	        			_Last_Action = 0; // Показатель что анимация закончилась
	        			break; // Выход из цикла
	        		}
	        	}
	          }
	        });
		t.start();
	}

В данной процедуре происходит цикличное выполнение операции уменьшения значения переменной _X на 1, тем самым показывая какой сектор должен быть показан на компоненте. Так как из дополнительных потоков нельзя влиять на внешний вид компонента, приходится посылать сообщения перерисовки через Handle. Поэтому в конструктор класса добавим реализацию перехвата сообщений для Handle и перерисовку внешнего вида виджета:
        myHandler = new Handler() {
            public void handleMessage(android.os.Message msg) {
            	if (msg.what==0){
             		invalidate(); // Принудительная перерисовка виджета
            		}
            }
        };

Теперь осталось изменить процедуру перерисовки виджета, а именно строку позиционирования ленты на поверхности (ширина одного квадратика на ленте, равна 60 pix, а общая длинна составляет 300 pix):
canvas.drawBitmap(_BMP_line, (_X*60)-240, 0,null);

Добавим все переменные в начало реализации класса.
В итоге класс будет меть вид:
package com.racckat.test_coponent;
import java.util.concurrent.TimeUnit;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class CustomButton2 extends View {
	private Paint mPaint;		// Настройки рисования
	public Bitmap _BMP_line;	// Цифровая линия
	int _Last_Action;			// Хранитель последнего действия с виджетом
	int _X = 0; 					// Переключение бегунка на позицию
	public Handler myHandler;	// Объект по работе с потоками
	Context _Context; 			// Контекст
	
	public CustomButton(Context context, AttributeSet attrs) {
		super(context, attrs);
		_Context = context;	// Сохраняем контекст
		// Загрузка заготовок
		_BMP_line = BitmapFactory.decodeResource(getResources(),R.drawable.line);
		// Настройка шрифта
		mPaint = new Paint();
	       	mPaint.setAntiAlias(true);
        	mPaint.setTextSize(16);
	        mPaint.setColor(0xFFFFFFFF);
        	mPaint.setStyle(Style.FILL);
        
        myHandler = new Handler() {
            public void handleMessage(android.os.Message msg) {
            	if (msg.what==0){
             		invalidate(); // Принудительная перерисовка виджета
            		}
            }
        };
	}
	@Override
	public boolean onTouchEvent(MotionEvent event)
	{
		// Вытягиваем совершённое действие
		Float X=(Float)event.getX();	// Позиция по X
		Float Y=(Float)event.getY();	// Позиция по Y
		int Action=event.getAction();	// Действие
		if((Action==MotionEvent.ACTION_DOWN)&&(X<60)&&(_Last_Action==0))
		{
			_Last_Action = 1; // Клик
			_X = 0;
		}
		if((Action==MotionEvent.ACTION_MOVE)&&(_Last_Action == 1))
		{
			_X = (int) (X/60);
			if (_X>4) _X=4; // Если пользователь далеко переставляет бегунок, то запускаем ограничение
			if (_X<0) _X=0;
			invalidate(); // Принудительная перерисовка виджета
		}
		if (Action==MotionEvent.ACTION_UP){
			_Last_Action = 2;
			if (_X>0)
				MyTimer(); // Запуск анимации
			else
				_Last_Action = 0;
		}
		return true;
	}
	// Реализация таймера
		public void MyTimer(){
			Thread t = new Thread(new Runnable() {
		        public void run() {
		        	for(;;){
		        		try {
		        			TimeUnit.MILLISECONDS.sleep(500);
		        		} catch (InterruptedException e) {e.printStackTrace();}
		        		_X--;
		        		myHandler.sendEmptyMessage(0);
		        		if (_X==0){// Проверка что лента вся спрятана
		        			myHandler.sendEmptyMessage(0); // Перерисовка виджета
		        			_Last_Action = 0; // Показатель что анимация закончилась
		        			break; // Выход из цикла
		        		}
		        	}
		          }
		        });
			t.start();
		}
	@Override
	protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) {
		setMeasuredDimension(300, 50);
	}
	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		canvas.drawRect(0,0, 300, 50, mPaint);
		canvas.drawBitmap(_BMP_line, (_X*60)-240, 0,null);
    }
}


Внешние сообщения

Сильно мудрить не будем, реализуем событие что «лента спрятана» с помощью широковещательных сообщений. В реализации таймера добавим строки отправки сообщений:
	        		// Отправка широковещательного сообщения
	        		Intent intent1 = new Intent("com.anprog.develop.timer_button_alarm");
	        		intent1.putExtra(Name, 1);
	        		_Context.sendBroadcast(intent1); // Отправляем широковещательное сообщение

В переменной «Name» хранится имя нашего компонента. Для сохранения имени, создадим дополнительную процедуру:
public void SetName(String _name){
		Name = _name;
	}

Добавим в блок объявления объектов имя компонента — public String Name.
Теперь в конструкторе нашей активности добавим перехватчик широковещательных сообщений:
// Перехват сообщений
		BroadcastReceiver _br = new BroadcastReceiver() {
			// действия при получении сообщений
			@Override
			public void onReceive(Context arg0, Intent intent) {
				int status_alarm_line_button_1 = intent.getIntExtra("line_button_1", 0);
				if (status_alarm_line_button_1==1)
				{
					// Вывод сообщения на экран
					Toast toast = Toast.makeText(getApplicationContext(),"Line alarm!!!", Toast.LENGTH_SHORT); 
					toast.show(); 
				}
			}
		};
		registerReceiver(_br, new IntentFilter("com.anprog.develop.timer_button_alarm"));

После строки создания объекта кнопки, добавим строку передачи нового имени в объект:
_CB1.SetName("line_button_1");	// Установка имени компонента

Всё, не стандартный компонент готов, приступайте к тестированию!
Так должно получиться в идеале — http://youtu.be/3iGxOlWHB0w
Архив примера со всеми комментариями можете скачать по следующей ссылке — http://www.anprog.com/documents/Line_timer.zip
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 20

    0
    В избранное. Спасибо
      +5
      Не нужно выделяться из общей массы программ, пожалуйста. Интерфейс должен быть стандартным и привычным.
        +8
        Это совсем не исключает наличие кастомных компонентов, и уж точно не означает что девелоперы не должны уметь их писать.
        Вы же против крайностей?
        +9
        Хорошая статья, но именование переменных оставляет желать лучшего.
        0
        Контекст доступен через getContext();, но если уж хранить его отдельно, тогда context.getApplicationContext();
          +2
          А еще, по моему мнению, сигнал о завершении скрытия ленты лучше посылать через обычный листенер, ну или хэндлер. В данном примере такое событие слишком мелкое, чтобы сообщать о нем всем. Ну а если уж действительно нужно сообщение — то его можно отправить из листенера. Лишний код и лишняя архетектура, но зато можно переюзать и красиво:)
            +11
            Идея неплоха.
            Уход от GUI Guidelines и создание своих контролов — спорно, но приемлемо.
            Страшненькое именование, паблик переменные, использование классовых Float — плохо, но на общую суть не влияет.
            Хендлер и меседжи вместо runOnUiThread — велосипед, незнание основ.
            Вечный цикл со слипами в MyTimer — говнокод.
              0
              Хендлер и меседжи вместо runOnUiThread — велосипед, незнание основ.


              public final void runOnUiThread(Runnable action) {
                      if (Thread.currentThread() != mUiThread) {
                          mHandler.post(action);
                      } else {
                          action.run();
                      }
                  }
              


              на самом деле не все так страшно, в остальном жирный плюс
              0
              Пожалуйста, не переводите Activity как «активность». Очень режит слух.

              ps — в избранное
                0
                В разных источниках встречал: активность, форма, окно. Мне больше нравится второй вариант, но тем не менее склоняюсь к первому, наиболее близкому к оригиналу.
                  +3
                  думаю будет лучше оставлять в тексте «activity» в таком случае
                  • UFO just landed and posted this here
                  +2
                  По треду на инстанс компонента ради таймера? О_О
                    –3
                    Это просто пример :)
                      +1
                      Пример не должен учить плохому :)
                      Вместо такого ада с потоками можно хотя бы тот же countdown готовый использовать.
                    +4
                    В избранное однозначно. Буду показывать джуниорам в качестве примера как не надо программировать UI под Android в частности и на Java в целом.
                      0
                      Примерно такие же мысли были :D
                      +1
                      если пример претендует на нечто действительно рабочее, а не «вот как можно сделать для моего девайса», то больше, чем 3- я бы не поставил. действительно много велосипедов, нет адаптации под разные экраны, переопределение onMeasure будет работать не всегда правильно. в общем, как то не очень.
                        0
                        Хорошо описаный пример. Спасибо. Как было замеченно, с кодистайлом недочеты есть. Но, покажу пример на своих уроках во ВШЭ.

                        А по сути комментариев, хочу заметить, что хватит разводить холивары на счет «гайдлайна по GUI». Хорошие качественные приложения всегда имеют наитивный интерфейс. Это основная задача дизайнеров. Наша задача, как специалистов своей области, реализовать качественный интерфейс. Возьмем для примера приложения Path, Foursquare, Facebook. В них ничего нет от гайдлайнов, пожалуй, кроме TitleBar, но это качественные приложения. Собирать приложения на стандартных котролах проще, но кастомизация развивает мышление. Да и пользователя не интерисуют гайдланы. Ему нужно чтобы «красиво и удобно и с анимацией», а разработчики упираются в гайдлайны, и это нормально, потому что мы живем этим — стандартами разработки.

                        Простите, накипело.

                        Only users with full accounts can post comments. Log in, please.