CountDownTimer

Timer App: CounDownTimer – Android Kotlin Tutorial Part 1

Nesse tutorial vamos aprender a fazer um Timer (Pomodoro) simples com as funções de start, pause e reset usando a classe CountDownTimer para realizar a contagem regressiva.

O que você vai aprender:

  1. Fazer a base de um contador que pode ser reutilizado em diferentes projetos
  2. Como utilizar a classe CountDownTimer
  3. Como formatar em horas um valor
  4. Utilizar biblioteca externa para customizar Seekbar
  5. Criar DialogFragment costumizado que também passe dados para uma activity
  6. Criar shapes com drawable e vetores
  7. Fazer um progressbar circular

Vamos começar adicionando a Lib do Bubble Seekbar:

//Bubble Seekbar
implementation 'com.xw.repo:bubbleseekbar:3.4-lite'

Deixaremos criar as strings resources que vamos usar na Parte 1:

<resources>
    <string name="txt_timer">Timer</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>

Agora precisamos adicionar um botão no layout do activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_start_pomodoro"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:text="@string/start_pomodoro"
        android:textStyle="bold"
        android:background="@drawable/bg_btn_main"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Para deixar no botão ais elegante vamos fazer um shape costumizado. Crie um novo Drawable com nome: bg_btn.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <gradient
        android:angle="0"
        android:startColor="#F0F0F0"
        android:endColor="#E9E7E7"/>

    <padding
        android:bottom="8dp"
        android:top="8dp"
        android:left="8dp"
        android:right="8dp"/>

    <stroke
        android:width="2dp"
        android:color="@color/colorPrimary"/>

</shape>

Para que nosso botão possa chamar o Dialog Personalizado vamos criar nosso layout seekbar_dialog.xml. Aqui usaremos nosso Bubble Seekbar:

<?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"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginBottom="16dp"
        android:layout_marginTop="16dp"
        android:text="@string/title_time"/>

    <com.xw.repo.BubbleSeekBar
        android:id="@+id/seekbar_time"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        app:bsb_bubble_color="@color/colorPrimary"
        app:bsb_bubble_text_color="#FFFFFF"
        app:bsb_max="50"
        app:bsb_min="1"
        app:bsb_progress="1"
        app:bsb_second_track_color="@color/colorPrimary"
        app:bsb_section_count="5"
        app:bsb_section_text_position="bottom_sides"
        app:bsb_show_progress_in_float="false"
        app:bsb_show_section_mark="true"
        app:bsb_show_section_text="true"
        app:bsb_show_thumb_text="true"
        app:bsb_track_color="#D8D8D8"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginBottom="16dp"
        android:layout_marginTop="16dp"
        android:text="@string/title_breaks"/>

    <com.xw.repo.BubbleSeekBar
        android:id="@+id/seekbar_breaks"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        app:bsb_bubble_color="@color/colorPrimary"
        app:bsb_bubble_text_color="#FFFFFF"
        app:bsb_max="10"
        app:bsb_min="0"
        app:bsb_progress="0"
        app:bsb_second_track_color="@color/colorPrimary"
        app:bsb_section_count="5"
        app:bsb_section_text_position="bottom_sides"
        app:bsb_show_progress_in_float="false"
        app:bsb_show_section_mark="true"
        app:bsb_show_section_text="true"
        app:bsb_show_thumb_text="true"
        app:bsb_track_color="#D8D8D8"/>

</LinearLayout>

Agora falta criar a classe CostumSeekbar para atribuir ao nosso seekbar_dialog. Essa classe será responsável por pegar os valores dos seekbars na função “onProgressChanged” da classe BubbleSeekbar.OnProgressChangedListener e manda-los para a activity do Timer:

import android.app.AlertDialog
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import androidx.fragment.app.DialogFragment
import com.xw.repo.BubbleSeekBar

