Pull to refresh

Простенький Time Manager для Android

Reading time 13 min
Views 35K
Решил тут на досуге написать статейку о том, как писать приложения для Android'а. Писать будет простенький тайм менеджер. В этой части я напишу саму программу, приделаю к ней интерфейс с анимацией, а в следующей, если будет интересно, напишу к ней сервис, чтоб программа работала еще и в фоновом режиме.

Что будет рассмотрено:
  • RelativeLayout для реализации интерфейса программы с наложением изображений друг на друга.
  • Timer для реализации алгоритма подсчета времени.
  • Animation для свистоперделок красивого интерфейса апплета.


Для наглядности добавил скринкаст как все это добро работает.

Итак, пишем простенький тайм менеджер для Android'а.

Предположим, что SDK + Eclipse у нас уже установлены.


Запускаем Eclipse, и идем в File — New — Other.
В открывшемся окне выбираем Android — Android Project.


Откроется вот такое окно.
Вбиваем параметры:
Project Name — это имя проекта, должно быть уникальным в текущем Workspace.
BuildTarget — версия ОС, для которой собирается проект. Следует помнить, что собирая проект для 1.5, он будет запускаться и на 1.6, и на 2.1, а если собирать для 1.6, то на 1.5 уже нет.
Application Name — название приложения. Будет отображаться в главном меню, откуда его можно будет запустить.
Package Name — com.<название конторы>.<название апплета>
Create Activity — имя Activity для запуска приложения.
Так как скрин делался после написания проекта, вы можете заметить у меня сверху ошибка «There is already a file ...» из-за того, что проект с таким именем уже создан.
Жмем Finish.

Интерфейс



Вот к такому виду его надо привести, при этом размер текста таймеров должен быть одинаковым, увеличиваться \ уменьшаться благодаря Animation.
Для этого используем RelativeLayout с тремя чилдренами: ImageView, для тени вверху апплета, и двумя LinearLayout'ами, один для таймеров и еще один для кнопок внизу.
Если есть время и возможность можете нарисовать все сами, но на всякий пожарный прикладываю архивчик со всеми графическими элементами.

TimeTracker.zip (194Кб)
Распаковываем и кидаем в директорию drawable файлы bg.png, *.9.png, icon.png и topshadow.png.
9.png это 9-patch drawable. Поподробнее о нем можно почитать на developer.android.com/guide/developing/tools/draw9patch.html, замечу лишь, что эти файлы имеют возможность растягивать только определенные части изображения при необходимости. У меня таким образом работают кнопки. Если текст содержимого больше исходного изображения, либо у кнопки стоит атрибут layout_width / height = fill_parent, то android растянет не весь битмап, а только отмеченные участки.

Когда начинал делать не учел, но на будущее замечу, что эти изображения должны иметь минимальную длинну и ширину, у меня же ширина — 100px, поэтому кнопку уже ста пикселей я уже не сделаю.

Теперь необходимо дать знать android'у как растягивать фоновое изображение, header и менять картинки от состояния кнопки.
Для этого создаем в папке drawable файлы: background.xml, greebutton.xml, redbutton.xml и header.xml.

В первый файл пишем:

<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/bg"
android:tileMode="repeat"/>


src — путь к битмапу, а именно к файлу bg.png, что лежит в папке drawable.
tileMode — как им правильно зарисовать поверхность.

Для greenbutton.xml:

<?xml version="1.0" encoding="utf-8"?#62;
<selector xmlns:android="http://schemas.android.com/apk/res/android">
	<item android:state_pressed="true"
		android:drawable="@drawable/greenbutton_dark" /> <!-- pressed -->
	<item android:state_focused="true"
		android:drawable="@drawable/greenbutton_dark" /> <!-- focused -->
	<item android:drawable="@drawable/greenbutton_light" /> <!-- default -->
 </selector>


Указываем битмап для каждого состояния кнопки. Можете, например добавить картинку для состояния focused.

Для redbutton.xml все так же как и в greenbutton.xml, за исключением битмапов. На хабре люди умные, думаю поймете, что туда надо написать.
Для header.xml тоже все так же как и в background.xml, только с другим битмапом.

