Как стать автором
Обновить

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

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

Задание:

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

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

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

Подготовка

Для создания нового компонента создадим новый проект. Далее создаём новый класс с именем «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
Теги:
Хабы:
Всего голосов 52: ↑44 и ↓8+36
Комментарии20

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань