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