Перебираемся в папочку layout — там лежат xml файлы, в которых описывается интерфейс апплета.
В нашем случае там лежит один единственный файл, открываем его и приводим к такому виду:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/background"
    >
</RelativeLayout>

RelativeLayout позволяет размещать элементы относительно себя или друг друга. Благодаря этому мы сможем наложить на header таймеры.
Я описал следующие параметры:
orientation — ориентация чилдренов этого лэйаута.
К примеру если разместить две кнопки в LinearLayout с параметром orientation=«vertical», то они будут друг под другом, а с параметром orientation=«horizontal», друг за другом.
layout_width и layout_height — ширина и высота. Принимают три типа значения: fill_parent (принять параметр родителя), wrap_content (минимальное значение, где содержание не будет при этом обрезано), либо числовое значение.
background — фон. Немного выше мы создали background.xml в папке drawable, вот мы к нему сейчас и обращаемся.
Поясню, "@<фолдер в папке res>/<файл в этом фолдере>". Таким же образом задаются идентификаторы, но об этом чуть позже.
Затем внутри RelativeLayout'а мы создаем еще 3 элемента, о которых я говорил чуть выше.
ImageView
<ImageView
	android:src="@drawable/header"
	android:layout_width="fill_parent"
	android:layout_height="100px"
	android:scaleType="fitXY"
	android:layout_alignParentTop="true"
/>

src — путь к header.xml, который мы недавно создали, в котором лежат параметры для тени вверху апплета.
scaleType — как его растягивать.
В принципе можно было обойтись без header.xml, а сразу указать путь к topshadow.png.
layout_alignParentTop — параметры такого типа есть у каждого чилдрена RelativeView, и указывают, где разместить элемент. Конкретно этот говорит о том, что рисовать надо у верхней границы родителя, тоесть RelativeView.

LinearLayout с таймерами
<LinearLayout
	android:orientation="vertical"
	android:layout_width="fill_parent"
	android:layout_height="200dip"
	android:layout_centerVertical="true"
	android:layout_centerInParent="true"
>
	<TextView
		android:text="00:00:00"
		android:id="@+id/rest_timer"
		android:textColor="#AAFFAA"
		android:textSize="40dip"
		android:gravity="center_horizontal|bottom"
		android:singleLine="true"
		android:layout_width="fill_parent"
		android:layout_height="80dip"
	/>
	<TextView
		android:text="00:00:00"
		android:id="@+id/work_timer"
		android:textColor="#FFAAAA"
		android:textSize="40dip"
		android:gravity="center_horizontal|top"
		android:singleLine="true"
		android:layout_width="fill_parent"
		android:layout_height="80dip"
	/>
</LinearLayout>

id — идентификатор виджета. Для создания нового идентификатора используется "@+id/<имя>", для доступа к уже существующему, например в настройках надо сделать пункт зависимым от выбора другого пункта "@id/<имя>".
textColor — цвет текста. Задается либо как #RRGGBB, либо #AARRGGBB, где AA — hex альфа канала (прозрачность), RR, GG и BB — hex каждого цвета.
singleLine — текст в одну строку.
gravity — выравнивание самого текста.
text — попробуйте сами догадаться XD.

LinearLayout с кнопками
<LinearLayout
	android:layout_alignParentBottom="true"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content"
	android:orientation="horizontal"
	>
	<Button
		android:id="@+id/rest"
		android:layout_weight="1"
		android:text="@string/button_rest"
		android:textSize="18dip"
		android:textColor="#FFFFFF"
		android:textStyle="bold"
		android:layout_height="wrap_content"
		android:layout_width="fill_parent"
		android:background="@drawable/greenbutton"
		/>
	<Button
		android:id="@+id/work"
		android:layout_weight="1"
		android:text="@string/button_work"
		android:textSize="18dip"
		android:textColor="#FFFFFF"
		android:textStyle="bold"
		android:layout_height="wrap_content"
		android:layout_width="fill_parent"
		android:background="@drawable/redbutton"
		
		/>	
</LinearLayout>

Тут впринципе все понятно, замечу лишь, что text задан не как строка, а как ссылка на строку в файле со строками… тафталогия какая-то… кароче чуть позже об этом напишу, пока оставте так.