class CustomSeekbar: DialogFragment() {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        val view = LayoutInflater.from(activity)
            .inflate(R.layout.seekbar_dialog, null)
        val alert = AlertDialog.Builder(activity)

        val seekbarTime: BubbleSeekBar = view.findViewById(R.id.seekbar_time)
        val seekbarBreaks:BubbleSeekBar = view.findViewById(R.id.seekbar_breaks)
        var time: Int = 1
        var breaks: Int = 0

        seekbarTime.setOnProgressChangedListener(
            object : BubbleSeekBar.OnProgressChangedListener{
                override fun onProgressChanged(progress: Int, progressFloat: Float) {
                    time = progress
                }

                override fun getProgressOnActionUp(progress: Int, progressFloat: Float) {

                }

                override fun getProgressOnFinally(progress: Int, progressFloat: Float) {

                }
            })

        seekbarBreaks.setOnProgressChangedListener(
            object : BubbleSeekBar.OnProgressChangedListener{
                override fun onProgressChanged(progress: Int, progressFloat: Float) {
                    breaks = progress
                }

                override fun getProgressOnActionUp(progress: Int, progressFloat: Float) {

                }

                override fun getProgressOnFinally(progress: Int, progressFloat: Float) {

                }
            })

        with(alert){

            setView(view)
            setTitle(getString(R.string.title_dialog))
            setPositiveButton(getString(R.string.action_start)){ _, _ -> onPositiveClick(time, breaks)}

        }

        return alert.create()
    }

    private fun onPositiveClick(time: Int, breaks: Int) {

        val i = Intent(activity!!.baseContext, Timer::class.java)
        i.putExtra("TIME", time)
        i.putExtra("BREAKS", breaks)
        startActivity(i)

    }

}

Ainda falta criar a activity Timer, mas antes já vamos colocar o botão na MainActivity chamando o DialogFragment:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val btn_start_pomodoro:Button = findViewById(R.id.btn_start_pomodoro)
        btn_start_pomodoro.setOnClickListener {

            val mySeekbarDialog = CustomSeekbar()
            mySeekbarDialog.show(supportFragmentManager, "")

        }

    }
}

Agora crie uma Activity Empty Timer. Com nosso Timer criado o erro na classe CustomSeekbar vai sumir. No layout do Timer teremos um progressbar circular, então vamos criar os Drawables necessários para isso.

Crie o Drawable progressbar_circle_shape.xml que representa o background do progressbar:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="ring"
    android:innerRadiusRatio="2.5"
    android:thickness="8dp"
    android:useLevel="false">

    <solid
        android:color="#D8D8D8"/>

</shape>

Crie o Drawable progressbar_circle.xml que é o shape que mostra o progresso no progressbar:

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromDegrees="270"
    android:toDegrees="270">

    <shape
        android:innerRadiusRatio="2.5"
        android:shape="ring"
        android:thickness="7dp"
        android:useLevel="true">

        <gradient
            android:angle="0"
            android:endColor="@color/colorPrimary"
            android:startColor="@color/colorPrimary"
            android:type="sweep"
            android:useLevel="false"/>

    </shape>

</rotate>

Precisamos também adicionar nossos 3 vetores (ic_play, ic_pause, ic_replay) através do Vector Asset disponível no Android Studio:

Seguiremos para a edição do nosso activity_timer.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    tools:context=".Timer">


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="@string/txt_timer"
        android:layout_centerHorizontal="true"
        android:textSize="35sp"
        android:textStyle="bold"/>

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="250dp"
        android:layout_height="250dp"
        android:layout_centerInParent="true"
        android:background="@drawable/progressbar_circle_shape"
        android:indeterminate="false"
        android:max="100"
        android:progress="100"
        android:progressDrawable="@drawable/progressbar_circle"
        android:textAlignment="center"/>

    <TextView
        android:id="@+id/txt_total_breaks"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0/10"
        android:textSize="18sp"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="8dp"
        android:layout_above="@+id/txt_time_play"/>

    <TextView
        android:id="@+id/txt_time_play"
        android:layout_width="136dp"
        android:layout_height="69dp"
        android:layout_centerInParent="true"
        android:text="10:00"
        android:textSize="45sp"
        android:textStyle="bold"
        android:textAlignment="center"/>

    <ImageView
        android:id="@+id/img_replay"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:src="@drawable/ic_replay"
        android:layout_centerHorizontal="true"
        android:layout_below="@+id/txt_time_play"/>

    <ImageView
        android:id="@+id/img_play"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_centerHorizontal="true"
        android:src="@drawable/ic_play"
        android:layout_below="@+id/progressBar"/>

