Search code examples
android-studioandroid-recyclerviewcountdowntimer

How to implement CountDownTimer in Recyclerview?


Hi I'm working on a small application which sets a countdown. Now I want this countdown to be seen in front of a background inside a recyclerview. So in the end a user will have set multiple countdown timers and these will be displayed with backgrounds inside a recyclerview.

What I have made right now is far from perfect, but it's in the right direction of what I want it to be. The only problem I have right now is, because I'm working with a recyclerview, the view (including the text with the countdown) will be recycled. So if I scroll down, the countdown will be reset. I think that's the problem I'm having but I have no clue how to solve it. This is the most important code:

The activity the recyclerview is in:

package anotherChallenge.example.criminalactivity;

import androidx.appcompat.app.AppCompatActivity; 
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Bundle;
import android.widget.DatePicker;
import com.example.criminalactivity.R;
import java.text.DateFormat;
import java.util.Calendar;

public class Overview extends AppCompatActivity {

private int[] allImages = {
        R.drawable.festive1_pictures,
        R.drawable.festive2_pictures,
        R.drawable.festive3_pictures,
        R.drawable.festive4_pictures,
        R.drawable.festive5_pictures,
        R.drawable.ferrari_materialistic_pictures,
        R.drawable.house_materialistic_pictures,
        R.drawable.boat_materialistic_pictures,
        R.drawable.rolex_materialistic_pictures,
        R.drawable.private_jet_materialistic_pictures,
        R.drawable.holiday1_pictures,
        R.drawable.holiday2_pictures,
        R.drawable.holiday3_pictures,
        R.drawable.holiday4_pictures,
        R.drawable.holiday5_pictures,
        R.drawable.meet1_pictures,
        R.drawable.meet2_pictures,
        R.drawable.meet3_pictures,
        R.drawable.meet4_pictures,
        R.drawable.meet5_pictures,
        R.drawable.other1_pictures,
        R.drawable.other2_pictures,
        R.drawable.other3_pictures,
        R.drawable.other4_pictures,
        R.drawable.other5_pictures,
};

private RecyclerView recyclerView2;
private DatePicker datePicker;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_overview);

  //        textClock.setFormat12Hour(null);
  //        textClock.setFormat24Hour("EEE/MMM d/yyyy   hh:mm:ss");

    recyclerView2 = findViewById(R.id.recyclerview2);
    datePicker = findViewById(R.id.datepicker);
    Calendar calendar = Calendar.getInstance();
    String currentDate = DateFormat.getDateInstance(DateFormat.FULL).format(calendar.getTime());
    System.out.println(currentDate);

 //        String filledInDate = datePicker.getDayOfMonth() + "/" + datePicker.getMonth() + "/"+
 //        datePicker.getYear();


    adapterForOverview adapter = new adapterForOverview(this, allImages);
    recyclerView2.setAdapter(adapter);
    recyclerView2.setLayoutManager(new LinearLayoutManager(this));

}

}

Adapter for the recyclerview:

package anotherChallenge.example.criminalactivity;

import android.content.Context;
import android.os.CountDownTimer;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;
import com.example.criminalactivity.R;


public class adapterForOverview extends RecyclerView.Adapter<adapterForOverview.MyViewHolder> {
AddItem addItem;
int[] images;
Context context;
CustomAdapterCardView customAdapterCardView;

public adapterForOverview(Context context, int[] images) {
    this.context=context;
    this.images=images;

    addItem = new AddItem();
  //        customAdapterCardView = new CustomAdapterCardView(context, 
customAdapterCardView.getArrayList());
}

@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
//        holder.cardView.setCardBackgroundColor(Color.parseColor("#696969"));
    new CountDownTimer(30000,1000) {
        @Override
        public void onTick(long millisUntilFinished) {
            holder.myTextview.setText("The title will be here \n" + millisUntilFinished/1000);
        }

        @Override
        public void onFinish() {
            holder.myTextview.setText("Achieved!");
        }
    }.start();

      holder.myImage.setImageResource(images[position]);
   //        holder.myImage.setImageDrawable(addItem.getDrawable());
}

@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    LayoutInflater layoutInflater = LayoutInflater.from(context);
    View view = layoutInflater.inflate(R.layout.itemsinoverview,parent,false);
    return new MyViewHolder(view);
}

@Override
public int getItemCount() {
    return images.length;
}

public class MyViewHolder extends RecyclerView.ViewHolder{

    CardView cardView;
    ImageView myImage;
    TextView myTextview;

    public MyViewHolder(@NonNull View itemView) {
        super(itemView);
        cardView = itemView.findViewById(R.id.cardview2);
        myImage = itemView.findViewById(R.id.imageView);
        myTextview = itemView.findViewById(R.id.textviewOfCountdown);
    }
 } 
 }

XML Code of the item inside the recyclerview:

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
>

<androidx.cardview.widget.CardView
    android:id="@+id/cardview2"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:layout_marginHorizontal="8dp"
    android:layout_marginVertical="4dp"
    >

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintlayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/imageView"
        android:scaleType="fitXY"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        />

    <TextView
        android:id="@+id/textviewOfCountdown"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:gravity="center"
        android:textColor="#ffffff"
        android:textSize="25sp"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>

Solution

  • Explanation:

    As you know android's recyclerView recycles its views and reuses its viewHolders by calling onBind each time to update the item's data. I noticed that in onBindViewHolder you create a CountDownTimer every time any data is bound, so you end up with multiple timers updating the same ViewHolder.

    Bad solution:

    One solution would be to make the adapter's items non-recyclable, but that wouldn't be optimal and negates the recycling ability of the recyclerview.

    Good Solution:

    The solution is to keep a reference to your timer inside your viewHolder called MyViewHolder. Then in onBindViewHolder check if the timer has already been instantiated. If yes, then cancel the previous timer and afterwards create again a new timer. If not, then there's no need to cancel anything you just proceed to the creation of a new timer object for the first time.

    public void onBindViewHolder(@NonNull MyViewHolder holder, final int position) {
        
        if (holder.timer != null) {
            holder.timer.cancel();
        }
        holder.timer = new CountDownTimer(30000, 1000) {
            ...
        }.start();
    }
    
    public static class MyViewHolder extends RecyclerView.ViewHolder {
        ...
        CountDownTimer timer;
        ...
    }