Анимация


Принцип такой. Жмем кнопку, один таймер уменьшается, другой увеличивается.
Создаем в папке res подпапку anim. Туда пихаем 4 xml файла: magnify_rest.xml, shrink_rest.xml, magnify_work.xml и shrink_work.xml.
magnify — увеличение таймера.
shrink — уменьшение таймера.

Открываем magnify_rest.xml.
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
	android:fromXScale="1.0"
	android:toXScale="2.0"
	android:fromYScale="1.0"
	android:toYScale="2.0"
	android:pivotX="50%"
	android:pivotY="100%"
	android:startOffset="0"
	android:duration="400"
/>

fromXScale и fromYScale — значения размеров до начала анимации. Если поставить 2.0, то текст сначала резко увеличится в два раза, и уже от этого значения будет менять свой размер.
toXScale и toYScale — значения размеров к концу анимации. У меня стоит 2.0, тоесть текст будет увеличиваться в два раза от исходного значения, заданного в layout'е, а не от fromXScale и fromYScale.
pivotX и pivotY — точки, от которых происходит анимация. К примеру: если поставить в обоих по 50%, то текст будет увеличен во все стороны, если pivotX = 0%, то текст будет увеличиваться вправо, 100% — влево.
startOffset — промежуток до начала анимации.
duration — время анимации в миллисекундах.

shrink_rest.xml:
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
	android:fromXScale="2.0"
	android:toXScale="1.0"
	android:fromYScale="2.0"
	android:toYScale="1.0"
	android:pivotX="50%"
	android:pivotY="100%"
	android:startOffset="0"
	android:duration="400"
/>

Тут все тоже самое, за исключением размеров.

В итоге я хочу получить:



два таймера друг над другом, выравненных по вертикальному центру родителя. Жму кнопку, верхний увеличивается вверх, нижний уменьшается тоже вверх. Жму на другую, и они уменьшаются и увеличиваются соответственно, только уже вниз. Все это сделано для того, чтоб они не наезжали друг на друга.
Поэтому в верхних двух pivot'ы указаны как нижняя центральная точка. В двух других файлах все тоже самое, за исключением того, что pivot'ы уже указывают на верхнюю центральную точку, тоесть 50% и 0%.

Строки и локализация


Идем в res/values/strings.xml и приводим его к такому виду:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="hello">Hello World, Main!</string>
    <string name="app_name">Time Manager</string>
    
    <string name="button_work">Work</string>
    <string name="button_rest">Rest</string>
</resources>

name — чуть ранее я упоминал про это, так вот конструкция string/hello выдаст нам Hello World, Main!
В этом файле содержаться все строки для использования в программе.

Для локализации создаем директорию values-<локаль>, в нашем случае — values-ru, и копируем туда файл strings.xml, затем открываем его и переводим на русский значения строк.

Программа


Дело за малым. Осталось написать саму программу для того, чтоб вся эта красота зашевелилась, затикала и занажималась.
Открываем java файл в папке src. Он там должен быть один.
Вверху видим название package, в моем случае com.nixan.timetracker.
Потом идут импорты других классов.
Затем объявление самого класса:

public class Main extends Activity

У вас вместо Main может быть что-нибудь еще, а именно, то, что мы задавали в create activity при создании проекта.
Activity — это… аналог форм под виндой, скажем так. Есть еще ListActivity — перечесляемый список каких — нибудь элементов, PreferenceActivity — окно с настройками, при этом все они определяются в отдельном xml файле и нет необходимости изобретать layout под всю эту красоту, но речь не об этом.
У этого класса есть метод onCreate(), который вызывается при создании окна. Таких методов несколько, подробнее можете посмотреть developer.android.com/guide/topics/fundamentals.html#actlife

Перед onCreate задаем переменные:
Button rest_button;
Button work_button;
TextView rest_timer;
TextView work_timer;
Animation magnify_rest;
Animation shrink_rest;
Animation magnify_work;
Animation shrink_work;
TimerTask counter;
Timer timer;
boolean resting;
boolean working;
int rest_time;
int work_time;
SharedPreferences.Editor stats_editor;

