Всем привет!
Недавно наткнулся в контакте на простую и гениальную на мой взгляд идею тайм менеждмента! Вот она. Под пунктом 1. Проста и гениальна, как раз такая какой мне нехватало. И решил реализовать простенькое приложение которо бы позволяло следить за временем.
Итак приложение должно быть таким:
1. Иметь возможность переключать одним нажатием один из трех режимов: работа, сон отдых.
2. Сохранять информацию о выбранном режиме даже когда из программы вышли, или телефон перезагружен.
3. Иметь более-менее приятный интерфейс.
Итак приступаем!
Для начала нам надо подготовить графику, я выбрал вот такие вот переключатели
и вот такой вот фон:
Теперь приступаем к кодингу. Реализуем сначала простейший класс выключатель на основе ImageView:
package ru.davidmd.timemanager; import java.util.ArrayList; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.AttributeSet; import android.widget.ImageView; /** * @author David Класс переключатель представляет собой imageView в котором * имеются две картинки. Одна для включенного состояния другая для * выключенного */ public class ExtendedSwitch extends ImageView { /** * Выключено и включено - идентификаторы ресурсов для картинок (R.drawable) */ int ON; int OFF; /** * Сами битмапы, которые используются контролом для прорисвки */ Bitmap bmpOn, bmpOff; /** * Состояние переключателя */ boolean isOn = false; /** * Список всех созданных переключателй необходим для их массового выключения * или включения */ public static ArrayList<ExtendedSwitch> arr = new ArrayList<ExtendedSwitch>(); /** * Контекст */ Context c; /** * Метод для инициализации получает и сохраняет контекст и добавляет * новосозданный объект в список * * @param cont */ private void init(Context cont) { c = cont; arr.add(this); } // Конструктор public ExtendedSwitch(Context context) { super(context); init(context); } // Еще конструктор public ExtendedSwitch(Context context, AttributeSet attrs) { super(context, attrs); init(context); } // И снова конструктор public ExtendedSwitch(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } /** * Устанавливает выбранные картинки из ресурсов как картинки для * выключенного и включенного состояния * * @param on * - идентификатор ресурса drawable для включенного состояния * @param off * - идентификатор ресурса drawable для выключенного состояния */ public void setOnOff(int on, int off) { this.ON = on; this.OFF = off; // Загружаем картинки на включение и выключение this.bmpOn = BitmapFactory.decodeResource(this.getResources(), ON); this.bmpOff = BitmapFactory.decodeResource(this.getResources(), OFF); update(); } /** * Обновляет состояние контрола */ private void update() { if (this.isOn) { this.setImageBitmap(bmpOn); } else { this.setImageBitmap(bmpOff); } } /** * переключет переключатель */ public void switchSelf() { this.isOn = !this.isOn; update(); } /** * Задает новове состояние переключателя * * @param state * - новое состояние истина - включен ложь - выключен */ public void setState(boolean state) { this.isOn = state; update(); } }
Теперь давайте вкратце разберемся что тут к чему. Итак мы переопределили все конструкторы ImageView с тем, чтобы доваить в них функцию init() которая занимается тем, что добавляет текущий ExtendedSwitch в статический список переключателей arr который у нас объявлен тут же. Для чего это нужно? Просто потом нам так будет легче. Помимо этого добавлены два public метода setState() и switchSelf(). Первый позволяет задать состояние переключателя вручную, а второй переключить его. Имеющийся здесь метод update() просто обновляет картинки которые выводятся на экран. Самое интересное — это метод setOnOff() который позволяет задавать картинки для включенного и выключенного состояния переключателя. В этот метод мы передаем два идентификатора ресурса. Тут нет никакаих проверок на валидность этих идентификаторов, поэтому используя этот компонент надо быть крайне осторожным :). А то мало ли какое целое число мы можем передать сюда в качестве ресурса?!
Итак, простейший переключатель готов. Затем я зачем-то реалзовал отдельный класс, сдержащий только один статический метод. Вот и он:
package ru.davidmd.timemanager; import java.util.Date; public class CalRout { /* * Возвращает количество секунд между двумя датами */ public static int getSecondsBetvwen(Date d1, Date d2) { if (d1.before(d2)) { int sec1 = d1.getSeconds(); int sec2 = d2.getSeconds(); int min1 = d1.getMinutes(); int min2 = d2.getMinutes(); int h1 = d1.getHours(); int h2 = d2.getHours(); int day1 = d1.getDate(); int day2 = d2.getDate(); int res = (sec2 - sec1) + (min2 - min1) * 60 + (h2 - h1) * 60 * 60 + (day2 - day1) * 60 * 60 * 24; if (res > 172800) { return -2;// слишком долго был занят одним делом врядли такое // возможно } else { return res; } } else { return -1;// первая дата после второй тут какой-то косяк 🙁 } } }
Ну здесь вопросов вообще быть не должно. Мы просто получаем разницу в секундах между двумя временами. Ну или ошибочные отрицательные значения.
И приступаем к самому главному. Класс activity которая собственно и отображает весь этот беспередел, а также реализует основную логику.
package ru.davidmd.timemanager; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Timer; import java.util.TimerTask; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.Vibrator; import android.util.Log; import android.view.Menu; import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageView; import android.widget.TextView; public class SplashActivity extends Activity implements OnClickListener { // Конистанты выбора public static final int WORK_SELECTED = 1; public static final int REST_SELECTED = 2; public static final int SLEEP_SELECTED = 3; // константы ключи для сохранения в SharedPreferences public static final String LAST_TIME = "lastTime"; public static final String SELECTED = "selected"; public static final String TIME_WORK = "timeWork"; public static final String TIME_REST = "timeRest"; public static final String TIME_SLEEP = "timeSleep"; // Формат времени используемый везде // так вообще не делается но мне так было удобнее SimpleDateFormat simpleDateFormat = new SimpleDateFormat( "dd.MM.yyyy HH:mm:ss"); // время работы public int timeWork = 0, timeRest = 0, timeSleep = 0; // целочисленное значение хранящее текущий выбраный переключатель или ноль // если он не выбран int selected; // Класс обновляет class TextUpdater extends TimerTask { // Этот метод обновляет текст на экране // запускается таймер таском раз в пол секунды @Override public void run() { if (selected > 0) { Date d = new Date(); int t = CalRout.getSecondsBetvwen(lastTimeDate, d); switch (selected) { case WORK_SELECTED: { editor.putInt(TIME_WORK, timeWork += t); break; } case REST_SELECTED: { editor.putInt(TIME_REST, timeRest += t); break; } case SLEEP_SELECTED: { editor.putInt(TIME_SLEEP, timeSleep += t); break; } } lastTimeDate = d; runOnUiThread(new Runnable() { @Override public void run() { updateText(); } }); } } } // Вложенный класс для обработки нажатия на один из // переключателй class SwitchListener implements OnClickListener { // Собственно сам метод который обрабатывает // переключение он достаточно мудреный, но // по сути ничего сложного в нем нет хотя логика его работы // мне поддалась не с первой попытки 🙂 @Override public void onClick(View v) { int k = (int) ((Integer) v.getTag());// Здесь мы получаем тэг // активированного // переключателя Vibrator vbr = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); vbr.vibrate(20); // Чуток вибрации для более приятного ощущения if (((ExtendedSwitch) v).isOn) { // Если текущий переключатель уже // включен тогда for (ExtendedSwitch sw : ExtendedSwitch.arr) { // Выключаем все sw.setState(false); // } Date d = new Date(); // берем текущее время int t; if (lastTimeDate != null) { // и если у нас имеется дата // последнего включения любого // переключателя t = CalRout.getSecondsBetvwen(lastTimeDate, d); // то // считаем // время // медлу // текущим // моментом // и // последним // включением } else { t = 0; // Иначе ноль, но такого случаться не должно 🙂 } editor.putString(LAST_TIME, formatDate(d)); // записываем время // последнего // включения (опять // таки этого делать // не нужно switch (k) { // потому что потом мы запишем сюда что был включен // нулевой переключатель case WORK_SELECTED: { // а здесь смотрим какой именно из // выключателй был выключен editor.putInt(TIME_WORK, timeWork += t); // и сохраняем // время работы break; } case REST_SELECTED: { editor.putInt(TIME_REST, timeRest += t); // или отдыха break; } case SLEEP_SELECTED: { editor.putInt(TIME_SLEEP, timeSleep += t); // или сна break; } } lastTimeDate = d; // в качестве последней даты принимаем текущее // время и дату selected = 0; // устанавливаем выбраным 0 (т.е. ничего не // выбрано) editor.putInt(SELECTED, 0); // и записываем это в хранилище editor.commit(); // комиттим изменения в хранилище } else { // А вот это работает когда мы переключили неактивный // переключатель // т.е. когда мы включили что-то выключенное for (ExtendedSwitch sw : ExtendedSwitch.arr) { // тогда мы все // выключаем sw.setState(false); } ((ExtendedSwitch) v).setState(true); // включаем выбранный // элемент Date d = new Date(); // получаем текущее время int t; if (lastTimeDate != null) { // проверяем если у нас уже есть // предидущее время t = CalRout.getSecondsBetvwen(lastTimeDate, d); // то // считаем // сколько // секунд // прошло // между // текущим // временем // и // временем // последней // активации // какого-то // из // переключателй } else { t = 0; // ну а если предидущего времени нет, то считаем что // прошло 0 } editor.putString(LAST_TIME, formatDate(d)); // записываем текуее // время как // последнее switch (selected) { // смотрим какой из переключателй был // активирован раньше case WORK_SELECTED: { // и записываем время проведенное за тем // или иным видом деятельности editor.putInt(TIME_WORK, timeWork += t); break; } case REST_SELECTED: { editor.putInt(TIME_REST, timeRest += t); break; } case SLEEP_SELECTED: { editor.putInt(TIME_SLEEP, timeSleep += t); break; } } lastTimeDate = d; // теперь считаем временем последнего действия // текущее время editor.putString(LAST_TIME, formatDate(d)); // его записываем editor.putInt(SELECTED, k); // записываем и выбранный // переключатель editor.commit(); // сохраняем изменения selected = k; // и устанавливаем выбранным текущий переключатель } } } SwitchListener lstnr = new SwitchListener(); // Слушатель для переключения ExtendedSwitch sw1, sw2, sw3; // наши переключатели TextView tv1, tv2, tv3; // текст вьюшки для отображения времени SharedPreferences prefs; // преверенсисы для чтения сохраненной информации SharedPreferences.Editor editor; // редактор для записи сохраненной // информации String workStr, restStr, sleepStr; // строки для отображения текщего времени // нужны для интернационализации Date lastTimeDate; // сюда мы будем записывать время посленей активации // любого из переключателй Timer timer; // это таймер он нужен для периодического обновления текста ImageView resetBtn; // а это картинка, которая будет помогать нам сбрасывать // все ImageView infoBtn; // information show 🙂 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); // Получаем сохраненные значения prefs = this.getPreferences(MODE_PRIVATE); int selectedSwitch = prefs.getInt(SELECTED, 0); // какой выключатель был // включен timeWork = prefs.getInt(TIME_WORK, 0); // общее время работы timeRest = prefs.getInt(TIME_REST, 0); // общее время отдыха timeSleep = prefs.getInt(TIME_SLEEP, 0); // общее время сна // здесь и далее получаем и обрабатываем время последнего включения String lastTime = prefs.getString(LAST_TIME, ""); if (lastTime != "") { lastTimeDate = new Date(); try { lastTimeDate = (simpleDateFormat.parse(lastTime)); } catch (ParseException e) { e.printStackTrace(); } Log.d("DATE", formatDate(lastTimeDate)); } editor = prefs.edit(); // получаем все строки которые нам нужны из ресурсов workStr = this.getResources().getString(R.string.work); restStr = this.getResources().getString(R.string.rest); sleepStr = this.getResources().getString(R.string.sleep); // Init text views tv1 = (TextView) this.findViewById(R.id.TextView01); tv2 = (TextView) this.findViewById(R.id.TextView02); tv3 = (TextView) this.findViewById(R.id.TextView03); // ImageView resetBtn = (ImageView) this.findViewById(R.id.imageView1); resetBtn.setOnClickListener(this); infoBtn = (ImageView) this.findViewById(R.id.imageView2); infoBtn.setOnClickListener(this); // инициализируем переключатели sw1 = (ExtendedSwitch) this.findViewById(R.id.SW1); sw1.setTag(WORK_SELECTED); // Устанавливаем теги, чтоб потом // при обработке переключения было легче sw2 = (ExtendedSwitch) this.findViewById(R.id.SW2); sw2.setTag(REST_SELECTED); sw3 = (ExtendedSwitch) this.findViewById(R.id.SW3); sw3.setTag(SLEEP_SELECTED); // здесь передаем идентификаторы ресурсов для каждого из состояний // переключателя sw1.setOnOff(R.drawable.btn_on, R.drawable.btn_off); sw2.setOnOff(R.drawable.btn_on_blue, R.drawable.btn_off_blue); sw3.setOnOff(R.drawable.btn_on_green, R.drawable.btn_off_green); // устанавливаем для всех один лисенер sw1.setOnClickListener(lstnr); sw2.setOnClickListener(lstnr); sw3.setOnClickListener(lstnr); // включаем последний включенный переключатель switch (selectedSwitch) { case WORK_SELECTED: { if (lastTime != "") { } sw1.setState(true); selected = 1; break; } case REST_SELECTED: { sw2.setState(true); selected = 2; break; } case SLEEP_SELECTED: { sw3.setState(true); selected = 3; break; } default: { break; } } // Обновляем надпись на экране updateText(); // Запускаем обновление времени на экране // для этого создаем новый таймер если он еще не создан if (timer == null) { timer = new Timer(); timer.schedule(new TextUpdater(), 0, 500); // timer. } } // Этот метод обновляет время на экране public void updateText() { synchronized (this) { tv1.setText(workStr + " " + timeWork / 60 / 60 + ":" + timeWork / 60 + ":" + timeWork % 60); tv2.setText(restStr + " " + timeRest / 60 / 60 + ":" + timeRest / 60 + ":" + timeRest % 60); tv3.setText(sleepStr + " " + timeSleep / 60 / 60 + ":" + timeSleep / 60 + ":" + timeSleep % 60); } } // форматирует дату и время и возвращает строку которая будет // использоваться для сохраннения времени protected String formatDate(Date date) { String strTime = simpleDateFormat.format(date); return (strTime); // 02/10/12 } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.splash, menu); return true; } // А это метод который обрабатвает нажатие на кнопку сброса // и вызов активити с подсказкой @Override public void onClick(View arg0) { if (arg0.getId() == R.id.imageView1) { editor.putInt(TIME_WORK, 0); editor.putInt(TIME_REST, 0); editor.putInt(TIME_SLEEP, 0); editor.putInt(SELECTED, 0); editor.putString(LAST_TIME, ""); timeRest = 0; timeSleep = 0; timeWork = 0; for (ExtendedSwitch sw : ExtendedSwitch.arr) { sw.setState(false); } this.selected = 0; editor.commit(); } if (arg0.getId()==R.id.imageView2){ Intent i = new Intent(this,ru.davidmd.timemanager.HelpActivity.class); this.startActivity(i); } } }
Вуаля. Теперь посмотрим что здесь к чему.
Во-первых у нас здесь имеется куча констант. Думаю в комментах к каждой вы прочтете что к чему 🙂
Во-вторых — Имеется два вложенных класса. Первый — TextUpdater наследуется от TimerTask и в методе run() обновлет текстовые поля на экране. Тут все просто и понятно, в зависимости от того, какой из выключателей включен мы обновляем текст в соотвествующем TextView. Единственный интересный момент который может вызвать вопросы вот:
runOnUiThread(new Runnable() { @Override public void run() { updateText(); } });
На самом деле все просто. Поскольку нельзя изменять состояние визуальных контролов из потоков кроме потока UI то мы запускаем в потоке UI (для этого используется метод runOnUiThread()) новый Runnable, в котором вызываем метод нашей активити updateText(). Экземпляр нашего класса TextUpdater мы будем использовать при запуске activity. Его мы передадим как задание таймеру timer.
Слудующий вложенный класс — SwitchListener фактически реализует всю логику приложения. Это класс имплементирующий интерфейс OnClickListener. И именно в методе onClick() и происходит все самое интересное. Экземпляр этого класса — lstnr используется как слушатель для всех трех переключателей имеющихся на экране. Давайте чуть подробнее остановимся на метое onClick(). Он как бы разбит на две части. Одна выполняется если текущий выключатель был активен. В таком случае щелчком по нему мы его выключаем и останавливаем следежние за временем. Здесь мы сразу записываем в SharedPreferences состояние нашей программы: т.е. когда какой выключатель был выключен, сколько времени он простоял во включенном состоянии, т.е. сколкьо времени мы занимались тем или иным делом. А вторая часть очень похожа на первую но выполняется тогда, когда мы включили не задействованный переключатель а значит выключаем тот, который был задействован до этого, и так же сохраняем информацию о том, сколько вермени мы уделили предидущему занятию. В общем метод достаточно подробно закомментирован, надеюсь вы разберетесь и даже подскажете как его улучшить!
Ну и onCreate() самой активити. Здесь мы инициализируем все наши переменные, связываем все компоненты UI с переменными класса. Обартите внимание на вот эти строки:
sw1.setOnOff(R.drawable.btn_on, R.drawable.btn_off); sw2.setOnOff(R.drawable.btn_on_blue, R.drawable.btn_off_blue); sw3.setOnOff(R.drawable.btn_on_green, R.drawable.btn_off_green);
Это как раз то о чем я говорил выше. Это инициализация ресурсами наших ExtendedSwith. В конце onCreate() мы запускаем таймер с тем самым заданием TextUpdater.
Ну вот, на этом и все.
Само приложение можно скачать из маркета по этой ссылке.
Есть вопросы, предложения, или что-то было непонятно? Пишите в комментах!
у Вас есть исходники данного приложения?
А если не секрет к чему они Вам? Тут вроде все нормально расписано 🙂
Полностью согласен с автором))
Спасибо за статью и за то, что делитесь знаниями!
Будьте добры исходники
Добрый день!
Что-то не работает у меня, помогите, плз.
1-04 13:32:43.155 32097-32097/ru.motorin.log D/dalvikvm? GC_FOR_ALLOC freed 87K, 12% free 9600K/10856K, paused 17ms, total 17ms
11-04 13:32:43.155 32097-32097/ru.motorin.log I/dalvikvm-heap? Grow heap (frag case) to 13.810MB for 3456016-byte allocation
11-04 13:32:43.190 32097-32106/ru.motorin.log D/dalvikvm? GC_FOR_ALLOC freed <1K, 9% free 12975K/14232K, paused 33ms, total 33ms
11-04 13:32:43.355 32097-32097/ru.motorin.log D/dalvikvm? GC_FOR_ALLOC freed 1K, 9% free 12973K/14232K, paused 12ms, total 13ms
11-04 13:32:43.390 32097-32097/ru.motorin.log D/AndroidRuntime? Shutting down VM
11-04 13:32:43.390 32097-32097/ru.motorin.log W/dalvikvm? threadid=1: thread exiting with uncaught exception (group=0x41d69700)
11-04 13:32:43.400 32097-32097/ru.motorin.log E/AndroidRuntime? FATAL EXCEPTION: main
java.lang.RuntimeException: Unable to start activity ComponentInfo{ru.motorin.log/ru.motorin.log.MainActivity}: java.lang.ClassCastException: android.widget.ImageView cannot be cast to ru.motorin.log.ExtendedSwitch
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2295)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2349)
at android.app.ActivityThread.access$700(ActivityThread.java:159)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1316)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:176)
at android.app.ActivityThread.main(ActivityThread.java:5419)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:525)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1046)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:862)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.ClassCastException: android.widget.ImageView cannot be cast to ru.motorin.log.ExtendedSwitch
at ru.motorin.log.MainActivity.onCreate(MainActivity.java:219)
at android.app.Activity.performCreate(Activity.java:5372)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1104)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2257)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2349)
at android.app.ActivityThread.access$700(ActivityThread.java:159)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1316)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:176)
at android.app.ActivityThread.main(ActivityThread.java:5419)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:525)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1046)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:862)
at dalvik.system.NativeStart.main(Native Method)
У кого это заработало, помогите..
Не создается ExtetndedSitch, уже полкнижки про Яву прочитал, а он все не создается))
ExtetndedSwitch