</RelativeLayout>

Para terminar só falta configurarmos no Timer com contagem regressiva usando a classe do Android CountDownTimer que tem as funções:

  • onTick” = que fará a contagem em milésimos de segundos
  • onFinish” = que será executada ao termino da contagem

Também temos nossa função “updateCountDownText” que fará a conversão do valor recebido para minutos e segundos direto no TextView do Timer. Além das funções de “startTimer“, “pauseTimer” e “resetTimer” na contagem:

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 java.util.*

class Timer : AppCompatActivity() {

    var txt_time: TextView? = null
    var txt_breaks: TextView? = null
    var img_play:ImageView? = null
    var img_replay: ImageView? = null
    var progressbar: ProgressBar? = null
    var timerLength: Int? = null
    var breaksCount: Int = 0
    var totalBreaks: Int? = null

    //CountDownTimer Control

    private enum class TimerStatus{
        STARTED, STOPPED
    }

    //When start activity start playing
    private var timerStatus = TimerStatus.STARTED

    var timeLeftInMillis:Long = 1 * 60000
    lateinit var countDownTimer: CountDownTimer


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_timer)

        init()
        getData()

        img_play!!.setOnClickListener {
            if (timerStatus == TimerStatus.STARTED){
                pauseTimer()
            }else{
                startTimer()
            }
        }

        img_replay!!.setOnClickListener {
            resetTimer()
        }

        //start with time and progressbar complete
        updateCountDownText()
        setProgressBarValues()

    }

    fun init(){

        txt_time = findViewById(R.id.txt_time_play)
        txt_breaks = 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)
        totalBreaks = intent.getIntExtra("BREAKS", 0)

        if (totalBreaks  == 0){
            txt_breaks!!.visibility = View.GONE
        }else{
            txt_breaks!!.setText(breaksCount.toString() + "/" + totalBreaks.toString())
        }

        timeLeftInMillis = (timerLength!! * 60 * 1000).toLong()
        startTimer()

    }

    private fun setProgressBarValues(){
        progressbar!!.setMax(timeLeftInMillis.toInt() / 1000)
        progressbar!!.setProgress(timeLeftInMillis.toInt() / 1000)
    }

    private fun startTimer() {
        countDownTimer = object : CountDownTimer(timeLeftInMillis, 1000){

            override fun onTick(millisUntilFinished: Long) {
                timeLeftInMillis = millisUntilFinished
                img_play!!.setImageResource(R.drawable.ic_pause)
                updateCountDownText()
                progressbar!!.setProgress((millisUntilFinished / 1000).toInt())
            }

            override fun onFinish() {
                timerStatus = TimerStatus.STOPPED
                resetTimer()
            }

        }.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!! * 60 * 1000).toLong()
        setProgressBarValues()
        updateCountDownText()
    }

    private fun updateCountDownText(){
        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)
    }

}

Bom, assim terminamos a parte 1 do nosso tutorial! Na parte 2 faremos a configuração dos intervalos, fazendo assim, nosso app funcionar como um pomodoro!
Até a próxima e caso tenha dúvidas você também ver o vídeo do tutorial no canal do Dev Mundi lincado abaixo:

Video Tutorial Timer app CountDownTimer Android – Parte 1

Deixe um comentário