rest_button и work_button — кнопки переключения работа/отдых.
rest_timer и work_timer — текст, отображающий текущие значения таймера.
magnify_rest, shrink_rest, magnify_work и shrink_work — анимации, которые мы задавали в xml файле.
counter — то, что таймер будет делать.
timer — сам таймер.
resting и working — переменные, в которых хранятся текущие состояния.
rest_time и work_time — собственно сколько секунд проработали и отдохнули.
stats_editor — настройки, в них хранятся отработанные и отдохнувшие секунды, для того, чтоб закрыв программу, таймер не сбрасывался.

Итак поехали!

Первым делом проверяем, есть ли вызов onCreate родителя.
super.onCreate(savedInstanceState);

Если нет, то дописываем.

Затем говорим этому Activity, какой именно layout надо отрисовать.
setContentView(R.layout.main);

R.layout.main — аналог "@layout/main", где main это main.xml в папке layout. Если ваш layout называется не main, поправьте вызов.
Перед setContentView(); можно дописать еще requestWindowFeature();, у которой в параметрах можно передать, например Window.FEATURE_NO_TITLE, чтоб убрать серый заголовок вверху окна.

Получаем сохраненные параметры таймеров:
SharedPreferences saved_stats = PreferenceManager.getDefaultSharedPreferences(this);
rest_time = saved_stats.getInt("key_rest_time", 0);
work_time = saved_stats.getInt("key_work_time", 0);

getInt — методу передаются два параметра: строка указывающая на имя параметра и параметр по-умолчанию, если строка не найдена.

Инициализируем переменные:
stats_editor = PreferenceManager.getDefaultSharedPreferences(this).edit();
magnify_rest = AnimationUtils.loadAnimation(this, R.anim.magnify_rest);
shrink_rest = AnimationUtils.loadAnimation(this, R.anim.shrink_rest);
magnify_work = AnimationUtils.loadAnimation(this, R.anim.magnify_work);
shrink_work = AnimationUtils.loadAnimation(this, R.anim.shrink_work);
resting = false;
working = false;
rest_timer = (TextView) findViewById(R.id.rest_timer);
work_timer = (TextView) findViewById(R.id.work_timer);
rest_button = (Button) findViewById(R.id.rest);
work_button = (Button) findViewById(R.id.work);


В моем случае я сделал преобразование секунд в строку ЧЧ: ММ: СС двумя разными способами.
Заранее sorry for my bad Java, но думается мне, что первый кушает меньше памяти, но работает медленнее, а другой наоборот, поэтому сильно не ругайте.
private String getTime(int time)
	{
		String result = "";
		int hours = time/3600;
		int minutes = (time - (hours * 3600))/60;
		int seconds = (time - (hours * 3600) - (minutes * 60));
		result = String.valueOf(hours) + ":";
		if (minutes < 10)
		{
			result += "0"+String.valueOf(minutes)+":";
		}
		else
		{
			result += String.valueOf(minutes)+":";
		}
		if (seconds < 10)
		{
			result += "0"+String.valueOf(seconds);
		}
		else
		{
			result += String.valueOf(seconds);
		}
		return result;
	}

Я еще раз скажу, не знаю, насколько это правильно и грамотно, но вроде работает.

И сразу после инициализации переменных добавляем:
rest_timer.setText(getTime(rest_time));
work_timer.setText(getTime(work_time));

Таким образом выставляем ранее посчитанные секунды в текст таймеров.

Создаем задание для таймера:
counter = new TimerTask()
	{
		@Override
		public void run() {
		}
	};


Перед Override переменные, которые отвечают за секунды, минуты и часы таймера. Так как весь процесс у меня реализован в одном таймере, то по набору на отдых и работу:
int seconds_r = 0;
int hours_r = 0;
int minutes_r = 0;
String seconds_ind_r;
String minutes_ind_r;
String hours_ind_r;
int seconds_w = 0;
int hours_w = 0;
int minutes_w = 0;
String seconds_ind_w;
String minutes_ind_w;
String hours_ind_w;


И собственно задание счета:
if (resting)
	{
		if (seconds_r == 0 && minutes_r == 0 && hours_r == 0)
		{
			hours_r = rest_time/3600;
			minutes_r = (rest_time - (hours_r * 3600))/60;
			seconds_r = (rest_time - (hours_r * 3600) - (minutes_r * 60));							
		}
		rest_time++;
		seconds_r++;
		if (seconds_r >= 60)
		{
			seconds_r = 0;
			minutes_r++;
			if (minutes_r >= 60)
			{
				minutes_r = 0;
				hours_r++;
			}
		}
		if (seconds_r < 10)
		{
			seconds_ind_r = "0"+String.valueOf(seconds_r);
		}
		else
		{
			seconds_ind_r = String.valueOf(seconds_r);
		}
		if (minutes_r < 10)
		{
			minutes_ind_r = "0"+String.valueOf(minutes_r);
		}
		else
		{
			minutes_ind_r = String.valueOf(minutes_r);
		}
		hours_ind_r = String.valueOf(hours_r);
		runOnUiThread(new Runnable()
		{
			public void run() {
			// TODO Auto-generated method stub
			rest_timer.setText(hours_ind_r+":"+minutes_ind_r+":"+seconds_ind_r);
			}
		});  
	}

resting — булевая переменная, обозначающая, что мы отдыхаем.
runOnUiThread() — исполняет участок кода в потоке с интерфейсом.
Выше — участок кода для отдыха, для работы же просто после этого добавляется схожий код, только с набором секунд, минут и часов для работы, первый if меняется на if (working) и текст мы устанавливаем уже не для rest_timer'а, а для work_timer'а.

Дело за малым: обработка нажатий кнопок.
rest_button.setOnClickListener(new OnClickListener()
	{
		public void onClick(View v) {
		if (!resting)
		{
			rest_timer.startAnimation(magnify_rest);
			resting = true;
		}
		else
		{
			rest_timer.startAnimation(shrink_rest);
			resting = false;
			stats_editor.putInt("key_rest_time", rest_time);
		}
		if (working)
		{
			work_timer.startAnimation(shrink_work);
			working = false;
			stats_editor.putInt("key_work_time", work_time);
		}
	stats_editor.commit();
	}
});       

Если в данный момент мы не отдыхаем, то начинаем отдыхать.
Если отдыхаем, то прекращаем отдыхать.
Если работает, то прекращаем работать.
При этом меняются только логические переменные working и resting, которые каждую секунду проверяются в таймере, и если они включены, то время начинает увеличиваться.

Таким образом нажимая на кнопку дважды мы вообще может выключить таймер, не вклчючая при этом режим работы.

Для кнопки работы все схоже.

Сохраняемя, подключаем трубку и запускаем приложение.

И сразу же находим первый баг. Если таймер запущен, и мы поворачиваем экран, то таймер останавливается.

Находим:
resting = false;
working = false;


И меняем на:
resting = saved_stats.getBoolean("key_resting", false);
working = saved_stats.getBoolean("key_working", false);
if (resting)
{
	rest_timer.startAnimation(magnify_rest);
}
if (working)
{
	work_timer.startAnimation(magnify_work);
}


Находим в каждом обработчике нажатий на кнопку:
stats_editor.commit();


И дописываем перед ним:
stats_editor.putBoolean("key_working", working);
stats_editor.putBoolean("key_resting", resting);

То есть при отрисовке формы, мы будем получать значения ключей, сохраненных при нажатии кнопок.

Запускаем, тыкаем, вроде все ок.

ЗЫ
Если стоит, продолжу чуть позже написанием сервиса, чтоб апплет висел в памяти и не было необходимости постоянно держать его запущенным.
ЗЫЫ
Исходники, если кому интересно — nixan.org/habr/timetracker/TimeManager.zip
ЗЫЫЫ
nixan.org/habr/timetracker/TimeManager.apk — пакет с прогой, либо в маркете pub:Nixan
ЗЫЫЫЫ
Добавил видеоролик.
ЗЫЫЫЫЫ
Простой Тайм Менеджер для Android. Часть 2
Tags:
Hubs:
+53
Comments 31
Comments Comments 31

Articles