Nessa continuação do nosso tutorial Timer Count Down vamos implementar a contagem dos intervalos (quando houver) entre as sessões de tempo assim como é feito na Técnica Pomodoro. Também usaremos o CountDownTimer nele, além de usarmos AlertDialog para realizar o controle de interrupção tanto dos intervalos quanto do Temporizador.
O que você vai aprender:
- Refatoração e “limpeza” de código
- Como utilizar AlerDialog
- Como criar String Resource
- Como retornar um resultado ao termino de outra Activity com a função “onActityResult”
- Como usar função “onClick” importada da classe “OnClickListener”
- Sobrescrever as funções: “onBackPressed” e “onSupportNavigateUp”
- Chamar TextView fora da classe Activity
Refatorar código é reestrutura-lo de uma forma que ele fique mais legível e limpo. Começaremos renomeando as variáveis na classe Timer. Basta clicar com botão direito em cima do nome da variável ir em Refactor -> Rename, dessa forma só precisaremos fazer isso uma vez que se refletirá em todos os lugares que essa variável aparecer:

Vamos colocar colocar os nomes mais adequados as variáveis e também deixar no singular as que tem número único para que quem leia entenda isso de cara!
var txt_breakCount: TextView? = null
var breakCount: Int = 0
var totalBreak: Int? = null
var ACTIVITY_BREAK = 0
Agora vamos criar um método para colocar todo que é necessário passar para quando a Activity iniciar! Basta selecionar todas as linhas de código desejadas, clicar com botão direito Refactor -> Extract -> Function. De o nome para a função de “initTimer” e aperte OK:

Também é importante deixar nosso métodos realizar a função que as descreve e somente ela, por isso precisamos tira da função “getData” todo que não esta relacionado com receber os dados e colocar no método “initTimer” (sobrescreveremos a função “onClick” da classe View.OnClickListener para receber o evento de click):
fun getData(){
timerLength = intent.getIntExtra("TIME", 1)
totalBreak = intent.getIntExtra("BREAKS", 0)
}
private fun initTimer() {
img_play!!.setOnClickListener (this)
img_replay!!.setOnClickListener(this)
if (totalBreak == 0){
txt_breakCount!!.visibility = View.GONE
}else{
txt_breakCount!!.setText(breakCount.toString() + "/" + totalBreak.toString())
}
timeLeftInMillis = (timerLength!! * 60000).toLong()
//start with time and progressbar complete
setProgressBarValues()
updateCountDownText()
startTimer()
}
override fun onClick(view: View?) {
when(view!!.id){
R.id.img_play -> {
if (timerStatus == TimerStatus.STARTED) {
pauseTimer()
} else {
startTimer()
}
}
R.id.img_replay -> resetTimer()
}
}
Bom, nossos intervalos (breaks) também terão contagem de tempo! Pensando nisso, seria bom não termos que repetir códigos que tanto o Timer quanto o Break usará, então vamos criar classes para podermos usa-la nos dois.
Primeiro vamos criar a classe TimerStatus:
enum class TimerStatus {
STARTED, STOPPED
}
Excluía a classe TimerStatus da class Timer. É agora que poderíamos ter usado ela direto da classe Timer para a Break, mas por convenção de uma estrutura mais limpa a separamos.
Segundo vamos criar a classe FormatText e passar nossa função “updateCountDownText” para ela, excluindo-a do Timer. Nossa classe FormatText precisará pegar o Context e o id do TextView onde será atualizado nosso tempo da Activity que a instanciar, assim como a função precisará de receber o “timeLeftInMillis”:
import android.app.Activity
import android.content.Context
import android.widget.TextView
import java.util.*
class FormatText(context: Context, text_time: Int) {
var txt_time:TextView = (context as Activity).findViewById(text_time)
fun updateCountDownText(timeLeftInMillis:Long){
val minutes: Int = (timeLeftInMillis.toInt() / 1000) / 60
val seconds: Int = (timeLeftInMillis.toInt() / 1000) % 60
val timeLeftFormatted: String =
java.lang.String.format(
Locale.getDefault(),
"%02d:%02d", minutes, seconds)
txt_time.setText(timeLeftFormatted)
}
}
Sem a função no Timer precisamos criar uma variável do tipo FormatText.
var formatText: FormatText? = null
Inicializa-la e chamar a função “updateCountDownTimer” (substitua em todos os lugares necessários) dela na função “initTimer”:
formatText = FormatText(this, R.id.txt_time_play)
formatText!!.updateCountDownText(timeLeftInMillis)
Vamos seguir as boas praticas e deixar todos os textos do app com strings resource. Abaixo estão todas que usaremos até a parte 2:
<resources>
<string name="message_finish_pomodoro">You finish your Pomorodo!</string>
<string name="title_finish_pomodoro">Congratulations</string>
<string name="title_atention">Attention</string>
<string name="action_yes">YES</string>
<string name="action_no">NO</string>
<string name="message_finish_break">Are you sure you want to leave your break?</string>
<string name="message_finis_timer">Are you sure you want to leave your pomodoro?</string>
<string name="action_ok">OK</string>
<string name="txt_timer">Timer</string>
<string name="note_break">It\'s time to take a break. Don\'t forget hydrate!</string>
<string name="leave_break">Leave Break</string>
<string name="start_pomodoro">START POMODORO</string>
<string name="title_breaks">Number of breaks:</string>
<string name="title_time">Time (minute):</string>
<string name="title_dialog">Set Time</string>
<string name="action_start">START</string>
</resources>
Obs.: Na parte 1 desse tutorial não tínhamos colocados os textos de descrições (que não mutáveis) em string resource, mas isso já foi atualizado! Caso você ainda não tenha feito retorne a ele!
Agora que nosso código esta mais “cheiroso” vamos partir para a parte 2!
Começando criando o vector “restore” que indicará que nosso intervalo começou:

