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

ссылка на оригинал статьи http://habrahabr.ru/post/188092/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *