...

понедельник, 29 июля 2013 г.

[Из песочницы] 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

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends: 'You Say What You Like, Because They Like What You Say' - http://www.medialens.org/index.php/alerts/alert-archive/alerts-2013/731-you-say-what-you-like-because-they-like-what-you-say.html


Комментариев нет:

Отправить комментарий