Crie o Activity empty Break. E vamos editar o layout activity_break:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".Break">
<ImageView
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_marginTop="42dp"
android:src="@drawable/ic_restore"
android:layout_gravity="center_horizontal"/>
<TextView
android:id="@+id/txt_time_break"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="24dp"
android:text="02:00"
android:textAlignment="center"
android:textSize="45sp"
android:textStyle="bold"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:text="@string/note_break"
android:textAlignment="center"
android:textSize="24sp"/>
<Button
android:id="@+id/btn_leave_break"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/bg_btn"
android:text="@string/leave_break"/>
</LinearLayout>
Agora implementar nossas funções “startBreak” e “pauseBreak”, além do botão “leave break” que usaremos a classe View.OnClickListener para receber o evento de click que criará um AlertDialog perguntando se o usuário realmente deseja interromper a contagem ou não:
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.CountDownTimer
import android.view.View
import android.widget.Button
import androidx.appcompat.app.AlertDialog
class Break : AppCompatActivity(), View.OnClickListener {
var btn_leave_break: Button? = null
var formatText: FormatText? = null
var timerStatus = TimerStatus.STARTED
lateinit var countDownTimer: CountDownTimer
var timeLeftInMillis: Long = (5 * 60000).toLong()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_break)
initBreak()
startBreak()
}
private fun initBreak() {
btn_leave_break = findViewById(R.id.btn_leave_break)
btn_leave_break!!.setOnClickListener(this)
formatText = FormatText(this, R.id.txt_time_break)
formatText!!.updateCountDownText(timeLeftInMillis)
}
override fun onClick(view: View?) {
when(view!!.id){
R.id.btn_leave_break -> {
if (timerStatus == TimerStatus.STARTED){
pauseBreak()
val builder = AlertDialog.Builder(this)
builder.setTitle(getString(R.string.title_atention))
builder.setMessage(getString(R.string.message_finish_break))
builder.setPositiveButton(getText(R.string.action_yes)){ _, _ -> finish()}
builder.setNegativeButton(getString(R.string.action_no)){ _, _ -> startBreak()}
val dialog = builder.create()
dialog.show()
}else{
finish()
}
}
}
}
private fun startBreak() {
countDownTimer = object : CountDownTimer(timeLeftInMillis, 1000){
override fun onTick(millisUntilFinished: Long) {
timeLeftInMillis = millisUntilFinished
formatText!!.updateCountDownText(timeLeftInMillis)
}
override fun onFinish() {
finish()
}
}.start()
timerStatus = TimerStatus.STARTED
}
fun pauseBreak(){
countDownTimer.cancel()
timerStatus = TimerStatus.STOPPED
}
}
Falta acrescentar algumas funções ainda ao Timer:
- O “statusBreak” para monitorar quando os intervalos ainda existem para chamar a Activity Break e quando eles acabam para que o Pomodoro termine (daremos o aviso com o AlerDialog). E depois coloca-lo na função onFinish do CountDownTimer;
- O “onActivityResult” que fará o controle de acrescentar mais “um” a variável “breakCount” sempre que a contagem da Activity Break finalizar ou ser interrompida.
- O “onBackPressed” que criará um AlertDialog sempre que o usuário apertar o botão voltar no celular perguntando se realmente deseja encerrar todo o Pomodoro.
- O “onSupportNavigateUp” que fará o mesmo que o “onBackPressed“, mas no botão “Home Up” que vamos inserir no “onCreate” por com o código:
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
private fun startTimer() {
countDownTimer = object : CountDownTimer(timeLeftInMillis, 1000){
override fun onTick(millisUntilFinished: Long) {
timeLeftInMillis = millisUntilFinished
img_play!!.setImageResource(R.drawable.ic_pause)
formatText!!.updateCountDownText(timeLeftInMillis)
progressbar!!.setProgress((millisUntilFinished / 1000).toInt())
}
override fun onFinish() {
timerStatus = TimerStatus.STOPPED
resetTimer()
statusBreak()
}
}.start()
timerStatus = TimerStatus.STARTED
}
override fun onBackPressed() {
pauseTimer()
val builder = AlertDialog.Builder(this)
builder.setTitle(getString(R.string.title_atention))
builder.setMessage(getString(R.string.message_finis_timer))
builder.setPositiveButton(getString(R.string.action_yes)){ _, _ -> finish()}
builder.setNegativeButton(getString(R.string.action_no)){ _, _ -> startTimer()}
val dialog = builder.create()
dialog.show()
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(requestCode == 0){
breakCount++
txt_breakCount!!.text = breakCount.toString() + "/" + totalBreak.toString()
startTimer()
}
}
Assim fica toda nossa classe Timer:
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.CountDownTimer
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
class Timer : AppCompatActivity(), View.OnClickListener {
var txt_breakCount: TextView? = null
var img_play:ImageView? = null
var img_replay: ImageView? = null
var progressbar: ProgressBar? = null
var timerLength: Int? = null
var breakCount: Int = 0
var totalBreak: Int? = null
var formatText: FormatText? = null
var ACTIVITY_BREAK = 0
//CountDownTimer Control
//When start activity start playing
private var timerStatus = TimerStatus.STARTED
var timeLeftInMillis:Long = 0
lateinit var countDownTimer: CountDownTimer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_timer)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
init()
getData()
initTimer()
}
fun init(){
txt_breakCount = findViewById(R.id.txt_total_breaks)
progressbar = findViewById(R.id.progressBar)
img_play = findViewById(R.id.img_play)
img_replay = findViewById(R.id.img_replay)
}
fun getData(){
timerLength = intent.getIntExtra("TIME", 1)
totalBreak = intent.getIntExtra("BREAKS", 0)
}
private fun initTimer() {
img_play!!.setOnClickListener (this)
img_replay!!.setOnClickListener(this)
if (totalBreak == 0){
txt_breakCount!!.visibility = View.GONE
}else{
txt_breakCount!!.setText(breakCount.toString() + "/" + totalBreak.toString())
}
timeLeftInMillis = (timerLength!! * 60000).toLong()
//start with time and progressbar complete
setProgressBarValues()
formatText = FormatText(this, R.id.txt_time_play)
formatText!!.updateCountDownText(timeLeftInMillis)
startTimer()
}
override fun onClick(view: View?) {
when(view!!.id){
R.id.img_play -> {
if (timerStatus == TimerStatus.STARTED) {
pauseTimer()
} else {
startTimer()
}
}
R.id.img_replay -> resetTimer()
}
}
private fun setProgressBarValues(){
progressbar!!.setMax(timeLeftInMillis.toInt() / 1000)
progressbar!!.setProgress(timeLeftInMillis.toInt() / 1000)
}
fun statusBreak(){
if(breakCount == totalBreak){
val builder = AlertDialog.Builder(this)
builder.setTitle(getString(R.string.title_finish_pomodoro))
builder.setMessage(getString(R.string.message_finish_pomodoro))
builder.setPositiveButton(getString(R.string.action_ok)){ _, _ -> finish()}
val dialog = builder.create()
dialog.show()
}else{
val intent = Intent(this@Timer, Break::class.java)
startActivityForResult(intent, ACTIVITY_BREAK)
}
}
private fun startTimer() {
countDownTimer = object : CountDownTimer(timeLeftInMillis, 1000){
override fun onTick(millisUntilFinished: Long) {
timeLeftInMillis = millisUntilFinished
img_play!!.setImageResource(R.drawable.ic_pause)
formatText!!.updateCountDownText(timeLeftInMillis)
progressbar!!.setProgress((millisUntilFinished / 1000).toInt())
}
override fun onFinish() {
timerStatus = TimerStatus.STOPPED
resetTimer()
statusBreak()
}
}.start()
timerStatus = TimerStatus.STARTED
}
private fun pauseTimer(){
countDownTimer.cancel()
timerStatus = TimerStatus.STOPPED
img_play!!.setImageResource(R.drawable.ic_play)
}
private fun resetTimer(){
pauseTimer()
timeLeftInMillis = (timerLength!! * 60000).toLong()
setProgressBarValues()
formatText!!.updateCountDownText(timeLeftInMillis)
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
override fun onBackPressed() {
pauseTimer()
val builder = AlertDialog.Builder(this)
builder.setTitle(getString(R.string.title_atention))
builder.setMessage(getString(R.string.message_finis_timer))
builder.setPositiveButton(getString(R.string.action_yes)){ _, _ -> finish()}
builder.setNegativeButton(getString(R.string.action_no)){ _, _ -> startTimer()}
val dialog = builder.create()
dialog.show()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(requestCode == ACTIVITY_BREAK){
breakCount++
txt_breakCount!!.text = breakCount.toString() + "/" + totalBreak.toString()
startTimer()
}
}
}
Nosso app estilo Pomodoro esta pronto!!! Para ficar igual a técnica basta colocar o seekbar de tempo com o minimo de 25 (para iniciar o Timer com no minimo 25 minutos)!
Obrigada por acompanhar o conteúdo e caso tenha dúvidas ainda você pode assistir os vídeos com passo-a-passo dos tutoriais no nosso canal Dev Mundi.
O vídeo da parte 2 se encontra abaixo: