Всем привет! Создание собственных компонентов интерфейса часто является необходимостью чтобы выделиться из общей массы похожих программ. В этой статье как раз рассматривается создание простого, нестандартного компонента на примере кнопки-таймера.
Задание:
Разработать кнопку-бегунок, которая работает следующим образом: прямоугольная область, слева находится блок со стрелкой, показывающий направление сдвига:
Пользователь зажимает стрелку и переводит её в право, по мере отвода, стрелка вытягивает цветные квадратики:
Как только пользователь отпускает блок, то вся линия сдвигается влево и скрывает все показанные блоки. После скрытия последнего блока должно генерироваться широковещательное сообщение что лента полностью спрятана.
Подготовка
Для создания нового компонента создадим новый проект. Далее создаём новый класс с именем «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
Автор: SunSunSun